JUnit 5 для розробників Kotlin

1. Вступ

Нещодавно випущений JUnit 5 - це наступна версія відомого тестування для Java. Ця версія включає ряд функцій, спеціально націлених на функціональність, представлену в Java 8 - вона в основному побудована на використанні лямбда-виразів.

У цій короткій статті ми покажемо, наскільки добре цей же інструмент працює з мовою Kotlin .

2. Прості тести JUnit 5

Найпростіший тест JUnit 5, написаний у Котліні, працює точно так, як і слід було очікувати. Ми пишемо тестовий клас, коментуємо наші методи тестування анотацією @Test , пишемо наш код і виконуємо твердження:

class CalculatorTest { private val calculator = Calculator() @Test fun whenAdding1and3_thenAnswerIs4() { Assertions.assertEquals(4, calculator.add(1, 3)) } }

Тут все просто працює нестандартно. Ми можемо використовувати стандартні анотації @Test, @BeforeAll, @BeforeEach, @AfterEach і @AfterAll . Ми також можемо взаємодіяти з полями в тестовому класі точно так само, як і в Java.

Зверніть увагу, що необхідний імпорт відрізняється, і ми робимо твердження, використовуючи клас Assertions замість класу Assert . Це стандартна зміна для JUnit 5 і не стосується Kotlin.

Перш ніж іти далі, давайте змінимо назву тесту та використаємо ідентифікатори b acktick у Kotlin:

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

Тепер це набагато читабельніше! У Kotlin ми можемо оголосити всі змінні та функції, використовуючи зворотні позначки, але це не рекомендується робити для звичайних випадків використання.

3. Розширені твердження

JUnit 5 додає кілька вдосконалених тверджень щодо роботи з лямбдами . Вони працюють так само в Котліні, як і на Java, але їх потрібно виражати дещо по-іншому через те, як працює мова.

3.1. Ствердження винятків

JUnit 5 додає твердження про те, коли очікується, що дзвінок викликає виняток. Ми можемо перевірити, що конкретний виклик - а не будь-який виклик у методі - видає очікуваний виняток. Ми можемо навіть стверджувати щодо самого винятку.

На Java ми передаємо лямбда-сигнал у виклик Assertions.assertThrows . Ми робимо те ж саме в Kotlin, але ми можемо зробити код більш читабельним , додавши блок до кінця виклику твердження:

@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.assertAll , передаючи довільну кількість лямбд.

У Котліні нам потрібно вирішити це дещо інакше. Функція фактично приймає змінну довжину параметра типу 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. Постачальники для істинних та помилкових тестів

Іноді ми хочемо перевірити, що якийсь виклик повертає істинне або хибне значення. Історично ми обчислювали це значення і закликали assertTrue або assertFalse відповідно. JUnit 5 дозволяє замість цього надати лямбду, яка повертає перевіряне значення.

Котлін дозволяє нам передавати лямбду так само, як ми бачили вище для тестування винятків. Ми також можемо передати посилання на методи . Це особливо корисно при тестуванні поверненого значення деякого існуючого об'єкта, як ми робимо тут за допомогою List.isEmpty :

@Test fun `isEmpty should return true for empty lists`() { val list = listOf() 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 є власна підтримка тестів на основі даних . Вони однаково добре працюють у Котліні , і використання функціональних відображень у колекціях може полегшити читання та обслуговування наших тестів.

4.1. Методи TestFactory

Найпростіший спосіб обробки тестів, керованих даними, - використання анотації @TestFactory . Це замінює анотацію @Test , і метод повертає деяку колекцію екземплярів DynamicNode - зазвичай створюваних за допомогою виклику DynamicTest.dynamicTest .

Це працює точно так само в Котліні, і ми можемо знову пройти в лямбду чистішим способом , як ми бачили раніше:

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

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

We can also create the input list as a class field and share it between multiple tests:

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. Parameterized Tests

There are experimental extensions to JUnit 5 to allow easier ways to write parameterized tests. These are done using the @ParameterizedTest annotation from the org.junit.jupiter:junit-jupiter-params dependency:

 org.junit.jupiter junit-jupiter-params 5.0.0 

The latest version can be found on Maven Central.

The @MethodSource annotation allows us to produce test parameters by calling a static function that resides in the same class as the test. This is possible but not obvious in Kotlin. We have to 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) ) }

This also means that the methods used to produce parameters must all be together since we can only have a single companion object per class.

All of the other ways of using parameterized tests work exactly the same in Kotlin as they do in Java. @CsvSource is of special note here, since we can use that instead of @MethodSource for simple test data most of the time to make our tests more readable:

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

5. Tagged Tests

The Kotlin language does not currently allow for repeated annotations on classes and methods. This makes the use of tags slightly more verbose, as we are required to 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.

Приклади всіх цих функцій можна знайти на GitHub.