Note: see the link below for the English version of this article.
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 String
và Int
.
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ị x
và y
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#.
One Thought on “Kluent – FluentAssertions cho Kotlin”