1. Введение
Недавно выпущенный JUnit 5 является следующей версией хорошо известного фреймворка тестирования для Java. Эта версия включает в себя ряд функций, которые специально ориентированы на функциональность, представленную в Java 8, — в основном она основана на использовании лямбда-выражений.
В этой краткой статье мы покажем, насколько хорошо тот же инструмент работает с языком Kotlin.
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. Мы также можем взаимодействовать с полями в классе test точно так же, как в Java.
Обратите внимание, что требуемый импорт отличается, и мы создаем утверждения, используя класс Assertions вместо класса Assert. Это стандартное изменение для JUnit 5 и не относится конкретно к Kotlin.
Прежде чем двигаться дальше, давайте изменим название теста и будем использовать идентификаторы обратной привязки в Kotlin:
@Test
fun `Adding 1 and 3 should be equal to 4`() {
Assertions.assertEquals(4, calculator.add(1, 3))
}
Теперь это намного удобнее для чтения! В Kotlin мы можем объявлять все переменные и функции с помощью обратных клавиш, но в обычных случаях использования это делать не рекомендуется.
3. Расширенные утверждения
JUnit 5 добавляет несколько расширенных утверждений для работы с лямбда-выражениями. Они работают в Kotlin так же, как и в Java, но должны быть выражены немного по-другому из-за особенностей языка.
3.1. Установление исключений
JUnit 5 добавляет утверждение для случаев, когда ожидается, что вызов вызовет исключение. Мы можем проверить, что ожидаемое исключение вызывает конкретный вызов, а не просто любой вызов в методе. Мы можем даже утверждать для самого исключения.
В Java мы бы передали лямбда-выражение в вызов Assertions.assertThrows. Мы делаем то же самое в Kotlin, но мы можем сделать код более читабельным, добавив блок в конец вызова assertion:
@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)
}
Этот код работает точно так же, как и Java-эквивалент, но его легче читать, поскольку нам не нужно передавать лямбда-символ внутри скобок, в которых мы вызываем функцию assertThrows.
3.2. Множественные утверждения
В JUnit 5 добавлена возможность выполнять несколько утверждений одновременно, и программа будет оценивать их все и сообщать обо всех ошибках.
Это позволяет нам собрать больше информации за один тестовый запуск, а не исправлять одну ошибку только для того, чтобы перейти к следующей. Для этого мы вызываем Assertions.assert All, передавая произвольное количество лямбда-выражений.
В Kotlin нам нужно обработать это немного по-другому. На самом деле функция принимает параметр varargs типа 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 позволяет нам передавать лямбда-выражение таким же образом, как мы видели выше, для тестирования исключений. Мы также можем передавать ссылки на методы. Это особенно полезно при тестировании возвращаемого значения какого-либо существующего объекта, как мы делаем здесь, используя List.isEmpty:
@Test
fun `isEmpty should return true for empty lists`() {
val list = listOf<String>()
Assertions.assertTrue(list::isEmpty)
}
3.4. Поставщики сообщений о сбоях
В некоторых случаях мы хотим предоставить наше собственное сообщение об ошибке, которое будет отображаться при сбое утверждения вместо сообщения по умолчанию.
Часто это простые строки, но иногда мы можем захотеть использовать строку, вычисление которой требует больших затрат. В JUnit 5 мы можем предоставить лямбда-выражение для вычисления этой строки, и оно вызывается только при сбое, а не вычисляется заранее.
Это может помочь ускорить выполнение тестов и сократить время сборки. Это работает точно так же, как мы видели ранее:
@Test
fun `3 is equal to 4`() {
Assertions.assertEquals(3, 4) {
"Three does not equal four"
}
}
4. Тесты, основанные на данных
Одним из важных улучшений в JUnit 5 является встроенная поддержка тестов, управляемых данными. Они одинаково хорошо работают в Kotlin, а использование функциональных сопоставлений с коллекциями может упростить чтение и поддержку наших тестов.
4.1. Заводские методы испытаний
Самый простой способ справиться с тестами, управляемыми данными, - это использовать аннотацию @TestFactory. Она заменяет аннотацию @Test, и метод возвращает некоторую коллекцию экземпляров DynamicNode, обычно создаваемую при вызове DynamicTest.dynamicTest.
Это работает точно так же в Kotlin, и мы можем снова передать лямбда-код более чистым способом, как мы видели ранее:
@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))}
)
Однако мы можем сделать что-то получше. Мы можем легко составить наш список, выполнив некоторое функциональное сопоставление с простым входным списком данных:
@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))
}
}
Сразу же мы можем легко добавить дополнительные тестовые примеры в список входных данных, и он автоматически добавит тесты.
Мы также можем создать список входных данных в виде поля класса и использовать его совместно с несколькими тестами:
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. Параметризованные тесты
Существуют экспериментальные расширения для JUnit 5, позволяющие упростить написание параметризованных тестов. Для этого используется аннотация @ParameterizedTest из org.junit.jupiter:зависимость junit-jupiter-params:
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-params</artifactId>
<version>5.0.0</version>
</dependency>
Аннотация @MethodSource позволяет нам создавать параметры теста путем вызова статической функции, которая находится в том же классе, что и тест. В Kotlin это возможно, но не очевидно. Мы должны использовать аннотацию @JvmStatic внутри сопутствующего объекта:
@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)
)
}
Это также означает, что все методы, используемые для создания параметров, должны быть объединены, поскольку у нас может быть только один сопутствующий объект для каждого класса.
Все остальные способы использования параметризованных тестов работают в Kotlin точно так же, как и в Java. Здесь особо следует отметить @CsvSource, поскольку в большинстве случаев мы можем использовать его вместо @MethodSource для простых тестовых данных, чтобы сделать наши тесты более удобочитаемыми:
@ParameterizedTest
@CsvSource(
"1, 1",
"2, 4",
"3, 9"
)
fun testSquares(input: Int, expected: Int) {
Assertions.assertEquals(expected, input * input)
}
5. Помеченные тесты
Язык Kotlin, начиная с версии 1.6, допускает повторяющиеся аннотации к классам и методам. Это делает использование тегов немного менее подробным, поскольку нам не требуется заключать их в аннотацию @Tags. Однако мы должны пометить @Tag как @java.lang.annotation. Повторяемый в байт-коде, чтобы он также повторялся в Java:
// JVM bytecode: @Tag.Container(value = {@Tag("slow"), @Tag("logarithms")})
@Tag("slow") @Tag("logarithms")
@Test
fun `Repeatable Tag 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.
Оригинал статьи: https://www.baeldung.com/kotlin/junit-5-kotlin