Note: see the link below for the English version of this article.

https://duongnt.com/kluent

Kluent - FluentAssertions cho Kotlin

Khi viết unit test trong C#, tôi thường dùng library FluentAssertions để viết assertion. Bây giờ khi viết test cho Java/Kotlin, tôi luôn thấy thiếu thiếu khi không thể dùng library này. Nhưng thật may là Kotlin có một library gọi là Kluent với cùng chức năng. Hôm nay, chúng ta sẽ tìm hiểu cách viết test bằng Kluent.

Cách cài đặt

Các bạn có thể cài Kluent bằng Gradle. Ta chỉ cần thêm dòng sau đây vào file build.gradle.kts.

dependencies {
    testImplementation("org.amshove.kluent:kluent:<version number>")
}

Ví dụ:

dependencies {
    testImplementation("org.amshove.kluent:kluent:1.72") // will install version 1.72
}

Assertion trong Kluent trông như thế nào?

Ví dụ đơn giản nhất

Giả sử ta có hàm sau đây.

infix fun Int.plus(number: Int) = this + number

Ta có thể dùng hàm assertEquals trong JUnit để viết test.

@Test
fun `should add two number`() {
    assertEquals(2, 1 plus 1)
}

Nếu dùng Kluent thì assertion của ta sẽ nghe tự nhiên hơn.

(1 plus 1).shouldBe(2)

Phiên bản sử dụng backtick trông còn dễ hiểu hơn nữa.

1 plus 1 `should be` 2

Ví dụ phức tạp hơn với exception

Bây giờ ta sẽ test một hàm luôn sinh exception.

fun blowUp(): Nothing = throw UnsupportedOperationException("This method is a bomb")

Code bình thường để kiểm tra rằng hàm đã sinh UnsupportedOperationException với đúng thông báo lỗi sẽ trông như sau.

val ex = assertFailsWith<UnsupportedOperationException> {
    blowUp()
}
assertEquals("This method is a bomb", ex.message)

Còn bản sử dụng Kluent thì trông giống như một câu văn bình thường.

{ blowUp() } `should throw` UnsupportedOperationException::class `with message` "This method is a bomb"

Có lẽ các bạn cũng đã thấy là Kluent giúp assertion của ta trở nên dễ hiểu hơn. Tuy nhiên trong những phần sau ta sẽ thấy là nó không chỉ thay đổi syntax của assertion.

Kluent có hàm extension để hỗ trợ nhiều kiểu dữ liệu

Những hàm extension của Kluent không chỉ làm assertion dễ hiểu hơn, nó còn giúp thông báo lỗi trở nên rõ ràng hơn. Ta sẽ điểm qua một số method của kiểu StringInt.

Xác nhận là một String không null và không chỉ gồm khoảng trắng

Với Kluent, việc viết assertion để xác nhận rằng một String? không phải là null hay chỉ gồm khoảng trắng là việc hết sức đơn giản.

target.`should not be null or blank`()

Trong khi đó, mặc dù ta có hàm assertNotNull, ta lại không có hàm assertNotNullOrBlank. Để viết assertion trên mà chỉ dùng thư viện có sẵn, ta phải bổ sung một chút logic.

assertTrue(!target.isNullOrBlank())

Hoặc

assertFalse(target.isNullOrBlank())

Bây giờ ta sẽ thử làm test chạy lỗi.

val target: String? = null

Đây là thông báo lỗi từ hàm assertTrue.

expected: <true> but was: <false>

Thông báo này không mấy dễ hiểu phải không? Ta phải đọc code thì mới tìm được nguyên nhân thực sự. Trong khi đó, Kluent cho ta biết nguyên nhân ngay trong thông báo lỗi.

Expected non null value, but value was null
(or "Expected the CharSequence to not be blank" if target only consists of blank space)

Xác nhận là một giá trị Int nằm trong khoảng cho trước

Đây là code thông thường để kiểm tra rằng một giá trị Int chỉ nằm trong khoảng 0..99. Ta lại phải sử dụng hàm assertTrue.

assertTrue(target in (0..99))

Còn với Kluent thì ta có thể dùng hàm extension.

target.`should be in range`(0..99)

Và có lẽ là các bạn cũng đoán được rằng khi target == 101, thông báo lỗi của Kluent là dễ hiểu hơn.

Expected 101 to be between (and including) 0 and 99

Tương phản với thông báo chung chung của assertTrue.

expected: <true> but was: <false>

Kluent cho phép nối nhiều assertion thành chuỗi

Đôi khi, ta cần tới nhiều bước để xác nhận một kết quả. Trong ví dụ dưới đây, ta kiểm tra xem một object String? có chứa giá trị định trước không.

fun readString(): String? = //... this method returns a String?

@Test
fun `should have expected value`() {
    val rs = readString()
    assertNotNull(rs)
    assertEqualsIgnoringCase("duong", rs!!)
}

Ta không những phải gọi 2 hàm assertion riêng rẽ, mà ta còn phải dùng operator !! để chỉ ra rằng rs không thể là null trong câu assertion thứ hai. Thay vào đó, ta có thể dùng Kluent để viết code như dưới đây.

@Test
fun `should have expected value`() {
    val rs = readString()
    rs.`should not be null`().shouldBeEqualToIgnoringCase("duong")
}

Kluent đủ thông minh để nhận ra rằng khi ta đến được hàm shouldBeEqualToIgnoringCase thì rs không thể là null được nữa.

Kluent giúp việc dùng property để so sánh object trở nên đơn giản hơn

Đây là một tình huống mà tôi thấy rằng Kluent rất hữu ích. Hãy xét class dưới đây.

class Point(val x: Float, val y: Float)

Giả sử ta muốn coi tất cả các object với cùng giá trị xy là bằng nhau. Nếu class của ta là data class thì ta có thể dùng hàm assertEquals. Nhưng vì Point là class bình thường nên assertEquals sẽ chỉ kiểm tra xem 2 object có trỏ đến cùng 1 vùng memory hay không. Code đúng để so sánh object bằng property là như sau.

@Test
fun `native equivalent correct`() {
    assertEquals(p1.x, p2.x)
    assertEquals(p1.y, p2.y)
}

Nếu class của ta có nhiều property thì phương pháp trên là không phù hợp. Thật may là với Kluent ta có thể thay thế tất cả các assertion ở trên bằng hàm shouldBeEquivalentTo. Chú ý: ta cần cho phép sử dụng hàm còn đang thử nghiệm để dùng hàm trên.

@OptIn(ExperimentalStdlibApi::class)
@Test
fun `kluent equivalent`() {
    p1.shouldBeEquivalentTo(p2)
}

Kết thúc

Tôi cho rằng một trong những thử thách khi chuyển sang một ngôn ngữ mới là ta không sử dụng được những tool thường dùng. Thật may là với Kluent tôi, vẫn có thể viết assertion theo như cách quen thuộc trong C#.

A software developer from Vietnam and is currently living in Japan.

One Thought on “Kluent – FluentAssertions cho Kotlin”

Leave a Reply