JUnit 5 für Kotlin-Entwickler

JUnit 5 für Kotlin-Entwickler

1. Einführung

Das neu veröffentlichteJUnit 5 ist die nächste Version des bekannten Testframeworks für Java. Diese Version enthält eine Reihe vonfeatures that specifically target functionality introduced in Java 8 - sie basiert hauptsächlich auf der Verwendung von Lambda-Ausdrücken.

In diesem kurzen Artikel zeigen wir, wie gut das gleiche Toolworks with the Kotlin language ist.

2. Einfache JUnit 5-Tests

Im einfachsten Fall funktioniert ein in Kotlin geschriebener JUnit 5-Test genau wie erwartet. Wir schreiben eine Testklasse, kommentieren unsere Testmethoden mit der Annotation@Test, schreiben unseren Code und führen die folgenden Aussagen aus:

class CalculatorTest {
    private val calculator = Calculator()

    @Test
    fun whenAdding1and3_thenAnswerIs4() {
        Assertions.assertEquals(4, calculator.add(1, 3))
    }
}

Hier funktioniert einfach alles von der Stange. Wir können die Standardanmerkungen@Test, @BeforeAll, @BeforeEach, @AfterEach, und@AfterAll verwenden. Wir können auch mit Feldern in der Testklasse genauso wie in Java interagieren.

Beachten Sie, dass die erforderlichen Importe unterschiedlich sind undwe doassertions using the Assertions class instead of the Assert class. Dies ist eine Standardänderung für JUnit 5 und nicht spezifisch für Kotlin.

Bevor wir fortfahren, ändern wir den Testnamen und verwendenbacktick identifiers in Kotlin:

@Test
fun `Adding 1 and 3 should be equal to 4`() {
    Assertions.assertEquals(4, calculator.add(1, 3))
}

Jetzt ist es viel besser lesbar! In Kotlin können wir alle Variablen und Funktionen mithilfe von Backticks deklarieren. Dies wird jedoch für normale Anwendungsfälle nicht empfohlen.

3. Erweiterte Behauptungen

JUnit 5 adds some advanced assertions forworking with lambdas. Diese funktionieren in Kotlin genauso wie in Java, müssen jedoch aufgrund der Funktionsweise der Sprache etwas anders ausgedrückt werden.

3.1. Ausnahmen geltend machen

JUnit 5 fügt eine Zusicherung für den Fall hinzu, dass ein Aufruf eine Ausnahme auslösen soll. We can test that a specific call — rather than just any call in the method — throws the expected exception. Wir können sogar die Ausnahme selbst geltend machen.

In Java übergeben wir ein Lambda an den Aufruf vonAssertions.assertThrows. Wir machen dasselbe in Kotlin, aber wir könnenmake the code more readable, indem wir einen Block an das Ende des Assertionsaufrufs anhängen:

@Test
fun `Dividing by zero should throw the DivideByZeroException`() {
    val exception = Assertions.assertThrows(DivideByZeroException::class.java) {
        calculator.divide(5, 0)
    }

    Assertions.assertEquals(5, exception.numerator)
}

Dieser Codeworks exactly the same as the Java equivalent but is easier to read, da wir kein Lambda in den Klammern übergeben müssen, in denen wir die FunktionassertThrows aufrufen.

3.2. Mehrere Behauptungen

JUnit 5 erweitertperform multiple assertions at the same timeum die Fähigkeit, alle zu bewerten und alle Fehler zu melden.

Dies ermöglicht es uns, mehr Informationen in einem einzigen Testlauf zu sammeln, anstatt gezwungen zu sein, nur einen Fehler zu beheben, um den nächsten zu treffen. Dazu rufen wirAssertions.assertAll auf und übergeben eine beliebige Anzahl von Lambdas.

In Kotlin müssen wir etwas anders damit umgehen. Diefunction actually takes a varargs parameter of type Executable.

Derzeit gibt es keine Unterstützung für das automatische Umwandeln eines Lambda in eine funktionale Schnittstelle. Daher müssen wir dies von Hand tun:

fun `The square of a number should be equal to that number multiplied in itself`() {
    Assertions.assertAll(
        Executable { Assertions.assertEquals(1, calculator.square(1)) },
        Executable { Assertions.assertEquals(4, calculator.square(2)) },
        Executable { Assertions.assertEquals(9, calculator.square(3)) }
    )
}

3.3. Lieferanten für wahre und falsche Tests

Gelegentlich möchten wir testen, ob ein Aufruf einen Wert vontrue oderfalsezurückgibt. In der Vergangenheit haben wir diesen Wert berechnet undassertTrue oderassertFalse aufgerufen. In JUnit 5 kann stattdessen ein Lambda angegeben werden, das den zu überprüfenden Wert zurückgibt.

Kotlin allows us to pass in a lambda auf die gleiche Weise, wie wir sie oben zum Testen von Ausnahmen gesehen haben. We can also pass in method references. Dies ist besonders nützlich, wenn Sie den Rückgabewert eines vorhandenen Objekts testen, wie hier mitList.isEmpty:

@Test
fun `isEmpty should return true for empty lists`() {
    val list = listOf()
    Assertions.assertTrue(list::isEmpty)
}

3.4. Lieferanten für Fehlermeldungen

In einigen Fällen möchten wir, dassprovide our own error message bei einem Assertionsfehler anstelle des Standardfehlers angezeigt wird.

Oft sind dies einfache Zeichenfolgen, abersometimes we may want to use a string that is expensive to compute. In JUnit 5 können wirprovide a lambda to compute this string und es istonly called on failure, anstatt im Voraus berechnet zu werden.

Dies kannmake the tests run faster and reduce build times helfen. Das funktioniert genauso wie wir es vorher gesehen haben:

@Test
fun `3 is equal to 4`() {
    Assertions.assertEquals(3, 4) {
        "Three does not equal four"
    }
}

4. Datengesteuerte Tests

Eine der großen Verbesserungen in JUnit 5 sind dienative support for data-driven tests. Diesework equally well in Kotlin und die Verwendung von Funktionszuordnungen für Sammlungen könnenmake our tests easier to read and maintain. sein

4.1. TestFactory-Methoden

Der einfachste Weg, datengesteuerte Tests durchzuführen, ist die Verwendung der Annotation@TestFactory. Dies ersetzt die Annotation von@Testund die Methode gibt eine Sammlung vonDynamicNode-Instanzen zurück, die normalerweise durch Aufrufen vonDynamicTest.dynamicTest erstellt werden.

Dies funktioniert in Kotlin genauso, und wir können wiederpass in the lambda in a cleaner way, wie wir zuvor gesehen haben:

@TestFactory
fun testSquares() = listOf(
    DynamicTest.dynamicTest("when I calculate 1^2 then I get 1") { Assertions.assertEquals(1,calculator.square(1))},
    DynamicTest.dynamicTest("when I calculate 2^2 then I get 4") { Assertions.assertEquals(4,calculator.square(2))},
    DynamicTest.dynamicTest("when I calculate 3^2 then I get 9") { Assertions.assertEquals(9,calculator.square(3))}
)

Wir können es aber besser machen. Wir können leichtbuild our list by performing some functional mapping on a simple input list of data:

@TestFactory
fun testSquares() = listOf(
    1 to 1,
    2 to 4,
    3 to 9,
    4 to 16,
    5 to 25)
    .map { (input, expected) ->
        DynamicTest.dynamicTest("when I calculate $input^2 then I get $expected") {
            Assertions.assertEquals(expected, calculator.square(input))
        }
    }

Sofort können wir der Eingabeliste problemlos weitere Testfälle hinzufügen, und es werden automatisch Tests hinzugefügt.

Wir können auchcreate the input list as a class field und es zwischen mehreren Tests teilen:

private val squaresTestData = listOf(
    1 to 1,
    2 to 4,
    3 to 9,
    4 to 16,
    5 to 25)
@TestFactory
fun testSquares() = squaresTestData
    .map { (input, expected) ->
        DynamicTest.dynamicTest("when I calculate $input^2 then I get $expected") {
            Assertions.assertEquals(expected, calculator.square(input))
        }
    }
@TestFactory
fun testSquareRoots() = squaresTestData
    .map { (expected, input) ->
        DynamicTest.dynamicTest("when I calculate the square root of $input then I get $expected") {
            Assertions.assertEquals(expected, calculator.squareRoot(input))
        }
    }

4.2. Parametrisierte Tests

Es gibtexperimental extensions to JUnit 5, um das Schreiben parametrisierter Tests zu vereinfachen. Dies erfolgt unter Verwendung der Annotation@ParameterizedTestaus der Abhängigkeitorg.junit.jupiter:junit-jupiter-params:


    org.junit.jupiter
    junit-jupiter-params
    5.0.0

Die neueste Version finden Sie unterMaven Central.

Die Annotation@MethodSource ermöglicht es uns,produce test parameters by calling a static function anzugeben, die sich in derselben Klasse wie der Test befinden. Dies ist möglich, abernot obvious in Kotlin. Wir müssenuse the @JvmStatic annotation inside a companion object:

@ParameterizedTest
@MethodSource("squares")
fun testSquares(input: Int, expected: Int) {
    Assertions.assertEquals(expected, input * input)
}

companion object {
    @JvmStatic
    fun squares() = listOf(
        Arguments.of(1, 1),
        Arguments.of(2, 4)
    )
}

Dies bedeutet auch, dass die zur Erzeugung von Parametern verwendeten Methoden alle seitwe can only have a single companion object per class zusammen sein müssen.

Alle anderen Methoden zur Verwendung parametrisierter Tests funktionieren in Kotlin genauso wie in Java. @CsvSource ist hier besonders zu beachten, da wir diese anstelle von@MethodSource meistens für einfache Testdaten verwenden können, um unsere Tests besser lesbar zu machen:

@ParameterizedTest
@CsvSource(
    "1, 1",
    "2, 4",
    "3, 9"
)
fun testSquares(input: Int, expected: Int) {
    Assertions.assertEquals(expected, input * input)
}

5. Tagged Tests

Die Kotlin-Sprachedoes not currently allow for repeated annotationsbezieht sich auf Klassen und Methoden. Dies macht die Verwendung von Tags etwas ausführlicher, da wirwrap them in the @Tags annotation: benötigen

@Tags(
    Tag("slow"),
    Tag("logarithms")
)
@Test
fun `Log to base 2 of 8 should be equal to 3`() {
    Assertions.assertEquals(3.0, calculator.log(2, 8))
}

Dies ist auch in Java 7 erforderlich und wird von JUnit 5 bereits vollständig unterstützt.

6. Zusammenfassung

JUnit 5 fügt einige leistungsfähige Testwerkzeuge hinzu, die wir verwenden können. Diese funktionieren fast alle einwandfrei mit der Kotlin-Sprache, wenn auch in einigen Fällen mit einer etwas anderen Syntax als in den Java-Entsprechungen.

Oft sind diese Änderungen in der Syntax bei Verwendung von Kotlin jedoch einfacher zu lesen und zu verarbeiten.

Beispiele für all diese Funktionen finden Sie inover on GitHub.