Note: phiên bản Tiếng Việt của bài này ở link dưới.
https://duongnt.com/kluent-vie
When writing C# unit tests, I usually assert the results with the FluentAssertions library. When writing Java/Kotlin tests, I often miss its expressive and elegant syntax. Fortunately, I found a Kotlin library called Kluent with similar functionalities. Today, we will see how Kluent can help us write better unit tests.
Installation
You can install Kluent with Gradle. Just add the following line into your build.gradle.kts
file.
dependencies {
testImplementation("org.amshove.kluent:kluent:<version number>")
}
For example:
dependencies {
testImplementation("org.amshove.kluent:kluent:1.72") // will install version 1.72
}
What does a Kluent assertion look like?
The simplest example
Let’s say we have this function.
infix fun Int.plus(number: Int) = this + number
We can write a test case for it using the assertion functions of JUnit.
@Test
fun `should add two number`() {
assertEquals(2, 1 plus 1)
}
An assertion in Kluent, however, is more similar to natural language.
(1 plus 1).shouldBe(2)
Or even better with the backtick version.
1 plus 1 `should be` 2
A more complicated test with exception
This time we test a function that always throws an exception.
fun blowUp(): Nothing = throw UnsupportedOperationException("This method is a bomb")
The native code to verify that an UnsupportedOperationException
was thrown with the correct message looks like this.
val ex = assertFailsWith<UnsupportedOperationException> {
blowUp()
}
assertEquals("This method is a bomb", ex.message)
While the Kluent version sounds just like a human sentence.
{ blowUp() } `should throw` UnsupportedOperationException::class `with message` "This method is a bomb"
Already, we can see that Kluent makes complicated assertions more readable. But in the following sections, we will see that it is much more than just simple syntactic sugar.
Kluent has extension methods to support multiple data parameter types
Kluent‘s extension methods not only make assertions more readable, they also result in clearer error messages. We will look at a few methods for the String
and Int
type.
Verify that a String is not null or blank
It’s easy to write an assertion to check if a String?
is not null or blank with Kluent.
target.`should not be null or blank`()
But while there is an assertNotNull
function, there is no assertNotNullOrBlank
one. To write the same assertion with native code, we have to add some logic.
assertTrue(!target.isNullOrBlank())
Or
assertFalse(target.isNullOrBlank())
Now let’s make the test fail.
val target: String? = null
Below is the error message from assertTrue
.
expected: <true> but was: <false>
That’s not very readable, is it? We have to dig into the code to find the actual cause. Meanwhile, Kluent tells us what went wrong right in the message.
Expected non null value, but value was null
(or "Expected the CharSequence to not be blank" if target only consists of blank space)
Verify that a number is within a range
This is the native code to check if an Int
falls in the 0..99
range. Once again, we have to use assertTrue
.
assertTrue(target in (0..99))
While the Kluent version can just use an extension method.
target.`should be in range`(0..99)
And as you can imagine, when target == 101
, the message from Kluent is much clearer.
Expected 101 to be between (and including) 0 and 99
Versus a generic one from assertTrue
.
expected: <true> but was: <false>
Kluent allows assertion chaining
Sometimes, we need to verify a result in multiple steps. The example below is to check if a String?
object has an expected value.
fun readString(): String? = //... this method returns a String?
@Test
fun `should have expected value`() {
val rs = readString()
assertNotNull(rs)
assertEqualsIgnoringCase("duong", rs!!)
}
Not only do we need two separate assertions, we also need the !!
operator in the second assertion to indicate that rs
is not null
in that case. At the same time, we can write the following code with Kluent
.
@Test
fun `should have expected value`() {
val rs = readString()
rs.`should not be null`().shouldBeEqualToIgnoringCase("duong")
}
As we can see, Kluent is smart enough to understand that after the first assertion, rs
can no longer be null
.
Kluent makes comparing objects by properties much easier
This is another scenario where I’ve found Kluent to be useful. Consider the class below.
class Point(val x: Float, val y: Float)
What if we want to consider all objects with the same value of x
and y
to be equal? If this is a data class then we can just use assertEquals
. But because Point
is a normal class, assertEquals
will only check if two objects reference the same memory. The correct way to compare objects by properties is shown below.
@Test
fun `native equivalent correct`() {
assertEquals(p1.x, p2.x)
assertEquals(p1.y, p2.y)
}
This can get ugly quickly when we have a lot of properties to check. However, with Kluent we can replace all those assertions with the method shouldBeEquivalentTo
. Note: we need to enable experimental features to use this method.
@OptIn(ExperimentalStdlibApi::class)
@Test
fun `kluent equivalent`() {
p1.shouldBeEquivalentTo(p2)
}
Conclusion
I think one of the difficulties we face when switching to a new language is that we no longer have our favorite tools at our disposal. Luckily, I can still write assertions the way I used to with the help of Kluent.
One Thought on “Kluent – FluentAssertions for Kotlin”