DEV Community

Sinuhe Jaime Valencia
Sinuhe Jaime Valencia

Posted on

Unit testing with JUnit

I already explained that writing tests is not that hard
but I got some comments about it… I can summarize those comments in just one:

Where are the code examples?

So this time I will focus more in the code part with some simple examples.

Note: I won't follow TDD, BDD or any specific methodology thingy for this article. I want to explain how to think tests and how to write them as a first approach. Also worth to mention once more: I'll be providing most of the code in Kotlin for JVM, the concepts still apply for other tech.

Unit Tests

When you write code, you want to see it working, you want to see magic that formed in your mind working in the computer, you want to corroborate that everything is ready for the next steps in you NITRO-TURBO-SUPER-MEGA-AWESOME-PROJECT. But running the whole thing every single time, doing a compilation of the whole dependent code, passing previous screens/steps to arrive at the part you want to test, or simply adding one thousand logs as part of the process to check everything works is annoying and takes a lot of time. Also, you need to do this for every big change (or even smaller changes) to validate this new thing doesn't break other stuff.

Writing Unit tests can save you from this. A unit test is a single test that validates the smallest part of a system. It generally doesn't require too much setup and it is easy to read. Performing a unit test involves checking that one single functionality works as expected for both good and bad cases.

Let's begin with a simple (and classic) example: FizzBuzz.

The rules for FizzBuzz are quite simple:

A group of children are counting numbers, they go in turns and say something depending on the number they got:

  1. When number is multiple of 3 they should say "Fizz" instead of the number

  2. When number is multiple of 5 they should say "Buzz" instead of the number

  3. When number is both multiple of 3 and 5 they should say "FizzBuzz" instead of the number

  4. Otherwise they just say the number

These rules seem easy and fun, so how does it go for our code? Well, we decided to go for a simple function:

fun fizzBuzz(countUntil: Int): List<String>
Enter fullscreen mode Exit fullscreen mode

This function will work correctly for every single element of the FizzBuzz game and will give us back the results. To keep the focus in the tests I removed the body of this function.

Our setup

Imagine that the previous function is located at filesystem in: project/src/main/kotlin/net/sierisimo/games/FizzBuzz.kt

Then our tests will be at: project/src/test/kotlin/net/sierisio/games/FizzBuzzTest.kt

This follows the standard of Java and is what Kotlin suggests in the official docs.

We are going to use JUnit 5 for our tests, so you should check the official docs if something's missing on this article. The only thing you need to know by now is that JUnit is a tool for running test frameworks on the JVM. It will help us run our tests and do some checks.

Let's write our tests cases!

Valid cases

First of all we need to focus on valid cases to check the function works as expected (quotes added to represent they are Strings):

  1. When we pass the number 1 we should get back: ["1"]
  2. When we pass the number 3 we should get back: ["1","2","Fizz"]
  3. When we pass the number 5 we should get back: ["1","2","Fizz","4","Buzz"]
  4. When we pass the number 60 we should get back: ["1", "2", "Fizz", "4", "Buzz", "Fizz", "7", "8", "Fizz", "Buzz", "11", "Fizz", "13", "14", "FizzBuzz", "16", "17", "Fizz", "19", "Buzz", "Fizz", "22", "23", "Fizz", "Buzz", "26", "Fizz", "28", "29", "FizzBuzz", "31", "32", "Fizz", "34", "Buzz", "Fizz", "37", "38", "Fizz", "Buzz", "41", "Fizz", "43", "44", "FizzBuzz", "46", "47", "Fizz", "49", "Buzz", "Fizz", "52", "53", "Fizz", "Buzz", "56", "Fizz", "58", "59", "FizzBuzz"]

This list can go on and on. For now we are going to stay with these four cases.

The first thing is to create a class that will hold our tests and set some methods on it to represent our valid cases:

import org.junit.jupiter.api.Test

class FizzBuzzTest {
    @Test
    fun whenWePass1WeGotASingleItemList() {
        //…
    }

    @Test
    fun whenWePass3TheLastItemIsFizz(){
        //…
    }

    @Test
    fun whenWePass5TheLastItemIsBuzz(){
        //…
    }

    @Test
    fun whenWePass60TheLastElementIsFizzBuzz(){
        //…
    }
}
Enter fullscreen mode Exit fullscreen mode

Things you can notice in this class:

  • The annotation @Test before each method. This tells JUnit which methods are test cases and should run individually.
  • Names are longer and descriptive

There's also the chance in Kotlin along with JUnit 5 to use the backtick notation for method names:

internal class FizzBuzzTest {
    @Test
    fun `when 1 is passed a single item list is returned`() {
        //…
    }
}
Enter fullscreen mode Exit fullscreen mode

Or if the names in this notation aren't what you like, you can also just use an annotation to put a fancy name on the test report:

@DisplayName("when 3 is passed the last item is 'Fizz'")
@Test
fun whenWePass3TheLastItemIsFizz(){
    //…
}
Enter fullscreen mode Exit fullscreen mode

The style depends on you and there are even ways to make the displaying more fancy
or complex.

Once we have the tests, let's say what parts are involved in each one.

I generally think on a test like 3 parts:

@Test
fun myTestCase(){
  // Setup of data
  // Call to my SUT (System Under Test)
  // Assertions/Verifications
}
Enter fullscreen mode Exit fullscreen mode

Our first test then goes:

@Test
fun when1IsPassedASingleItemListIsReturned() {
    //Setup of data:
    val limit = 1
    //Call
    val actual = fizzBuzz(limit)
    //Assertions/Verification
    assertTrue(actual.isNotEmpty())

    assertEquals(1, actual.size)
    assertEquals("1", actual.first())
}
Enter fullscreen mode Exit fullscreen mode

If you run this on IntelliJ/Android Studio or with Gradle/Maven you will notice if your function is working.

The assertions like assertEquals and assertTrue are included with JUnit 5 but you can search the internet for other assertion libraries that include a more versatile and variant set of assertion (like AssertJ)

Here's a simple example of how the class can end with the tests:

package net.sierisimo.games

import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test

internal class FizzBuzzTest {
    @DisplayName("when 1 is passed a single item list is returned")
    @Test
    fun when1IsPassedASingleItemListIsReturned() {
        //Setup of data:
        val limit = 1
        //Call
        val actual = fizzBuzz(limit)
        //Assertions/Verification
        assertTrue(actual.isNotEmpty())

        assertEquals(1, actual.size)
        assertEquals("1", actual.first())
    }

    @DisplayName("when 3 is passed the last item is 'Fizz'")
    @Test
    fun whenWePass3TheLastItemIsFizz() {
        val limit = 3

        val expected = "Fizz"
        val actual = fizzBuzz(limit)

        assertTrue(actual.isNotEmpty())
        assertEquals(expected, actual.last())
    }

    @DisplayName("when 5 is passed the last item is 'Buzz'")
    @Test
    fun whenWePass5TheLastItemIsBuzz() {
        val limit = 5

        val expected = "Buzz"
        val actual = fizzBuzz(limit)

        assertTrue(actual.isNotEmpty())
        assertEquals(expected, actual.last())
    }

    @Test
    fun whenWePass60TheLastElementIsFizzBuzz() {
        val limit = 60

        val expected = "FizzBuzz"
        val actual = fizzBuzz(limit)

        assertTrue(actual.isNotEmpty())
        assertEquals(expected, actual.last())
    }
}
Enter fullscreen mode Exit fullscreen mode

Invalid Cases

Testing for the happy path is great and shows that the function works correctly, but we know users don't work like that. They don't follow the happy path, they always try to break our stuff and they send weird stuff to our code.

The best thing we can do is to write tests for invalid cases too. The first ones that come to mind are:

  1. Zero is not a valid parameter and the function should return an empty list: []
  2. Negative numbers are not valid parameters and function should return empty list: []

Optional for other languages: Checking the data type is correct is also worth testing. In the case of Kotlin there's no need because this is checked at compile time.

These two cases are quite easy and we can even put them in a single test (and introduce new annotations):

@ParameterizedTest
@ValueSource(ints = [0, -1, -50, -1000, -123459, Int.MIN_VALUE])
fun whenLimitIsLessOrEqualThanZeroReturnsEmptyList(limit: Int) {
    val actual = fizzBuzz(limit)

    assertTrue(actual.isEmpty())
}
Enter fullscreen mode Exit fullscreen mode

@ParameterizedTest allows the test to be executed multiple times passing the values found in @ValueSource individually to the test method. The test method then needs to take one parameter.

One big advantage of this kind of test is that we are now able to test all the invalid test cases we want with a single method and if a new case that behaves in the same way is needed we can just add it to the @ValueSource list.


Conclusion

This is just a simple introduction on how to write unit tests for a single function, things could get more complex when we need to pass more complex data or we need to test other kind of elements, but we can still see that testing is not that hard and we can easily validate the code we work with. If you are interested in the final test class it's available here, along with the markdown of this article.

Once more: Thanks for reading!

(And special thanks to Jhoon Saravia and Alejandro Tellez for helping me with grammar and typos)

Top comments (3)

Collapse
 
v1sd3v profile image
v1sd3v

A great example, It would be great to have even more.

Collapse
 
sierisimo profile image
Sinuhe Jaime Valencia • Edited

I'll be posting more on this same topic with examples. The next thing is adding the increment function and bring up some problems you face when writing the tests in the same way we did here. I'll keep this article updated with series thing


I just noticed I answered without seeing I had a draft… I'll be posting more information

Collapse
 
v1sd3v profile image
v1sd3v

Looking forward!