When I first learned about the concept of writing unit tests, I find them lame and wonder what’s the benefit. I can’t really comprehend the benefit of doing so, especially the developer who writes the code also writes the test. The test will definitely pass!
fun plus(valueA: Int, valueB: Int) Int {
return valueA + valueB
}
@Test
fun `test plus`() {
val x = plus(1, 2)
assertEqual(3, x)
}
Later, I learned that unit-tests provide assurance that our future code changes don’t break the logic. That’s what we learn in college.After years of development, now I found out it has much more value beyond testing. Let me share with you with some (perhaps oversimplified) examples, and hope I get what it helps make it clear.
What do I mean corner cases? Don’t we think about it when we code when we are given the spec?Let’s look at the spec
Program a function that generates a factorial number. Given an X value, the
Factorial(A) is Ax(A-1)x(A-2)x … 3x2x1 e.g. Given 5!, the value should be 5x4x3x2x1 = 120.
Cool, let write the code which covers the factorial spec fully:
fun factorial(value: Int) : Int {
var temp = value
var result = 1
while (temp > 0) {
result *= temp--
}
return result
}
Without writing a unit-test for it, the above looks all good.Let’s try to write unit-test for it
@Test
fun `test factorial`() {
assertEqual(factorial(5), 120)
assertEqual(factorial(4), 24)
assertEqual(factorial(3), 6)
assertEqual(factorial(2), 2)
assertEqual(factorial(1), 1)
assertEqual(factorial(0), 1)
// Ops, whataboout when it is -1?
}
We then discover some outside the scope cases to cover, i.e value < 0. If it is not in the spec (aka acceptance criteria), we then will need to have a discussion with how it should perform.The above is an oversimplified example. If you have been practicing writing unit tests before, this is something you’d surely experienced.
Each unit test is used to test a particular scenario of a function. However, if it turns out there are too many assertions are required for a scenario, some updates are required on the code.E.g. a function that returns both min and max value in a pair.
fun minMax(list: List<Int>): Pair<Int, Int> {
list.ifEmpty {
throw Throwable("list should not be empty")
}
var min = list[0]
var max = list[0]
list.forEach {
if (min > it) min = it
if (max < it) max = it
}
return Pair(min, max)
}
When we’re doing some testing, we notice for each scenario we’ll need to have more than one assertion.
@Test
fun `test minMax`() {
val (min, max) = minMax(listOf(1, 2, 3, 4))
Assert.assertEquals(min, 1)
Assert.assertEquals(min, 4)
}
This might raise an eyebrow. Perhaps we should have smaller responsibility for the function and pass the responsibility to other functions
fun minMax(list: List<Int>): Pair<Int, Int> {
list.ifEmpty {
throw Throwable("list should not be empty")
}
return Pair(min(list),max(list))
}
fun min(list: List<Int>): Int {
var min = list[0]
list.forEach {
if (min > it) min = it
}
return min
}
fun max(list: List<Int>): Int {
var max = list[0]
list.forEach {
if (max < it) max = it
}
return max
}
The writing unit-test process exposes the need for a single responsibility function.
#computer-science #software-testing #software-engineering #programming #software-development #testing