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.
Join the DZone community and get the full member experience.
Join For FreeThis 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.
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:
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:
The abstract is also applicable to the class:
@DisplayName("Override class name")
class HelloJunit5Test {
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:
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()
}
}
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()
}
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)
}
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` () {
}
It is possible to customize the displayed test name:
@RepeatedTest (10, name = "{displayName} {currentRepetition} of {totalRepetitions}")
fun `Repeat test` () {
}
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:
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.
Opinions expressed by DZone contributors are their own.
Comments