Перейти к основному содержимому

Лучшие практики для модульного тестирования в Java

· 7 мин. чтения

1. Обзор

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

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

В этом руководстве мы обсудим несколько передовых методов модульного тестирования в Java.

2. Что такое модульное тестирование?

Модульное тестирование — это методология тестирования исходного кода на предмет его пригодности для использования в производстве.

Мы начинаем писать модульные тесты с создания различных тестовых случаев для проверки поведения отдельной единицы исходного кода.

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

Давайте рассмотрим простой сценарий.

Для начала создадим класс Circle и реализуем в нем метод calculateArea :

public class Circle {

public static double calculateArea(double radius) {
return Math.PI * radius * radius;
}
}

Затем мы создадим модульные тесты для класса Circle , чтобы убедиться, что метод calculateArea работает должным образом.

Давайте создадим класс CalculatorTest в каталоге src/main/test :

public class CircleTest {

@Test
public void testCalculateArea() {
//...
}
}

В этом случае мы используем аннотацию JUnit @Test вместе с инструментами сборки, такими как Maven или Gradle , для запуска теста.

3. Лучшие практики

3.1. Исходный код

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

Кроме того, это исключает любую возможность запуска тестового кода в производственной среде.

Мы можем следовать шагам инструментов сборки, таких как Maven и Gradle, которые ищут каталог src/main/test для тестовых реализаций.

3.2. Соглашение об именах пакетов

Мы должны создать аналогичную структуру пакета в каталоге src/main/test для тестовых классов, таким образом улучшив читабельность и удобство сопровождения тестового кода.

Проще говоря, пакет тестового класса должен соответствовать пакету исходного класса , единицу исходного кода которого он будет тестировать.

Например, если наш класс Circle существует в пакете com.foreach.math, класс CircleTest также должен существовать в пакете com.foreach.math в структуре каталогов src/main/test .

3.3. Соглашение об именах тестовых случаев

Имена тестов должны быть информативными , и пользователи должны понимать поведение и ожидания теста, просто взглянув на само название.

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

Поэтому мы должны назвать тест с действием и ожиданием, например testCalculateAreaWithGeneralDoubleValueRadiusThatReturnsAreaInDouble , testCalculateAreaWithLargeDoubleValueRadiusThatReturnsAreaAsInfinity .

Тем не менее, мы все еще можем улучшить имена для лучшей читабельности.

Часто бывает полезно назвать тестовые случаи в Given_when_then , чтобы уточнить цель модульного теста :

public class CircleTest {

//...

@Test
public void givenRadius_whenCalculateArea_thenReturnArea() {
//...
}

@Test
public void givenDoubleMaxValueAsRadius_whenCalculateArea_thenReturnAreaAsInfinity() {
//...
}
}

Мы также должны описать блоки кода в формате Given , When и Then . Кроме того, это помогает дифференцировать тест на три части: ввод, действие и вывод.

Во-первых, блок кода, соответствующий данному разделу, создает тестовые объекты, имитирует данные и упорядочивает ввод.

Затем блок кода для раздела when представляет конкретное действие или тестовый сценарий.

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

3.4. Ожидаемое и фактическое

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

Чтобы подтвердить идею ожидаемых и фактических значений, мы можем взглянуть на определение метода assertEquals класса JUnit Assert :

public static void assertEquals(Object expected, Object actual)

Давайте используем утверждение в одном из наших тестовых случаев:

@Test 
public void givenRadius_whenCalculateArea_thenReturnArea() {
double actualArea = Circle.calculateArea(1d);
double expectedArea = 3.141592653589793;
Assert.assertEquals(expectedArea, actualArea);
}

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

3.5. Предпочитаете простой тестовый пример

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

Не рекомендуется вычислять площадь круга для сопоставления с возвращаемым значением метода calculateArea :

@Test 
public void givenRadius_whenCalculateArea_thenReturnArea() {
double actualArea = Circle.calculateArea(2d);
double expectedArea = 3.141592653589793 * 2 * 2;
Assert.assertEquals(expectedArea, actualArea);
}

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

Следовательно, мы должны создать простой тестовый пример, который сопоставляет жестко закодированное ожидаемое значение с фактическим.

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

3.6. Соответствующие утверждения

Всегда используйте правильные утверждения для проверки ожидаемых и фактических результатов. Мы должны использовать различные методы, доступные в классе Assert JUnit или подобных фреймворках, таких как AssertJ .

Например, мы уже использовали метод Assert.assertEquals для утверждения значения. Точно так же мы можем использовать assertNotEquals , чтобы проверить, не равны ли ожидаемые и фактические значения.

Другие методы, такие как assertNotNull , assertTrue и assertNotSame , полезны в отдельных утверждениях.

3.7. Конкретные модульные тесты

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

Конечно, иногда возникает соблазн проверить несколько сценариев в одном и том же тесте, но лучше держать их отдельно. Тогда, в случае сбоев тестов, будет проще определить, какой именно сценарий не сработал, и, соответственно, проще исправить код.

Поэтому всегда пишите модульный тест для проверки одного конкретного сценария.

Модульный тест не будет слишком сложным для понимания. Кроме того, позже будет проще отлаживать и поддерживать модульные тесты.

3.8. Сценарии тестового производства

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

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

3.9. Макет внешних служб

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

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

Мы можем использовать различные фреймворки, такие как Mockito , EasyMock и JMockit , для имитации внешних сервисов.

3.10. Избегайте избыточности кода

Создавайте все больше и больше вспомогательных функций для создания часто используемых объектов и имитации данных или внешних сервисов для аналогичных модульных тестов.

Как и в случае с другими рекомендациями, это повышает удобочитаемость и удобство сопровождения тестового кода.

3.11. Аннотации

Часто среды тестирования предоставляют аннотации для различных целей, например, для выполнения настройки, выполнения кода до и удаления после выполнения теста.

В нашем распоряжении различные аннотации, такие как @Before , @BeforeClass и @After JUnit, а также другие тестовые среды, такие как TestNG .

Мы должны использовать аннотации для подготовки системы к тестам , создавая данные, упорядочивая объекты и удаляя все это после каждого теста, чтобы тестовые случаи были изолированы друг от друга.

3.12. 80% покрытие тестами

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

Как правило, мы должны попытаться покрыть 80% кода модульными тестами.

Кроме того, мы можем использовать такие инструменты, как JaCoCo и Cobertura, вместе с Maven или Gradle для создания отчетов о покрытии кода.

3.13. TDD-подход

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

Преимущество включает тестируемый производственный код с самого начала, надежную реализацию с простым рефакторингом и меньшим количеством регрессий.

3.14. Автоматизация

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

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

Следовательно, выполнение модульных тестов должно быть частью конвейеров CI-CD и предупреждать заинтересованные стороны в случае сбоев.

4. Вывод

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