📜 ⬆️ ⬇️

Testing with JUnit 5 on Kotlin

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

Official user guide
Test source code from this article: GitHub

Before creating the first test, specify the dependency in pom.xml

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

Create the first test:
 import org.junit.jupiter.api.Test class HelloJunit5Test { @Test fun `First test`() { print("Hello, JUnit5!") } } 

The test passes successfully:
')
image

Let us turn to an overview of the main features of JUnit 5 and various technical nuances.

Test Display Name


The value of the annotation @DisplayName , as well 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 annotation value takes precedence over the function name:

image

The annotation also applies to the class:

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

image

Assertions


Assertions are in the 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 displayed in case of an error, and in the other, a lambda expression that implements the Supplier functional interface, which allows to calculate the value of the string only in the case of a failed test:

 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, first create a Person class with two properties:

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

Both assertions will be executed:

 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 references to methods in true / false checks


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

Exceptions


Work with exceptions is more transparent compared to JUnit 4:

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

Test run time test


As in the rest of the examples, everything is done simply:

  @Test fun `Timeout not exceeded`() { //     -,    1000  assertTimeout(ofMillis(1000)) { print(" ,     1 ") Thread.sleep(3) } } 

In this case, the lambda expression is executed completely, even when the execution time has already exceeded the allowable one. In order for the test to fall immediately after the time allotted, it is necessary to use the assertTimeoutPreemptively method:

  @Test fun `Timeout not exceeded with preemptively exit`() { //  ,      1000  assertTimeoutPreemptively(ofMillis(1000)) { print(" ,     1 ") 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 possibilities for checking 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 perform 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 unfulfilled assumption does not fall, but is interrupted:

image

Data driven testing


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

Test factory


Before generating tests, for greater clarity, we will make the Person class a 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 stack of tests 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

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

The life cycle of running dynamic tests differs from the @Test methods in that the method annotated by the @BeforeEach only performed for the @TestFactory method, and not for each dynamic test. For example, when executing the following code, the Reset some var function will be called only once, as can be seen 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

Parameterized Tests


Parameterized tests, like dynamic ones, allow you to create a set of tests based on one method, but they do it in a way different from @TestFactory . To illustrate the operation of this method, we first add the dependency to pom.xml :

  <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 verifying that the incoming dates are already 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())) } } 

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

@EnumSource allows the test method to accept enum 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

It is possible to specify the 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 declaring it in a companion object and annotating @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 per 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 test repetitions is indicated as follows:

  @RepeatedTest(10) fun ` `() { } 

image

It is possible to customize the displayed test name:

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

image

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 highlight the relationships between them. Create an example using the Person class and our own provider of test arguments that returns 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 pretty visual:

image

Conclusion


JUnit 5 is quite easy to use and provides many convenient features for writing tests. Data driven testing using Kotlin provides ease of development and concise code.

Thank!

Source: https://habr.com/ru/post/346452/


All Articles