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.

Identify corner cases that are not in the spec

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.

Discover a function that does too many things

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

Writing Unit Test is Not Just for Testing
1.15 GEEK