Test-Driving Kotlin in ZK (Part 2)
This time around, we see how to create test cases with Kotlin for your ZK apps. Check out some suggestions and limitations to be aware of.
Join the DZone community and get the full member experience.Join For Free
As promised in Part 1, it's now time to build a more useful example to try out if/how data-classes work in a ZK application. Finally wrapping up to check how Kotlin might help when writing the oh-so-dreaded test cases.
Building a Simple CRUD UI
The screenshot on the left shows a basic CRUD UI (without any fancy styling, just concentrating on the essentials) for Persons (with an Address).
As the acronym indicates, it is possible to Create new and Read/Update existing Persons, as well es Deleting them.
Using ZK's-MVVM pattern should be straightforward, so this article can concentrate on how to use idiomatic Kotlin features during the implementation and how to overcome obstacles on the way.
Let's dive into the implementation.
Using Data Classes
Data classes are created in a breeze — just define the properties and their default values.
I think I don't need to paste the equivalent Java code here ... everyone knows the ceremony (generate getters, setters, constructors, equals, hashcode, toString, clone, etc). All these come for free (with reasonable defaults) when using a data-class.
property-binding: YES works instantly -> the implicit getters/setters are fully compatible - so one can use them in bind-annotations and EL-expressions.
form-binding: YES (but ... not instantly)
Naively I tried this ...
... resulting in the following exception.
"OK", I thought. Fair enough, classes are
final by default in Kotlin (preventing Java-proxies from generating subclasses dynamically). No problem, I just declare it
open instead, but then ...
... better RTF(riendly)M.
Luckily, especially for those cases, there are compiler plugins (called allopen and noarg) that generate extensible classes compatible with Java-proxies (also useful when dealing with Hibernate entity classes or Spring proxies).
The allopen plugin makes compiled classes
open so they can be extended at runtime, but still keeps them
final to avoid unintended programming mistakes.
In a similar way, the noarg plugin adds a no-arg-constructor, allowing runtime instantiation of generated proxy classes without exposing it to the developer unnecessarily.
We all know boilerplate code(-comments) like this, which complicate our well thought-out API - allowing for
Person instances with uninitialized properties if used improperly:
The noarg pugin solves this nicely. (I know Lombok has it too — here I am talking about Kotlin — but always good to have alternatives).
To enable the plugins just add them to build.gradle:
In my example, both plugins are configured to only affect classes annotated with my own
@FormBean annotation (other configuration methods, e.g. package scanning, also exist).
After adding the annotations, the data-classes (
Address) now elegantly support form-binding.
Amazing! For a short moment, I thought about giving up on data-classes. This was, again, a very positive surprise on how much Kotlin cares about interoperability with existing libraries.
Without the blocking obstacles the remaining UI code is trivial, just see for yourself:
1. Worth mentioning is the commands-object in PersonCrudViewModel. Aggregating the commands, the ViewModel wants to expose to the ZUL file (almost don't need to mention anymore: in a refactoring-safe manner).
In the ZUL file, they can be accessed via
@command(vm.commands.SAVE) or as in my example shortened by a reference binding
@command(cmds.SAVE)(personCrud.zul Line 11 / Line 45).
2. Kotlin's (setter/getter functions) can be used for calculated view model properties with dependencies.
personToEdit holds the current person being edited. Whenever it is changed in the code, I want the UI to update (i.e. all data-bindings referring to this instance to reload). The depending flag
editing is notified programmatically to render/re-render/detach the editPersonForm accordingly.
In Java, this can be implemented like that:
The hard-coded Strings (mentioned in the previous article) corresponding to property names could break during refactoring without the compiler complaining.
The same in Kotlin:
Now the compiler and IDE can help reliably when updating property names or trying to find out which other functions use a specific field or which field depends on another.
In order to update the UI, all you need is to assign a person:
personToEdit = person.
To stop editing setting it to null:
personToEdit = null.
In each case, the setter-function is called updating the field, and notifying the UI to reflect the changes automatically.
Testing With ZATS
ZK comes with a testing framework called ZATS, which allows testing ViewModels/Controllers by mimicking the user agent without the complexities and maintenance effort of real browser tests (e.g. using Selenium — still Selenium tests are important to verify the final result is working in each targeted browser). The goal with ZATS is to test as much interaction between ZK and your application as possible without having to deal with real browser specifics. As a benefit, the test-cases run much faster — encouraging to run them more often during development.
Simplified spoken the ZATS API serves 2 functions:
- Query components on a page (find and access their state).
- Trigger events (mimic user interaction — resulting in UI changes).
In Java, a simple test case may look like this:
The same in Kotlin ... Works, but looks a little ugly — maybe hard to read later on.
Unfortunately — not being originally designed for Kotlin — ZATS uses a reserved word,
as as a function name so it needs to be escaped using back ticks
`as`. On top of that, the references to Java classes aren't beautiful either. With a simple extension function for the
ComponentAgent interface, this can be streamlined using an Inline Function with Reified Type Parameters (sounds frightening at first — but is well-explained). I often miss such a feature in Java — maybe Project Valhalla will add this in future Java versions.
I am not fully certain that this is the simplest way to do this. At least after implementing this function in one place, the remaining test case becomes a lot easier to read (IMO).
To increase maintainability, the component selectors can be defined above the test functions, which makes them reusable in multiple test methods. Adjusting them in one place should be sufficient when pages are restructured or component IDs change.
At the same time, repeated component casting and property access can be extracted into additional extension functions (here done by
asLabel). I might be over-engineering things a little, so decide for yourself what's useful for your eyes.
nameInput.input("Tester") will actually call
this refers to the
DesktopAgent object returned by
I Want a More Complex Example
Going back the
PersonCrud ... a test case for all CRUD-functions of the above example can be implemented in just under 100 lines without looking too cryptic. See for yourself: PersonCrudTest.kt.
This includes chained component selectors (e.g. to get the delete button after the 2nd person in the
personList — Line 59)
... or querying elements inside other elements using a nested
personEdit is a calculated value, its result can change with each evaluation. That's the reason we can use it multiple times to assert changes between NULL and non-NULL values. We can't accidentally reuse a stale value.
In lines 5 and 14,
assert[Not]Null(personEdit) will query the
personEdit each time to check the
editPerson element is actually removed from the page after clicking the save-button (in line 12).
In the end, this was an insightful experience for me and I am looking forward to future language developments be it Kotlin or Java 10, 11 ... or something different. It's great to see how much even a modern programming language such as Kotlin cares about existing APIs/Frameworks allowing for a smoother transition or, as in my case, just experimenting without having to relearn everything from scratch.
As usual, the full source code is available on github.com/zkoss-demo/zk-kotlin with running instructions.
I am looking forward to any comments or suggestions.
Opinions expressed by DZone contributors are their own.