JUnit 5 для разработчиков Kotlin

JUnit 5 для разработчиков Kotlin

1. Вступление

Недавно выпущенныйJUnit 5 - это следующая версия хорошо известной среды тестирования для Java. Эта версия включает несколькоfeatures that specifically target functionality introduced in Java 8 - она ​​в основном построена на использовании лямбда-выражений.

В этой быстрой статье мы покажем, насколько хорош тот же инструментworks with the Kotlin language.

2. Простые тесты JUnit 5

В самом простом виде тест JUnit 5, написанный на Kotlin, работает точно так, как и следовало ожидать. Мы пишем тестовый класс, аннотируем наши тестовые методы аннотацией@Test, пишем наш код и выполняем утверждения:

class CalculatorTest {
    private val calculator = Calculator()

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

Все здесь просто работает из коробки. Мы можем использовать стандартные аннотации@Test, @BeforeAll, @BeforeEach, @AfterEach, и@AfterAll. Мы также можем взаимодействовать с полями в тестовом классе точно так же, как в Java.

Обратите внимание, что требуемый импорт отличается, иwe doassertions using the Assertions class instead of the Assert class. Это стандартное изменение для JUnit 5 и не относится к Kotlin.

Прежде чем двигаться дальше, давайте изменим имя теста и используемbacktick identifiers в Kotlin:

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

Теперь он стал более читаемым! В Kotlin мы можем объявлять все переменные и функции с помощью обратных кавычек, но это не рекомендуется делать в обычных случаях использования.

3. Расширенные утверждения

JUnit 5 adds some advanced assertions forworking with lambdas. Они работают так же в Kotlin, как и в Java, но должны быть выражены немного по-другому из-за того, как работает язык.

3.1. Утверждение исключений

JUnit 5 добавляет утверждение, когда ожидается, что вызов вызовет исключение. We can test that a specific call — rather than just any call in the method — throws the expected exception. Мы можем утверждать даже об исключении.

В Java мы передаем лямбду в вызовAssertions.assertThrows. Мы делаем то же самое в Kotlin, но можемmake the code more readable, добавив блок в конец вызова утверждения:

@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)
}

Этот кодworks exactly the same as the Java equivalent but is easier to read, поскольку нам не нужно передавать лямбда внутри скобок, где мы вызываем функциюassertThrows.

3.2. Множественные утверждения

JUnit 5 добавляет возможностьperform multiple assertions at the same time, и он оценит их все и сообщит обо всех сбоях.

Это позволяет нам собирать больше информации за один прогон теста, а не заставлять исправлять одну ошибку только для следующего. Для этого мы вызываемAssertions.assertAll, передавая произвольное количество лямбда-выражений.

In Kotlin, нам нужно обработать это немного иначе. function actually takes a varargs parameter of type Executable.

В настоящее время нет поддержки автоматического преобразования лямбда-выражения в функциональный интерфейс, поэтому нам нужно сделать это вручную:

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. Поставщики для истинных и ложных испытаний

Иногда мы хотим проверить, что какой-то вызов возвращает значениеtrue илиfalse. Раньше мы вычисляли это значение и вызывалиassertTrue илиassertFalse соответственно. JUnit 5 позволяет вместо этого использовать лямбду, которая возвращает проверяемое значение.

Kotlin allows us to pass in a lambda таким же образом, как мы видели выше для тестирования исключений. We can also pass in method references. Это особенно полезно при тестировании возвращаемого значения некоторого существующего объекта, как мы это делаем здесь, используяList.isEmpty:

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

3.4. Поставщики сообщений о сбоях

В некоторых случаях мы хотим, чтобыprovide our own error message отображался при ошибке утверждения вместо ошибки по умолчанию.

Часто это простые строки, ноsometimes we may want to use a string that is expensive to compute. В JUnit 5 мы можемprovide a lambda to compute this string, и этоonly called on failure вместо того, чтобы вычисляться заранее.

Это может помочьmake the tests run faster and reduce build times. Это работает точно так же, как мы видели раньше:

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

4. Тесты на основе данных

Одно из больших улучшений в JUnit 5 -native support for data-driven tests. Этиwork equally well in Kotlin и использование функциональных отображений в коллекциях можетmake our tests easier to read and maintain.

4.1. Методы TestFactory

Самый простой способ обрабатывать тесты, управляемые данными, - использовать аннотацию@TestFactory. Это заменяет аннотацию@Test, и метод возвращает некоторую коллекцию экземпляровDynamicNode, обычно создаваемую путем вызоваDynamicTest.dynamicTest.

Это работает точно так же в Kotlin, и мы снова можемpass in the lambda in a cleaner way, как мы видели ранее:

@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))}
)

Мы можем сделать лучше, чем это, хотя. Мы легко можемbuild 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))
        }
    }

Сразу же, мы можем легко добавить больше тестовых случаев в список ввода, и он автоматически добавит тесты.

Мы также можем использоватьcreate the input list as a class field и поделиться им между несколькими тестами:

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. Параметризованные тесты

Естьexperimental extensions to JUnit 5, чтобы упростить написание параметризованных тестов. Это делается с помощью аннотации@ParameterizedTest из зависимостиorg.junit.jupiter:junit-jupiter-params:


    org.junit.jupiter
    junit-jupiter-params
    5.0.0

Последнюю версию можно найти наMaven Central.

Аннотация@MethodSource позволяет нам использоватьproduce test parameters by calling a static function, который находится в том же классе, что и тест. Это возможно, ноnot obvious in Kotlin. Мы должныuse 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)
    )
}

Это также означает, что все методы, используемые для создания параметров, должны быть вместе, начиная сwe can only have a single companion object per class.

Все остальные способы использования параметризованных тестов в Kotlin работают точно так же, как и в Java. Здесь особо следует отметить@CsvSource, поскольку мы можем использовать это вместо@MethodSource для простых тестовых данных большую часть времени, чтобы сделать наши тесты более читаемыми:

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

5. Помеченные тесты

Язык Kotlindoes not currently allow for repeated annotations о классах и методах. Это делает использование тегов более подробным, так как мы должныwrap them in the @Tags annotation:

@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))
}

Это также требуется в Java 7 и уже полностью поддерживается JUnit 5.

6. Резюме

JUnit 5 добавляет несколько мощных инструментов тестирования, которые мы можем использовать. Почти все они прекрасно работают с языком Kotlin, хотя в некоторых случаях с немного другим синтаксисом, чем мы видим в эквивалентах Java.

Однако часто эти изменения в синтаксисе легче читать и работать при использовании Kotlin.

Примеры всех этих функций можно найтиover on GitHub.