Руководство по надежным модульным и интеграционным тестам с JUnit

Опубликовано: 2022-03-11

Автоматизированные тесты программного обеспечения критически важны для долгосрочного качества, ремонтопригодности и расширяемости программных проектов, а для Java JUnit — это путь к автоматизации.

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

Среда тестирования JUnit — это распространенный бесплатный инструмент с открытым исходным кодом для тестирования проектов на основе Java.

На момент написания этой статьи JUnit 4 является текущим основным выпуском, выпущенным более 10 лет назад, а последнее обновление было выпущено более двух лет назад.

JUnit 5 (с моделями программирования и расширения Jupiter) находится в активной разработке. Он лучше поддерживает языковые функции, представленные в Java 8, и включает другие новые интересные функции. Некоторые команды могут найти JUnit 5 готовым к использованию, в то время как другие могут продолжать использовать JUnit 4 до официального выпуска версии 5. Мы рассмотрим примеры из обоих.

Запуск JUnit

Тесты JUnit можно запускать непосредственно в IntelliJ, но их также можно запускать в других IDE, таких как Eclipse, NetBeans или даже в командной строке.

Тесты всегда должны запускаться во время сборки, особенно модульные тесты. Сборка с любыми неудачными тестами должна считаться неудачной, независимо от того, находится ли проблема в рабочей среде или в тестовом коде — это требует от команды дисциплины и готовности уделить высший приоритет разрешению неудачных тестов, но необходимо придерживаться дух автоматизации.

Тесты JUnit также можно запускать и получать отчеты с помощью систем непрерывной интеграции, таких как Jenkins. Проекты, в которых используются такие инструменты, как Gradle, Maven или Ant, имеют дополнительное преимущество, заключающееся в возможности запускать тесты как часть процесса сборки.

Группы шестеренок, указывающие на совместимость: JUnit 4 с NetBeans в одном, JUnit 5 с Eclipse и Gradle в другом и последний с JUnit 5 с Maven и IntelliJ IDEA.

Грейдл

В качестве примера проекта Gradle для JUnit 5 см. раздел Gradle руководства пользователя JUnit и репозиторий junit5-samples.git. Обратите внимание, что он также может запускать тесты, использующие API JUnit 4 (называемый «винтажным» ).

Проект можно создать в IntelliJ с помощью пункта меню «Файл» > «Открыть…» > перейти в junit-gradle-consumer sub-directory > «ОК» > «Открыть как проект» > «ОК», чтобы импортировать проект из Gradle.

Для Eclipse плагин Buildship Gradle можно установить из Help > Eclipse Marketplace… Затем проект можно импортировать с помощью File > Import… > Gradle > Gradle Project > Next > Next > Перейти в junit-gradle-consumer > Next > Далее > Готово.

После настройки проекта Gradle в IntelliJ или Eclipse запуск задачи build Gradle будет включать запуск всех тестов JUnit с test задачей. Обратите внимание, что тесты могут быть пропущены при последующих запусках build , если в код не были внесены изменения.

Для JUnit 4 см. Использование JUnit с вики Gradle.

Мавен

Для JUnit 5 см. раздел Maven руководства пользователя и репозиторий junit5-samples.git для примера проекта Maven. Это также может запускать старые тесты (те, которые используют API JUnit 4).

В IntelliJ используйте «Файл» > «Открыть…» > перейдите к junit-maven-consumer/pom.xml > «ОК» > «Открыть как проект». Затем тесты можно запустить из Maven Projects > junit5-maven-consumer > Lifecycle > Test.

В Eclipse используйте «Файл» > «Импорт…» > «Maven» > «Существующие проекты Maven» > «Далее» > «Обзор» в junit-maven-consumer > с выбранным pom.xml > «Готово».

Тесты можно выполнить, запустив проект как Maven build… > укажите цель test > Выполнить.

Для JUnit 4 см. JUnit в репозитории Maven.

Среды разработки

Помимо запуска тестов с помощью инструментов сборки, таких как Gradle или Maven, многие IDE могут напрямую запускать тесты JUnit.

IntelliJ ИДЕЯ

IntelliJ IDEA 2016.2 или более поздней версии требуется для тестов JUnit 5, а тесты JUnit 4 должны работать в более старых версиях IntelliJ.

Для целей этой статьи вы можете создать новый проект в IntelliJ из одного из моих репозиториев GitHub ( JUnit5IntelliJ.git или JUnit4IntelliJ.git), который включает все файлы из простого примера класса Person и использует встроенный JUnit-библиотеки. Тест можно запустить, выбрав «Выполнить» > «Выполнить все тесты». Тест также можно запустить в IntelliJ из класса PersonTest .

Эти репозитории были созданы с помощью новых проектов IntelliJ Java и содержат структуры каталогов src/main/java/com/example и src/test/java/com/example . Каталог src/main/java был указан как исходная папка, а src/test/java — как тестовая исходная папка. После создания класса PersonTest с тестовым методом, аннотированным @Test , он может не скомпилироваться, и в этом случае IntelliJ предлагает добавить JUnit 4 или JUnit 5 в путь к классу, который можно загрузить из дистрибутива IntelliJ IDEA (см. ответы на переполнение стека для более подробной информации). Наконец, для всех тестов была добавлена ​​конфигурация запуска JUnit.

См. также Руководство по тестированию IntelliJ.

Затмение

Пустой проект Java в Eclipse не будет иметь тестового корневого каталога. Это было добавлено из проекта «Свойства»> «Путь сборки Java»> «Добавить папку…»> «Создать новую папку…»> укажите имя папки> «Готово». Новый каталог будет выбран в качестве исходной папки. Нажмите OK в обоих оставшихся диалогах.

Тесты JUnit 4 можно создать с помощью File > New > JUnit Test Case. Выберите «Новый тест JUnit 4» и только что созданную исходную папку для тестов. Укажите «тестируемый класс» и «пакет», убедившись, что пакет соответствует тестируемому классу. Затем укажите имя тестового класса. После завершения работы мастера, если будет предложено, выберите «Добавить библиотеку JUnit 4» в путь сборки. Затем проект или отдельный тестовый класс можно запустить как тест JUnit. См. также Написание Eclipse и выполнение тестов JUnit.

NetBeans

NetBeans поддерживает только тесты JUnit 4. Тестовые классы можно создать в проекте Java NetBeans, выбрав Файл > Новый файл… > Модульные тесты > Тест JUnit или Тест для существующего класса. По умолчанию корневой каталог теста называется test в каталоге проекта.

Простой производственный класс и его тестовый пример JUnit

Давайте рассмотрим простой пример производственного кода и соответствующий ему код модульного тестирования для очень простого класса Person . Вы можете скачать образец кода из моего проекта на github и открыть его через IntelliJ.

src/main/java/com/example/Person.java
 package com.example; class Person { private final String givenName; private final String surname; Person(String givenName, String surname) { this.givenName = givenName; this.surname = surname; } String getDisplayName() { return surname + ", " + givenName; } }

Неизменяемый класс Person имеет конструктор и метод getDisplayName() . Мы хотим проверить, что getDisplayName() возвращает имя, отформатированное так, как мы ожидаем. Вот тестовый код для одиночного модульного теста (JUnit 5):

src/test/java/com/example/PersonTest.java
 package com.example; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.*; class PersonTest { @Test void testGetDisplayName() { Person person = new Person("Josh", "Hayden"); String displayName = person.getDisplayName(); assertEquals("Hayden, Josh", displayName); } }

PersonTest использует @Test и утверждение JUnit 5. Для JUnit 4 класс и метод PersonTest должны быть общедоступными, и следует использовать другой импорт. Вот пример JUnit 4 Gist.

После запуска класса PersonTest в IntelliJ тест проходит успешно, а индикаторы пользовательского интерфейса становятся зелеными.

Общие соглашения JUnit

Именование

Хотя это и не требуется, мы используем общие соглашения при именовании тестового класса; в частности, мы начинаем с имени тестируемого класса ( Person ) и добавляем к нему «Test» ( PersonTest ). Именование тестового метода аналогично, начиная с тестируемого метода ( getDisplayName() ) и добавляя к нему слово «test» ( testGetDisplayName() ). Несмотря на то, что существует множество других вполне приемлемых соглашений для именования тестовых методов, важно, чтобы они были согласованными между командой и проектом.

Имя в производстве Имя в тестировании
Человек Тест человека
getDisplayName() testDisplayName()

Пакеты

Мы также используем соглашение о создании класса PersonTest тестового кода в том же пакете ( com.example ), что и класс Person производственного кода. Если бы мы использовали другой пакет для тестов, нам пришлось бы использовать модификатор открытого доступа в классах производственного кода, конструкторах и методах, на которые ссылаются модульные тесты, даже там, где это неуместно, поэтому лучше просто хранить их в одном пакете. . Однако мы используем отдельные исходные каталоги ( src/main/java и src/test/java ), поскольку обычно не хотим включать тестовый код в выпущенные производственные сборки.

Структура и аннотация

Аннотация @Test (JUnit 4/5) указывает JUnit выполнить метод testGetDisplayName() в качестве тестового метода и сообщить, проходит он или нет. До тех пор, пока все утверждения (если они есть) проходят успешно и не выдаются исключения, тест считается пройденным.

Наш тестовый код следует структурному шаблону Arrange-Act-Assert (AAA). Другие распространенные шаблоны включают в себя Given-When-Then и Setup-Exercise-Verify-Teardown (Teardown обычно явно не требуется для модульных тестов), но в этой статье мы используем AAA.

Давайте посмотрим, как наш тестовый пример соответствует AAA. Первая строка «упорядочить» создает объект Person , который будет протестирован:

 Person person = new Person("Josh", "Hayden");

Вторая строка, «act», выполняет метод Person.getDisplayName() производственного кода:

 String displayName = person.getDisplayName();

Третья строка, «утверждение», подтверждает, что результат соответствует ожидаемому.

 assertEquals("Hayden, Josh", displayName);

Внутри assertEquals() использует метод equals объекта String «Хейден, Джош» для проверки совпадения фактического значения, возвращаемого из производственного кода ( displayName ). Если бы он не совпадал, тест был бы помечен как не пройденный.

Обратите внимание, что тесты часто имеют более одной строки для каждой из этих фаз AAA.

Модульные тесты и производственный код

Теперь, когда мы рассмотрели некоторые соглашения по тестированию, давайте обратим внимание на обеспечение тестируемости производственного кода.

Мы возвращаемся к нашему классу Person , где я реализовал метод для возврата возраста человека на основе его или ее даты рождения. В примерах кода требуется Java 8, чтобы использовать новые даты и функциональные API. Вот как выглядит новый класс Person.java :

Person.java
 // ... class Person { // ... private final LocalDate dateOfBirth; Person(String givenName, String surname, LocalDate dateOfBirth) { // ... this.dateOfBirth = dateOfBirth; } // ... long getAge() { return ChronoUnit.YEARS.between(dateOfBirth, LocalDate.now()); } public static void main(String... args) { Person person = new Person("Joey", "Doe", LocalDate.parse("2013-01-12")); System.out.println(person.getDisplayName() + ": " + person.getAge() + " years"); // Doe, Joey: 4 years } }

Запуск этого класса (на момент написания) объявляет, что Джоуи 4 года. Добавим тестовый метод:

PersonTest.java
 // ... class PersonTest { // ... @Test void testGetAge() { Person person = new Person("Joey", "Doe", LocalDate.parse("2013-01-12")); long age = person.getAge(); assertEquals(4, age); } }

Это проходит сегодня, но что насчет того, когда мы запустим его через год? Этот тест является недетерминированным и ненадежным, поскольку ожидаемый результат зависит от текущей даты системы, выполняющей тест.

Заглушка и внедрение поставщика ценности

При работе в производственной среде мы хотим использовать текущую дату LocalDate.now() для вычисления возраста человека, но для проведения детерминированного теста даже через год тесты должны предоставлять свои собственные значения currentDate .

Это известно как внедрение зависимостей. Мы не хотим, чтобы наш объект Person сам определял текущую дату, но вместо этого мы хотим передать эту логику как зависимость. Модульные тесты будут использовать известное заглушенное значение, а производственный код позволит системе предоставить фактическое значение во время выполнения.

Давайте добавим поставщика LocalDate в Person.java :

Person.java
 // ... class Person { // ... private final LocalDate dateOfBirth; private final Supplier<LocalDate> currentDateSupplier; Person(String givenName, String surname, LocalDate dateOfBirth) { this(givenName, surname, dateOfBirth, LocalDate::now); } // Visible for testing Person(String givenName, String surname, LocalDate dateOfBirth, Supplier<LocalDate> currentDateSupplier) { // ... this.dateOfBirth = dateOfBirth; this.currentDateSupplier = currentDateSupplier; } // ... long getAge() { return ChronoUnit.YEARS.between(dateOfBirth, currentDateSupplier.get()); } public static void main(String... args) { Person person = new Person("Joey", "Doe", LocalDate.parse("2013-01-12")); System.out.println(person.getDisplayName() + ": " + person.getAge() + " years"); // Doe, Joey: 4 years } }

Чтобы упростить тестирование getAge() , мы изменили его, чтобы использовать currentDateSupplier , поставщика LocalDate , для получения текущей даты. Если вы не знаете, что такое поставщик, рекомендую прочитать о встроенных функциональных интерфейсах Lambda.

Мы также добавили внедрение зависимостей: новый конструктор тестирования позволяет тестам предоставлять свои собственные текущие значения даты. Исходный конструктор вызывает этот новый конструктор, передавая ссылку на статический метод LocalDate::now , который предоставляет объект LocalDate , поэтому наш основной метод по-прежнему работает, как и раньше. А как насчет нашего метода тестирования? PersonTest.java :

PersonTest.java
 // ... class PersonTest { // ... @Test void testGetAge() { LocalDate dateOfBirth = LocalDate.parse("2013-01-02"); LocalDate currentDate = LocalDate.parse("2017-01-17"); Person person = new Person("Joey", "Doe", dateOfBirth, ()->currentDate); long age = person.getAge(); assertEquals(4, age); } }

Теперь тест вставляет свое собственное значение currentDate , поэтому наш тест все равно будет пройден при запуске в следующем году или в течение любого года. Это обычно называется заглушкой или предоставлением известного возвращаемого значения, но сначала нам пришлось изменить Person , чтобы разрешить внедрение этой зависимости.

Обратите внимание на лямбда-синтаксис ( ()->currentDate ) при создании объекта Person . Это рассматривается как поставщик LocalDate , как того требует новый конструктор.

Имитация и заглушка веб-службы

Мы готовы к тому, чтобы наш объект Person , все существование которого было в памяти JVM, мог взаимодействовать с внешним миром. Мы хотим добавить два метода: метод publishAge() , который будет публиковать текущий возраст человека, и метод getThoseInCommon() , который будет возвращать имена известных людей, у которых один день рождения или один возраст с нашим Person . Предположим, есть сервис RESTful, с которым мы можем взаимодействовать, который называется «Дни рождения людей». Для этого у нас есть Java-клиент, состоящий из одного класса BirthdaysClient .

com.example.birthdays.BirthdaysClient
 package com.example.birthdays; import java.io.IOException; import java.util.Arrays; import java.util.Collection; public class BirthdaysClient { public void publishRegularPersonAge(String name, long age) throws IOException { System.out.println("publishing " + name + "'s age: " + age); // HTTP POST with name and age and possibly throw an exception } public Collection<String> findFamousNamesOfAge(long age) throws IOException { System.out.println("finding famous names of age " + age); return Arrays.asList(/* HTTP GET with age and possibly throw an exception */); } public Collection<String> findFamousNamesBornOn(int month, int dayOfMonth) throws IOException { System.out.println("finding famous names born on day " + dayOfMonth + " of month " + month); return Arrays.asList(/* HTTP GET with month and day and possibly throw an exception */); } }

Давайте улучшим наш класс Person . Мы начинаем с добавления нового тестового метода для желаемого поведения publishAge() . Зачем начинать с теста, а не с функциональности? Мы следуем принципам разработки через тестирование (также известной как TDD), согласно которой сначала мы пишем тест, а затем код, чтобы он прошел.

PersonTest.java
 // … class PersonTest { // … @Test void testPublishAge() { LocalDate dateOfBirth = LocalDate.parse("2000-01-02"); LocalDate currentDate = LocalDate.parse("2017-01-01"); Person person = new Person("Joe", "Sixteen", dateOfBirth, ()->currentDate); person.publishAge(); } }

На этом этапе тестовый код не компилируется, потому что мы не создали вызываемый им метод publishAge() . Как только мы создадим пустой Person.publishAge() , все пройдет. Теперь мы готовы к тесту, чтобы убедиться, что возраст человека действительно публикуется в BirthdaysClient .

Добавление фиктивного объекта

Поскольку это модульный тест, он должен выполняться быстро и в памяти, поэтому тест создаст наш объект Person с фиктивным BirthdaysClient , поэтому на самом деле он не будет делать веб-запрос. Затем тест будет использовать этот фиктивный объект, чтобы убедиться, что он был вызван, как ожидалось. Для этого мы добавим зависимость от фреймворка Mockito (лицензия MIT) для создания фиктивных объектов, а затем создадим фиктивный объект BirthdaysClient :

PersonTest.java
 // ... import com.example.birthdays.BirthdaysClient; // ... import static org.mockito.Mockito.mock; class PersonTest { private BirthdaysClient birthdaysClient = mock(BirthdaysClient.class); // ... @Test void testPublishAge() { // ... Person person = new Person("Joe", "Sixteen", dateOfBirth, ()->currentDate, birthdaysClient); // ... } }

Кроме того, мы расширили сигнатуру конструктора Person , чтобы он принимал объект BirthdaysClient , и изменили тест, чтобы внедрить фиктивный объект BirthdaysClient .

Добавление фиктивного ожидания

Затем мы добавляем в конец нашего testPublishAge ожидание того, что будет вызван BirthdaysClient . Person.publishAge() должен вызвать его, как показано в нашем новом PersonTest.java :

PersonTest.java
 // ... class PersonTest { // ... @Test void testPublishAge() throws IOException { // ... Person person = new Person("Joe", "Sixteen", dateOfBirth, ()->currentDate, birthdaysClient); verifyZeroInteractions(birthdaysClient); person.publishAge(); verify(birthdaysClient).publishRegularPersonAge("Joe Sixteen", 16); } }

Наш Mockito-расширенный BirthdaysClient отслеживает все вызовы, которые были сделаны к его методам, и именно так мы проверяем, не было ли вызовов BirthdaysClient с помощью verifyZeroInteractions() перед вызовом publishAge() . Хотя, возможно, это и не обязательно, тем самым мы гарантируем, что конструктор не будет делать никаких мошеннических вызовов. В строке verify() мы указываем, как должен выглядеть вызов BirthdaysClient .

Обратите внимание: поскольку в сигнатуре publishRegularPersonAge есть IOException, мы также добавляем его в сигнатуру нашего тестового метода.

На данный момент тест не проходит:

 Wanted but not invoked: birthdaysClient.publishRegularPersonAge( "Joe Sixteen", 16L ); -> at com.example.PersonTest.testPublishAge(PersonTest.java:40)

Это ожидаемо, учитывая, что мы еще не внесли необходимые изменения в Person.java , так как мы следуем разработке через тестирование. Теперь мы пройдем этот тест, внеся необходимые изменения:

Person.java
 // ... class Person { // ... private final BirthdaysClient birthdaysClient; Person(String givenName, String surname, LocalDate dateOfBirth) { this(givenName, surname, dateOfBirth, LocalDate::now, new BirthdaysClient()); } // Visible for testing Person(String givenName, String surname, LocalDate dateOfBirth, Supplier<LocalDate> currentDateSupplier, BirthdaysClient birthdaysClient) { // ... this.birthdaysClient = birthdaysClient; } // ... void publishAge() { String nameToPublish = givenName + " " + surname; long age = getAge(); try { birthdaysClient.publishRegularPersonAge(nameToPublish, age); } catch (IOException e) { // TODO handle this! e.printStackTrace(); } } }

Проверка исключений

Мы заставили конструктор производственного кода создавать экземпляр нового BirthdaysClient , а publishAge() теперь вызывает birthdaysClient . Все тесты проходят; все зелено. Здорово! Но обратите внимание, что publishAge() проглатывает IOException. Вместо того, чтобы позволять ему всплывать, мы хотим обернуть его нашим собственным PersonException в новый файл с именем PersonException.java :

PersonException.java
 package com.example; public class PersonException extends Exception { public PersonException(String message, Throwable cause) { super(message, cause); } }

Мы реализуем этот сценарий как новый тестовый метод в PersonTest.java :

PersonTest.java
 // ... class PersonTest { // ... @Test void testPublishAge_IOException() throws IOException { LocalDate dateOfBirth = LocalDate.parse("2000-01-02"); LocalDate currentDate = LocalDate.parse("2017-01-01"); Person person = new Person("Joe", "Sixteen", dateOfBirth, ()->currentDate, birthdaysClient); IOException ioException = new IOException(); doThrow(ioException).when(birthdaysClient).publishRegularPersonAge("Joe Sixteen", 16); try { person.publishAge(); fail("expected exception not thrown"); } catch (PersonException e) { assertSame(ioException, e.getCause()); assertEquals("Failed to publish Joe Sixteen age 16", e.getMessage()); } } }

Вызов Mockito doThrow() заглушает birthdaysClient для создания исключения при вызове метода publishRegularPersonAge() . Если PersonException не выбрасывается, мы не проходим тест. В противном случае мы утверждаем, что исключение было правильно связано с IOException, и проверяем, что сообщение об исключении соответствует ожидаемому. Прямо сейчас, поскольку мы не реализовали никакой обработки в нашем производственном коде, наш тест завершается неудачно, потому что ожидаемое исключение не было сгенерировано. Вот что нам нужно изменить в Person.java , чтобы тест прошел:

Person.java
 // ... class Person { // ... void publishAge() throws PersonException { // ... try { // ... } catch (IOException e) { throw new PersonException("Failed to publish " + nameToPublish + " age " + age, e); } } }

Заглушки: когда и утверждения

Теперь мы реализуем метод Person.getThoseInCommon() , в результате чего наш класс Person.Java выглядеть следующим образом.

Наш testGetThoseInCommon() , в отличие от testPublishAge() , не проверяет, были ли сделаны конкретные вызовы методов birthdaysClient . Вместо этого он использует, when вызовы заглушки возвращают значения для вызовов findFamousNamesOfAge() и findFamousNamesBornOn() , которые необходимо выполнить getThoseInCommon() . Затем мы утверждаем, что все три предоставленных нами заглушенных имени возвращены.

Обертывание нескольких утверждений с помощью assertAll() JUnit 5 позволяет проверять все утверждения как единое целое, а не останавливаться после первого ошибочного утверждения. Мы также включаем сообщение с assertTrue() , чтобы идентифицировать конкретные имена, которые не включены. Вот как выглядит наш метод тестирования «счастливого пути» (идеального сценария) (обратите внимание, это не надежный набор тестов по своей природе «счастливого пути», но мы поговорим о том, почему позже.

PersonTest.java
 // ... class PersonTest { // ... @Test void testGetThoseInCommon() throws IOException, PersonException { LocalDate dateOfBirth = LocalDate.parse("2000-01-02"); LocalDate currentDate = LocalDate.parse("2017-01-01"); Person person = new Person("Joe", "Sixteen", dateOfBirth, ()->currentDate, birthdaysClient); when(birthdaysClient.findFamousNamesOfAge(16)).thenReturn(Arrays.asList("JoeFamous Sixteen", "Another Person")); when(birthdaysClient.findFamousNamesBornOn(1, 2)).thenReturn(Arrays.asList("Jan TwoKnown")); Set<String> thoseInCommon = person.getThoseInCommon(); assertAll( setContains(thoseInCommon, "Another Person"), setContains(thoseInCommon, "Jan TwoKnown"), setContains(thoseInCommon, "JoeFamous Sixteen"), ()-> assertEquals(3, thoseInCommon.size()) ); } private <T> Executable setContains(Set<T> set, T expected) { return () -> assertTrue(set.contains(expected), "Should contain " + expected); } // ... }

Держите тестовый код в чистоте

Хотя это часто упускают из виду, не менее важно не допускать дублирования тестового кода. Чистый код и такие принципы, как «не повторяйтесь», очень важны для поддержания высокого качества кодовой базы, производственного и тестового кода. Обратите внимание, что самый последний PersonTest.java имеет некоторое дублирование теперь, когда у нас есть несколько тестовых методов.

Чтобы исправить это, мы можем сделать несколько вещей:

  • Извлеките объект IOException в закрытое конечное поле.

  • Извлеките создание объекта Person в отдельный метод ( в данном случае createJoeSixteenJan2() ), поскольку большинство объектов Person создаются с одинаковыми параметрами.

  • Создайте assertCauseAndMessage() для различных тестов, проверяющих выброшенные PersonExceptions .

Результаты чистого кода можно увидеть в этой версии файла PersonTest.java.

Испытайте больше, чем счастливый путь

Что нам делать, если у объекта Person дата рождения более поздняя, ​​чем текущая дата? Дефекты в приложениях часто возникают из-за неожиданных входных данных или непредусмотрительности угловых, краевых или граничных случаев. Важно попытаться предвидеть такие ситуации как можно лучше, и модульные тесты часто являются подходящим местом для этого. При создании Person и PersonTest мы включили несколько тестов для ожидаемых исключений, но они ни в коем случае не были завершены. Например, мы используем LocalDate , который не представляет и не хранит данные о часовом поясе. Однако наши вызовы LocalDate.now() возвращают LocalDate на основе часового пояса системы по умолчанию, который может быть на день раньше или позже, чем у пользователя системы. Эти факторы следует учитывать с помощью соответствующих тестов и поведения.

Границы также должны быть проверены. Рассмотрим объект Person с getDaysUntilBirthday() . Тестирование должно включать в себя проверку того, прошел ли день рождения человека в текущем году, является ли день рождения человека сегодня и как високосный год влияет на количество дней. Эти сценарии можно охватить, проверив один день до дня рождения человека, день и один день после дня рождения человека, где следующий год является високосным. Вот соответствующий тестовый код:

PersonTest.java
 // ... class PersonTest { private final Supplier<LocalDate> currentDateSupplier = ()-> LocalDate.parse("2015-05-02"); private final LocalDate ageJustOver5 = LocalDate.parse("2010-05-01"); private final LocalDate ageExactly5 = LocalDate.parse("2010-05-02"); private final LocalDate ageAlmost5 = LocalDate.parse("2010-05-03"); // ... @Test void testGetDaysUntilBirthday() { assertAll( createPersonAndAssertValue(ageAlmost5, 1, Person::getDaysUntilBirthday), createPersonAndAssertValue(ageExactly5, 0, Person::getDaysUntilBirthday), createPersonAndAssertValue(ageJustOver5, 365, Person::getDaysUntilBirthday) ); } private Executable createPersonAndAssertValue(LocalDate dateOfBirth, long expectedValue, Function<Person, Long> personLongFunction) { Person person = new Person("Given", "Sur", dateOfBirth, currentDateSupplier); long actualValue = personLongFunction.apply(person); return () -> assertEquals(expectedValue, actualValue); } }

Интеграционные тесты

В основном мы сосредоточились на модульных тестах, но JUnit также можно использовать для интеграционных, приемочных, функциональных и системных тестов. Такие тесты часто требуют большего количества установочного кода, например, запуск серверов, загрузка баз данных известными данными и т. д. Хотя мы часто можем запустить тысячи модульных тестов за секунды, запуск больших наборов тестов интеграции может занять минуты или даже часы. Интеграционные тесты, как правило, не должны использоваться, чтобы попытаться охватить каждую перестановку или путь в коде; Модульные тесты больше подходят для этого.

Создание тестов для веб-приложений, которые управляют веб-браузерами при заполнении форм, нажатии кнопок, ожидании загрузки контента и т. д., обычно выполняется с использованием Selenium WebDriver (лицензия Apache 2.0) в сочетании с «шаблоном объекта страницы» (см. вики SeleniumHQ github). и статью Мартина Фаулера об объектах страницы).

JUnit эффективен для тестирования RESTful API с использованием HTTP-клиента, такого как HTTP-клиент Apache или Spring Rest Template (хороший пример — HowToDoInJava.com).

В нашем случае с объектом Person интеграционный тест может включать использование настоящего BirthdaysClient , а не фиктивного, с конфигурацией, указывающей базовый URL-адрес службы People Birthdays. Затем интеграционный тест будет использовать тестовый экземпляр такой службы, проверять, опубликованы ли в нем дни рождения, и создавать известных людей в службе, которые будут возвращены.

Другие функции JUnit

JUnit имеет множество дополнительных функций, которые мы еще не рассмотрели в примерах. Мы опишем некоторые и предоставим ссылки для других.

Тестовые приспособления

Следует отметить, что JUnit создает новый экземпляр тестового класса для запуска каждого метода @Test . JUnit также предоставляет перехватчики аннотаций для запуска определенных методов до или после всех или каждого из методов @Test . Эти хуки часто используются для настройки или очистки базы данных или фиктивных объектов и различаются между JUnit 4 и 5.

Юнит 4 Юнит 5 Для статического метода?
@BeforeClass @BeforeAll да
@AfterClass @AfterAll да
@Before @BeforeEach Нет
@After @AfterEach Нет

В нашем примере PersonTest мы решили настроить фиктивный объект BirthdaysClient в самих методах @Test , но иногда необходимо создавать более сложные фиктивные структуры, включающие несколько объектов. @BeforeEach (в JUnit 5) и @Before (в JUnit 4) часто подходят для этого.

Аннотации @After* чаще используются в интеграционных тестах, чем в модульных тестах, поскольку сборка мусора JVM обрабатывает большинство объектов, созданных для модульных тестов. @BeforeClass и @BeforeAll чаще всего используются для интеграционных тестов, которые требуют однократного выполнения дорогостоящих действий по настройке и демонтажу, а не для каждого метода тестирования.

Для JUnit 4 см. руководство по тестовым приборам (общие концепции по-прежнему применимы к JUnit 5).

Наборы тестов

Иногда вам нужно запустить несколько связанных тестов, но не все тесты. В этом случае группы тестов могут быть объединены в наборы тестов. Чтобы узнать, как это сделать в JUnit 5, ознакомьтесь со статьей HowToProgram.xyz для JUnit 5 и документацией группы JUnit для JUnit 4.

@Nested и @DisplayName в JUnit 5

В JUnit 5 добавлена ​​возможность использовать нестатические вложенные внутренние классы, чтобы лучше показать взаимосвязь между тестами. Это должно быть хорошо знакомо тем, кто работал с вложенными описаниями в тестовых средах, таких как Jasmine для JavaScript. Внутренние классы аннотированы @Nested , чтобы использовать это.

Аннотация @DisplayName также является новой для JUnit 5, позволяя вам описать тест для отчета в строковом формате, который будет отображаться в дополнение к идентификатору метода тестирования.

Хотя @Nested и @DisplayName можно использовать независимо друг от друга, вместе они могут обеспечить более четкие результаты тестирования, описывающие поведение системы.

Хэмкрест Матчерс

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

 //Traditional assert assertEquals("Hayden, Josh", displayName); //Hamcrest assert assertThat(displayName, equalTo("Hayden, Josh"));

Hamcrest можно использовать как с JUnit 4, так и с 5. Учебное пособие Vogella.com по Hamcrest достаточно подробное.

Дополнительные ресурсы

  • В статье Unit Tests, How to Write Testable Code and Why it Matters рассматриваются более конкретные примеры написания чистого, тестируемого кода.

  • Создавайте с уверенностью: руководство по тестам JUnit исследует различные подходы к модульному и интеграционному тестированию, а также почему лучше выбрать один и придерживаться его.

  • JUnit 4 Wiki и Руководство пользователя JUnit 5 всегда являются отличным ориентиром.

  • Документация Mockito содержит информацию о дополнительных функциях и примеры.

JUnit — путь к автоматизации

Мы изучили многие аспекты тестирования в мире Java с помощью JUnit. Мы рассмотрели модульные и интеграционные тесты с использованием платформы JUnit для кодовых баз Java, интеграцию JUnit в среды разработки и сборки, как использовать макеты и заглушки с поставщиками и Mockito, общие соглашения и лучшие практики кода, что тестировать, а также некоторые из другие замечательные функции JUnit.

Теперь настала очередь читателя научиться умело применять, поддерживать и пожинать плоды автоматических тестов с использованием среды JUnit.