Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

How To Perform a Productive Testing by using JUnit 5 on Kotlin

DZone's Guide to

How To Perform a Productive Testing by using JUnit 5 on Kotlin

Data-driven testing using JUnit 5 Kotlin provides usability in the development and conciseness of the code, as well as many convenient features for writing tests.

· Performance Zone ·
Free Resource

Sensu is an open source monitoring event pipeline. Try it today.

This article will discuss the main features of the JUnit 5 platform and give examples of their use on Kotlin. The material is aimed at beginners in Kotlin and/or JUnit, however, and more experienced developers will find interesting things.

Official user guide

Source code for tests from this article: GitHub

Before creating the first test, let's specify in pom.xml the dependency:

<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter-api</artifactId>
    <version>5.0.2</version>
    <scope>test</scope>
</dependency>

Let's create the first test:

import org.junit.jupiter.api.Test

class HelloJunit5Test {

    @Test
    fun `First test`() {
        print("Hello, JUnit5!")
    }
}

The test is successful:

Image title

Let's pass a review of the main features of JUnit 5 and various technical nuances.

Displaying the Name of Test

In the meaning of the @DisplayName   annotation, as in the name of the Kotlin function, in addition to the readable display name of the test, you can specify special characters and emoji:

import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test

class HelloJunit5Test {

    @DisplayName("\uD83D\uDC4D")
    @Test
    fun `First test ╯°□°)╯`() {
        print("Hello, JUnit5!")
    }
}

As you can see, the value of the annotation takes precedence over the name of the function:

Image title

The abstract is also applicable to the class:

@DisplayName("Override class name")
class HelloJunit5Test {

Image title

Assertions

Assertions are in class org.junit.jupiter.Assertions and are static methods.

Basic Assertions

JUnit includes several options for checking the expected and real values. In one of them, the last argument is a message output in case of an error, and in the other, a lambda expression that implements the Supplier function interface, which allows you to calculate the string value only if the test fails:

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

class HelloJunit5Test {

    @Test
    fun `Base assertions`() {
        assertEquals("a", "a")
        assertEquals(2, 1 + 1, "Optional message")
        assertEquals(2, 1 + 1, { "Assertion message " + "can be lazily evaluated" })
    }
}

Group Assertions

To test group assertions, we first create a Person class with two properties:

class Person(val firstName: String, val lastName: String)

Both assertions will be fulfilled:

import org.junit.jupiter.api.Assertions.assertAll
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.function.Executable

class HelloJunit5Test {

    @Test
    fun `Grouped assertions`() {
        val person = Person("John", "Doe")
        assertAll("person",
                Executable { assertEquals("John", person.firstName) },
                Executable { assertEquals("Doe", person.lastName) }
        )
    }
}

Passing lambdas and method references in verifications to true/false:

 @Test
    fun `Test assertTrue with reference and lambda`() {
        val list = listOf("")
        assertTrue(list::isNotEmpty)
        assertTrue {
            !list.contains("a")
        }
    }

Exceptions

More transparent work with exceptions in comparison with JUnit 4:

 @Test
    fun `Test exception`() {
        val exception: Exception = assertThrows(IllegalArgumentException::class.java, {
            throw IllegalArgumentException("exception message")
        })
        assertEquals("exception message", exception.message)
    }

Checking the Test Execution Time

As in the other examples, everything is done simply:

@Test
     fun `Timeout not exceeded` () {
         // The test will fail after the lambda expression is executed, if it exceeds 1000 ms
         assertTimeout (ofMillis (1000)) {
             print ("An operation that takes less than 1 second takes place")
             Thread.sleep (3)
         }
     }

In this case, the lambda expression is executed completely, even when the execution time has already exceeded the permissible. In order for the test to drop immediately after the expiration of the allotted time, you need to use the assertTimeoutPreemptively   method:

@Test
     fun `Timeout not exceeded with preemptively exit` () {
         // The test will fail as soon as the execution time exceeds 1000 ms
         assertTimeoutPreemptively (ofMillis (1000)) {
             print ("An operation that takes less than 1 second takes place")
             Thread.sleep (3)
         }
     }

External Assertion-Libraries

Some libraries provide more powerful and expressive means of using assertions than JUnit. In particular, Hamcrest, among others, provides many opportunities to test arrays and collections. A few examples:

import org.hamcrest.MatcherAssert.assertThat
import org.hamcrest.Matchers.containsInAnyOrder
import org.hamcrest.Matchers.greaterThanOrEqualTo
import org.hamcrest.Matchers.hasItem
import org.hamcrest.Matchers.notNullValue
import org.junit.jupiter.api.Test

class HamcrestExample {

    @Test
    fun `Some examples`() {
        val list = listOf("s1", "s2", "s3")
        assertThat(list, containsInAnyOrder("s3", "s1", "s2"))
        assertThat(list, hasItem("s1"))
        assertThat(list.size, greaterThanOrEqualTo(3))
        assertThat(list[0], notNullValue())
    }
}

Assumptions

Assumptions provide the ability to run tests only if certain conditions are met:

import org.junit.jupiter.api.Assumptions.assumeTrue
import org.junit.jupiter.api.Test

class AssumptionTest {

    @Test
    fun `Test Java 8 installed`() {
        assumeTrue(System.getProperty("java.version").startsWith("1.8"))
        print("Not too old version")
    }

    @Test
    fun `Test Java 7 installed`() {
        assumeTrue(System.getProperty("java.version").startsWith("1.7")) {
            "Assumption doesn't hold"
        }
        print("Need to update")
    }
}

In this case, the test with the fulfilled hypothesis does not fall, but is interrupted:

Image title

Data-Driven Testing

One of the main features of JUnit 5 is support for data-driven testing.

Test actory

Before generating the tests for greater visibility, we make the class Person data class, which, among other things, will override the toString () method, and add the birthDate and age properties:

import java.time.LocalDate
import java.time.Period

data class Person(val firstName: String, val lastName: String, val birthDate: LocalDate?) {

    val age
        get() = Period.between(this.birthDate, LocalDate.now()).years
}

The following example will generate a test packet to verify that the age of each person is not less than the specified:

import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.DynamicTest
import org.junit.jupiter.api.DynamicTest.dynamicTest
import org.junit.jupiter.api.TestFactory
import java.time.LocalDate

class TestFactoryExample {

    @TestFactory
    fun `Run multiple tests`(): Collection<DynamicTest> {
        val persons = listOf(
                Person("John", "Doe", LocalDate.of(1969, 5, 20)),
                Person("Jane", "Smith", LocalDate.of(1997, 11, 21)),
                Person("Ivan", "Ivanov", LocalDate.of(1994, 2, 12))
        )

        val minAgeFilter = 18
        return persons.map {
            dynamicTest("Check person $it on age greater or equals $minAgeFilter") {
                assertTrue(it.age >= minAgeFilter)
            }
        }.toList()
    }
}

Image title

In addition to the DynamicTest collections, in a method annotated with  @TestFactory , you can return Stream, Iterable, Iterator.

The life cycle of performing dynamic tests differs from @Test methods in that the method annotated @BeforeEach will only execute for the @TestFactory method, and not for each dynamic test. For example, if you execute the following code, the Reset some var function will be called only once, as you can see by using the variable  someVar:

private var someVar: Int? = null

    @BeforeEach
    fun `Reset some var`() {
        someVar = 0
    }

    @TestFactory
    fun `Test factory`(): Collection<DynamicTest> {
        val ints = 0..5
        return ints.map {
            dynamicTest("Test №$it incrementing some var") {
                someVar = someVar?.inc()
                print(someVar)
            }
        }.toList()
    }

Image title

Parameterized Tests

Parameterized tests, like dynamic tests, allow you to create a set of tests based on one method, but they make it different from the @TestFactory image. To illustrate the work of this method, we first add to the pom.xml the following:

<dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter-params</artifactId>
            <version>5.0.2</version>
            <scope>test</scope>
        </dependency>

The code of the test that checks that incoming dates are in the past:

class ParameterizedTestExample {

    @ParameterizedTest
    @ValueSource(strings = ["2002-01-23", "1956-03-14", "1503-07-19"])
    fun `Check date in past`(date: LocalDate) {
        assertTrue(date.isBefore(LocalDate.now()))
    }
}

Values of @ValueSource annotation can be arrays int, long, double and String. In the case of a string array, as seen in the example above, an implicit conversion to the type of the input parameter will be used, if possible. The @ValueSource allows you to pass only one input parameter for each test call.

 @EnumSource  allows the test method to accept enumeration constants:

 @ParameterizedTest
    @EnumSource(TimeUnit::class)
    fun `Test enum`(timeUnit: TimeUnit) {
        assertNotNull(timeUnit)
    }

You can leave or exclude certain constants:

@ParameterizedTest
    @EnumSource(TimeUnit::class, mode = EnumSource.Mode.EXCLUDE, names = ["SECONDS", "MINUTES"])
    fun `Test enum without days and milliseconds`(timeUnit: TimeUnit) {
        print(timeUnit)
    }

Image title

It is possible to specify a method that will be used as a data source:

@ParameterizedTest
    @MethodSource("intProvider")
    fun `Test with custom arguments provider`(argument: Int) {
        assertNotNull(argument)
    }

    companion object {
        @JvmStatic
        fun intProvider(): Stream<Int> = Stream.of(0, 42, 9000)
    }

In java-code this method should be static. In Kotlin this is achieved by its declaration in the companion object and annotation of  @JvmStatic. To use a non-static method, you need to change the life cycle of the test instance; more precisely, create one instance of the test for the class, instead of one instance per method, as is done by default:

@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class ParameterizedTestExample {

    @ParameterizedTest
    @MethodSource("intProvider")
    fun `Test with custom arguments provider`(argument: Int) {
        assertNotNull(argument)
    }

    fun intProvider(): Stream<Int> = Stream.of(0, 42, 9000)
}

Repeatable Tests

The number of repetitions of the test is indicated as follows:

@RepeatedTest (10)
     fun `Repeat test` () {

     }

Image title

It is possible to customize the displayed test name:

@RepeatedTest (10, name = "{displayName} {currentRepetition} of {totalRepetitions}")
     fun `Repeat test` () {

     }

Image title

Access to information about the current test and the group of repeated tests can be obtained through the corresponding objects:

  @RepeatedTest(5)
    fun `Repeated test with repetition info and test info`(repetitionInfo: RepetitionInfo, testInfo: TestInfo) {
        assertEquals(5, repetitionInfo.totalRepetitions)
        val testDisplayNameRegex = """repetition \d of 5""".toRegex()
        assertTrue(testInfo.displayName.matches(testDisplayNameRegex))
    }

Nested Tests

JUnit 5 allows you to write nested tests for greater visibility and highlighting the relationships between them. Let's create an example using the Person class and our own provider of test arguments, returning a stream of Person objects:

class NestedTestExample {

    @ Nested
    inner class `Check age of person` {

        @ParameterizedTest
        @ArgumentsSource (PersonProvider :: class)
        fun `Check age greater or equals 18` (person: Person) {
            assertTrue (person.age> = 18)
        }

        @ParameterizedTest
        @ArgumentsSource (PersonProvider :: class)
        fun `Check birth date is after 1950` (person: Person) {
            assertTrue (LocalDate.of (1950, 12, 31) .isBefore (person.birthDate))
        }
    }

    @ Nested
    inner class `Check name of person` {

        @ParameterizedTest
        @ArgumentsSource (PersonProvider :: class)
        fun `Check first name length is 4` (person: Person) {
            assertEquals (4, person.firstName.length)
        }
    }

    internal class PersonProvider: ArgumentsProvider {
        override fun provideArguments (context: ExtensionContext): Stream <out Arguments> = Stream.of (
                Person ("John", "Doe", LocalDate.of (1969, 5, 20)),
                Person ("Jane", "Smith", LocalDate.of (1997, 11, 21)),
                Person ("Ivan", "Ivanov", LocalDate.of (1994, 2, 12))
        ) .map {Arguments.of (it)}
    }
}

The result will be quite clear:

Image title

Conclusion

JUnit 5 is fairly easy to use and provides many convenient features for writing tests. Data driven testing using Kotlin provides usability in the development and conciseness of the code.

Sensu: workflow automation for monitoring. Learn more—download the whitepaper.

Topics:
kotlin ,junit 5 ,Performance ,Testing ,DDT ,Nested Tests

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}