DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Zones

Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks

How does AI transform chaos engineering from an experiment into a critical capability? Learn how to effectively operationalize the chaos.

Data quality isn't just a technical issue: It impacts an organization's compliance, operational efficiency, and customer satisfaction.

Are you a front-end or full-stack developer frustrated by front-end distractions? Learn to move forward with tooling and clear boundaries.

Developer Experience: Demand to support engineering teams has risen, and there is a shift from traditional DevOps to workflow improvements.

Related

  • Why Testing is a Long-Term Investment for Software Engineers
  • JUnit, 4, 5, Jupiter, Vintage
  • Readability in the Test: Exploring the JUnitParams
  • Creating Your Swiss Army Knife on Java Test Stack

Trending

  • HTAP Using a Star Query on MongoDB Atlas Search Index
  • The Synergy of Security and Development: Integrating Threat Models With DevOps
  • Zero-Latency Architecture: Database Triggers + Serverless Functions for Modern Reactive Architectures
  • Turbocharge Load Testing: Yandex.Tank + ghz Combo for Lightning-Fast Code Checks
  1. DZone
  2. Testing, Deployment, and Maintenance
  3. Testing, Tools, and Frameworks
  4. How To Perform a Productive Testing by using JUnit 5 on Kotlin

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.

By 
Alex Maison user avatar
Alex Maison
·
Jan. 18, 18 · Tutorial
Likes (3)
Comment
Save
Tweet
Share
20.6K Views

Join the DZone community and get the full member experience.

Join For Free

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.

JUnit Testing Kotlin (programming language)

Opinions expressed by DZone contributors are their own.

Related

  • Why Testing is a Long-Term Investment for Software Engineers
  • JUnit, 4, 5, Jupiter, Vintage
  • Readability in the Test: Exploring the JUnitParams
  • Creating Your Swiss Army Knife on Java Test Stack

Partner Resources

×

Comments

The likes didn't load as expected. Please refresh the page and try again.

ABOUT US

  • About DZone
  • Support and feedback
  • Community research
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 100
  • Nashville, TN 37211
  • [email protected]

Let's be friends: