engineering

March 17, 2020   |   6min read

JUnit 5 in Kotlin Testing

What is JUnit 5?

JUnit 5 is a set of testing frameworks that allows launching, parametrizing, and organizing tests. JUnit is one of the top frameworks for Java unit testing (an obvious alternative is TestNG), but it can be easily used in different conditions—for instance, it comes as the default framework for Android Espresso E2E testing. Wide options of configuration encourage us to use JUnit as well in the web E2E tests based on Selenium.

Compared to the 4th edition, it comes with more extensive options of test suite configuration by annotations (syntactic meta-data is added to code with a @ mark, which controls the code execution). Also, some assertions have changed, and new ones were introduced.

The great news is that it is fully compatible with Kotlin—which is becoming a top choice for Android development. In this blog post I would like to highlight key features of JUnit 5 and how to use them in Kotlin.

Annotations

Probably most of us have seen @Test annotations in code many times. Usually, it is good enough to go with this very simple implementation but some cases require more advanced usage of JUnit features. JUnit comes with a number of very useful annotations that allow us to adjust how tests are executed. It can save execution time, increase the readability of test results, or even result in maintaining less code for the same assertions.

I would like to show you some annotations that are available since JUnit 5 is on, which I found the most useful.

@DisplayName

This annotation organizes test classes in an easy to understand way. For example, without this annotation tests for the class named CalculatorTests would be displayed like shown below:

Test class execution without customized name

In order to a get-human friendly test description you can annotate the test class with:

@DisplayName("Set of tests that verify basic calculator usage")

and get as a result:

Test class execution with the customized name

This annotation increases test readability and enhances the quality of our test reports by applying easy descriptions instead of programming method names.

@Execution

JUnit5 comes with the ability to boost up the execution of your test suites. For independent tests—written in the way that avoids, for instance, interfering with the same database— you can benefit from using concurrency.

To show the usage of this annotation, I created a sample test that waits 5 seconds with a simple assertion. Using annotation @Repeated I forced JUnit to perform this test 10 times.

By default, your test suite execution is performed in the same thread—it means JUnit forces execution in one thread. With a default mode (ExecutionMode.SAME_THREAD) we have to wait until the work on a thread is finished to move onto the next test cases. Below you can see the results for this mode:

Summary report with timings without tests parallelization

Now let’s compare the same test with a flag ExecutionMode.CONCURRENT:

Summary report with timings with tests parallelization

@ParametrizedTest

This annotation is very convenient when you need to automate many variations of the same test case. Thanks to @parametrizedTest you can achieve it without writing redundant code. Using the calculator example, I’ll show you how to save lines of code when automating tests for the addition.

The main part of the test case is an assertion of equation items:

fun add(first: Int, second: Int, expectedResult: Int) {
   assertEquals(expectedResult, calculator.add(first, second)) {
       "$first + $second should equal $expectedResult"
   }
}

For this method, it’s required to pass arguments. I did it by using another useful annotation

@CsvSource:

@CsvSource(
            "0,    1,   1",
            "1,    2,   3",
            "49,  51, 100",
            "1,  100, 101"
    )

Data included inside the @CsvSource annotation is an input data for the test case. The test case will consume those values as parts of the equation.

In order to increase the readability of the test, I passed the @ParametrizedTest annotation, which dynamically generated test names that contain ingredients of the equation. Next step:

@ParameterizedTest(name = "{0} + {1} = {2}")
@CsvSource(
       "0,    1,   1",
       "1,    2,   3",
       "49,  51, 100",
       "1,  100, 101"
)
fun add(first: Int, second: Int, expectedResult: Int) {
   assertEquals(expectedResult, calculator.add(first, second)) {
       "$first + $second should equal $expectedResult"
   }
}

The output from this test method is:

Parameterized test execution

Thanks to this assertion, I saved lines of code by reusing the same test method with various input data that can be loaded directly from code as on my example or from the external csv file.

@TestFactory

Another way to run multiple test cases with a single test method is using dynamic tests. In my example, I added the first number to itself and expected it to be equal to the right side value.

@TestFactory
fun testDynamicAdd() = listOf(
      1 to 2,
      2 to 4,
      3 to 6,
      4 to 8,
      5 to 10,
      6 to 12,
      7 to 14,
      8 to 16
)
      .map { (input, expected) ->
          DynamicTest.dynamicTest("when I calculate $input + $input then I get $expected") {
              Assertions.assertEquals(expected, calculator.add(input, input))
          }
      }

The output from tests execution is:

Dynamic tests execution

All arguments in the list are mapped and used for test execution. Dynamic test uses them to establish names of tests and to perform the assertion on the math result. Without this feature, I would have to write more code to cover these 8 cases separately.

@Nested

It’s common to use before and after actions for a test class. They can be used to make sure that the application state is the same for each test case in the class. Sometimes a bunch of tests requires some additional steps before its execution. With @Nested annotation, it is possible to have a parent test class with global before and after actions and nested test classes with their own before and after steps.

Let’s have a look at a screenshot from such a test suite execution:

Test suite execution with nested test classes

In my example, NestedTests is a parent class with @beforeAlland @beforeEach actions. @beforeAll is executed one time for test class. @beforeEach is executed every time before each test starts.

Inside this test class there are nested classes Positive and Negative with their own @beforeEach methods. Those methods are executed for every test case just after the parent @beforeEach steps are finished.

See a code example here:

class NestedTests {
   val calculator = Calculator()

   companion object {
       @BeforeAll
       @JvmStatic
       fun beforeAll() {
           println("Before All NestedTests\n")
       }
   }


   @BeforeEach
   fun beforeEach() {
       println("   Before Each NestedTests\n")
   }


   @DisplayName("positive Calc tests")
   @Nested
   inner class Positive {
       @BeforeEach
       fun beforeEach(testInfo: TestInfo) {
           System.out.println("        Before " + testInfo.displayName + "\n");
       }

       @ParameterizedTest(name = "positive {0} + {1} = {2}")
       @CsvSource(
               "0,    1,   1",
               "1,    2,   3",
               "49,  51, 100",
               "1,  100, 101"
       )
       fun add(first: Int, second: Int, expectedResult: Int) {
           assertEquals(expectedResult, calculator.add(first, second)) {
               "$first + $second should equal $expectedResult"
           }
       }

   }

   @DisplayName("negative Calc tests")
   @Nested
   inner class Negative {
       @BeforeEach
       fun beforeEach(testInfo: TestInfo) {
           System.out.println("        Before " + testInfo.displayName + "\n");
       }

       @ParameterizedTest(name = "negative {0} + {1} = {2}")
       @CsvSource(
               "0,    -1,   -1",
               "-1,    -2,   -3",
               "-49,  -51, -100",
               "-1,  -100, -101"
       )
       fun add(first: Int, second: Int, expectedResult: Int) {
           assertEquals(expectedResult, calculator.add(first, second)) {
               "$first + $second should equal $expectedResult"
           }
       }
   }
}

With this feature, we can organize groups of tests that share common steps in the same class and easily extend them whenever we need to.

Summary

The features I presented in this blog post helped me a lot with maintaining a repository of tests. With parametrization, I increased the clearance of my test suites and saved a lot of lines of code to maintain. Concurrent tests can reduce time spent on CI builds—for one build, they can save seconds but if the CI infrastructure is limited then they can really decrease those queue times. All the benefits can be achieved very easily by using simple annotations that come with JUnit.

Adam Stasiak

Senior Test Engineer

Did you enjoy the read?

If you have any questions, don’t hesitate to ask!