Учебник JUnit 5, часть 1: Модульное тестирование с JUnit 5, Mockito и Hamcrest

JUnit 5 - это новый стандарт де-факто для разработки модульных тестов на Java. Эта новейшая версия оставила позади ограничения Java 5 и интегрировала многие функции из Java 8, в первую очередь поддержку лямбда-выражений.

В первой половине введения двух частей к JUnit 5, вы будете начать с тестирования с помощью JUnit 5. Я покажу вам , как настроить проект Maven использовать JUnit 5, как тесты записи с помощью @Testи @ParameterizedTestаннотации, и как работать с новыми аннотациями жизненного цикла в JUnit 5. Вы также увидите краткий пример использования тегов фильтра, и я покажу вам, как интегрировать JUnit 5 со сторонней библиотекой утверждений - в данном случае Hamcrest . Наконец, вы получите краткое руководство по интеграции JUnit 5 с Mockito, чтобы вы могли писать более надежные модульные тесты для сложных реальных систем.

скачать Получить код Получите исходный код примеров из этого руководства. Создано Стивеном Хейнсом для JavaWorld.

Разработка через тестирование

Если вы какое-то время занимались разработкой кода Java, вы, вероятно, хорошо знакомы с разработкой через тестирование, поэтому я буду держать этот раздел кратким. Однако важно понимать, почему мы пишем модульные тесты, а также какие стратегии используют разработчики при разработке модульных тестов.

Разработка через тестирование (TDD) - это процесс разработки программного обеспечения, в котором переплетаются кодирование, тестирование и дизайн. Это подход, ориентированный на тестирование и направленный на улучшение качества ваших приложений. Разработка через тестирование определяется следующим жизненным циклом:

  1. Добавить тест.
  2. Запустите все тесты и посмотрите, как новый тест не проходит.
  3. Реализуйте код.
  4. Запустите все тесты и посмотрите, как новый тест проходит успешно.
  5. Выполните рефакторинг кода.

На рисунке 1 показан жизненный цикл TDD.

Стивен Хейнс

Написание тестов перед написанием кода преследует двоякую цель. Во-первых, это заставляет задуматься о бизнес-проблеме, которую вы пытаетесь решить. Например, как должны вести себя успешные сценарии? Какие условия должны не выполняться? Как они должны потерпеть неудачу? Во-вторых, сначала тестирование дает вам больше уверенности в своих тестах. Всякий раз, когда я пишу тесты после написания кода, мне всегда приходится ломать их, чтобы убедиться, что они действительно выявляют ошибки. Написание тестов сначала позволяет избежать этого дополнительного шага.

Написание тестов для счастливого пути обычно несложно: при правильном вводе класс должен вернуть детерминированный ответ. Но написание отрицательных (или неудачных) тестовых примеров, особенно для сложных компонентов, может быть более сложным.

В качестве примера рассмотрим написание тестов для репозитория базы данных. По счастливому пути мы вставляем запись в базу данных и получаем обратно созданный объект, включая любые сгенерированные ключи. На самом деле мы также должны учитывать возможность конфликта, например, при вставке записи с уникальным значением столбца, которое уже содержится в другой записи. Кроме того, что происходит, когда репозиторий не может подключиться к базе данных, возможно, из-за изменения имени пользователя или пароля? Что произойдет, если при передаче возникнет сетевая ошибка? Что произойдет, если запрос не будет завершен в установленный вами предел времени ожидания?

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

Переход на JUnit 5

Если вы какое-то время использовали JUnit, некоторые изменения в JUnit 5 будут корректироваться. Вот краткое изложение различий между двумя версиями:

  • JUnit 5 теперь упакован в org.junit.jupiterгруппу, что меняет способ его включения в свои проекты Maven и Gradle.
  • JUnit 4 требует минимального JDK 5; JUnit 5 требует как минимум JDK 8.
  • JUnit 4 - х @Before, @BeforeClass, @After, и @AfterClassаннотации были заменены @BeforeEach, @BeforeAll, @AfterEachи @AfterAll, соответственно.
  • @IgnoreАннотация JUnit 4 была заменена @Disabledаннотацией.
  • @CategoryАннотаций был заменен @Tagаннотацией.
  • JUnit 5 добавляет новый набор методов утверждения.
  • Runners были заменены расширениями с новым API для разработчиков расширений.
  • JUnit 5 вводит предположения, которые останавливают выполнение теста.
  • JUnit 5 поддерживает вложенные и динамические тестовые классы.

В этой статье мы рассмотрим большинство из этих новых функций.

Модульное тестирование с JUnit 5

Начнем с простого, со сквозного примера настройки проекта для использования JUnit 5 для модульного теста. В листинге 1 показан MathToolsкласс, метод которого преобразует числитель и знаменатель в double.

Листинг 1. Пример проекта JUnit 5 (MathTools.java)

 package com.javaworld.geekcap.math; public class MathTools { public static double convertToDecimal(int numerator, int denominator) { if (denominator == 0) { throw new IllegalArgumentException("Denominator must not be 0"); } return (double)numerator / (double)denominator; } }

У нас есть два основных сценария тестирования MathToolsкласса и его метода:

  • Действует тест , в котором мы передаем ненулевые целые числа для числителя и знаменателя.
  • Сценарий провала , в котором мы передаем нулевое значение знаменателя.

В листинге 2 показан тестовый класс JUnit 5 для тестирования этих двух сценариев.

Листинг 2. Тестовый класс JUnit 5 (MathToolsTest.java)

 package com.javaworld.geekcap.math; import java.lang.IllegalArgumentException; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; class MathToolsTest { @Test void testConvertToDecimalSuccess() { double result = MathTools.convertToDecimal(3, 4); Assertions.assertEquals(0.75, result); } @Test void testConvertToDecimalInvalidDenominator() { Assertions.assertThrows(IllegalArgumentException.class, () -> MathTools.convertToDecimal(3, 0)); } }

В листинге 2 testConvertToDecimalInvalidDenominatorметод выполняет MathTools::convertToDecimalметод внутри assertThrowsвызова. Первый аргумент - это ожидаемый тип исключения. Второй аргумент - это функция, которая вызовет это исключение. assertThrowsМетод выполняет функцию и подтверждает , что ожидаемый тип исключения.

Класс Assertions и его методы

В  org.junit.jupiter.api.Testаннотации обозначен метод тестирования. Обратите внимание, что @Testаннотация теперь поступает из пакета JUnit 5 Jupiter API, а не из пакета JUnit 4 org.junit. testConvertToDecimalSuccessМетод сначала выполняет MathTools::convertToDecimalметод с числителем 3 и знаменателем 4, то утверждает , что результат равен 0,75. org.junit.jupiter.api.AssertionsКласс предоставляет набор staticметодов для сравнения фактических и ожидаемых результатов. В Assertionsклассе есть следующие методы, охватывающие большинство примитивных типов данных:

  • assertArrayEquals сравнивает содержимое фактического массива с ожидаемым массивом.
  • assertEquals сравнивает фактическое значение с ожидаемым значением.
  • assertNotEquals сравнивает два значения, чтобы убедиться, что они не равны.
  • assertTrue подтверждает, что предоставленное значение истинно.
  • assertFalse проверяет, что предоставленное значение ложно.
  • assertLinesMatchсравнивает два списка Strings.
  • assertNull validates that the provided value is null.
  • assertNotNull validates that the provided value is not null.
  • assertSame validates that two values reference the same object.
  • assertNotSame validates that two values do not reference the same object.
  • assertThrows validates that the execution of a method throws an expected exception (you can see this in the testConvertToDecimalInvalidDenominator example above).
  • assertTimeout validates that a supplied function completes within a specified timeout.
  • assertTimeoutPreemptively validates that a supplied function completes within a specified timeout, but once the timeout is reached it kills the function's execution.

If any of these assertion methods fail, the unit test is marked as failed. That failure notice will be written to the screen when you run the test, then saved in a report file.

Using delta with assertEquals

When using float and double values in an assertEquals, you can also specify a delta that represents a threshold of difference between the two. In our example we could have added a delta of 0.001, in case 0.75 was actually returned as 0.750001.

Analyzing your test results

In addition to validating a value or behavior, the assert methods can also accept a textual description of the error, which can help you diagnose failures. For example:

 Assertions.assertEquals(0.75, result, "The MathTools::convertToDecimal value did not return the correct value of 0.75 for 3/4"); Assertions.assertEquals(0.75, result, () -> "The MathTools::convertToDecimal value did not return the correct value of 0.75 for 3/4"); 

The output will show the expected value of 0.75 and the actual value. It will also display the specified message, which can help you understand the context of the error. The difference between the two variations is that the first one always creates the message, even if it is not displayed, whereas the second one only constructs the message if the assertion fails. In this case, the construction of the message is trivial, so it doesn't really matter. Still, there is no need to construct an error message for a test that passes, so it's usually a best practice to use the second style.

Finally, if you're using an IDE like IntelliJ to run your tests, each test method will be displayed by its method name. This is fine if your method names are readable, but you can also add a @DisplayName annotation to your test methods to better identify the tests:

@Test @DisplayName("Test successful decimal conversion") void testConvertToDecimalSuccess() { double result = MathTools.convertToDecimal(3, 4); Assertions.assertEquals(0.751, result); }

Running your unit test

In order to run JUnit 5 tests from a Maven project, you need to include the maven-surefire-plugin in the Maven pom.xml file and add a new dependency. Listing 3 shows the pom.xml file for this project.

Listing 3. Maven pom.xml for an example JUnit 5 project

  4.0.0 com.javaworld.geekcap junit5 jar 1.0-SNAPSHOT    org.apache.maven.plugins maven-compiler-plugin 3.8.1  8 8    org.apache.maven.plugins maven-surefire-plugin 3.0.0-M4    junit5 //maven.apache.org   org.junit.jupiter junit-jupiter 5.6.0 test   

JUnit 5 dependencies

JUnit 5 packages its components in the org.junit.jupiter group and we need to add the junit-jupiter artifact, which is an aggregator artifact that imports the following dependencies:

  • junit-jupiter-api defines the API for writing tests and extensions.
  • junit-jupiter-engine - реализация механизма тестирования, запускающего модульные тесты.
  • junit-jupiter-params обеспечивает поддержку параметризованных тестов.

Затем нам нужно добавить maven-surefire-pluginплагин сборки, чтобы запускать тесты.

Наконец, не забудьте включить в maven-compiler-pluginверсию Java 8 или новее, чтобы вы могли использовать такие функции Java 8, как лямбды.

Запустить его!

Используйте следующую команду для запуска тестового класса из вашей IDE или из Maven:

mvn clean test

В случае успеха вы должны увидеть результат, подобный следующему:

 [INFO] ------------------------------------------------------- [INFO] T E S T S [INFO] ------------------------------------------------------- [INFO] Running com.javaworld.geekcap.math.MathToolsTest [INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.04 s - in com.javaworld.geekcap.math.MathToolsTest [INFO] [INFO] Results: [INFO] [INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0 [INFO] [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESS [INFO] ------------------------------------------------------------------------ [INFO] Total time: 3.832 s [INFO] Finished at: 2020-02-16T08:21:15-05:00 [INFO] ------------------------------------------------------------------------