<style>.lazy{display:none}</style>Лучшие практики юнит-тестирования

Лучшие практики юнит-тестирования

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

Содержание

БЕСПЛАТНО СКАЧАТЬ КНИГИ в телеграм канале "Библиотека тестировщика"

Что такое юнит-тестирование?

Юнит-тестирование – это методология проверки исходного кода на пригодность к использованию в продакшен.

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

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

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

Для начала создадим класс 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.

Юнит-тестирование: best practices

Исходный код

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

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

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

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

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

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

Соглашение об именовании тест-кейсов

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

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

Поэтому следует называть тест с указанием действия и ожидания, например:

  • testCalculateAreaWithGeneralDoubleValueRadiusThatReturnsAreaInDouble
  • testCalculateAreaWithLargeDoubleValueRadiusThatReturnsAreaAsInfinity

Однако мы все ещё можем улучшить названия для лучшей читаемости.

Часто бывает полезно называть тестовые случаи в формате “given_when_then”, чтобы уточнить цель модульного теста:

public class CircleTest {

    //...

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

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

В формате Given, When и Then следует описывать и блоки кода. Это помогает разделить тест на три части: вход, действие и выход.

Сначала блок кода, соответствующий разделу given, создает тестовые объекты, моделирует данные и организует ввод. Далее блок кода секции when представляет конкретное действие или тестовый сценарий. Раздел then указывает на выходные данные кода, которые сверяются с ожидаемым результатом с помощью утверждений.

Ожидаемое и фактическое в юнит-тестировании

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

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

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

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

Отдавайте предпочтение простому тест-кейсу

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

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

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

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

Поэтому следует создать простой тест-кейс, который будет утверждать, что захардкоженое ожидаемое значение не совпадает с фактическим.

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

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

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

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

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

Специфические модульные тесты

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

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

Поэтому всегда пишите модульный тест для проверки одного конкретного сценария. Такой юнит-тест не будет слишком сложным для понимания. Более того, в дальнейшем его будет проще отлаживать и поддерживать.

Тестирование рабочих сценариев

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

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

Мокирование внешних сервисов

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

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

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

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

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

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

Аннотации

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

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

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

80% тестового покрытия

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

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

Примечание редакции: предлагаем почитать о сочетании 80/20 в статье “Принцип Парето в тестировании”.

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

Подход TDD

Test-Driven Development (TDD) – это методология, при которой тест-кейсы создаются до и в процессе реализации. Этот подход сочетается с процессом проектирования и выполнения исходного кода.

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

Автоматизация юнит-тестирования

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

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

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

Заключение

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

Перевод статьи «Best Practices for Unit Testing in Java».

Оставьте комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *