Код
#статьи

JUnit: модульное тестирование в Java и test-driven development

Изучаем главный инструмент тестировщика приложений на Java и пишем первую программу по методологии TDD.

Иллюстрация: Оля Ежак для Skillbox Media

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

Для таких случаев используют специальные тестовые фреймворки. Один из них — JUnit, фреймворк для модульного тестирования Java-приложений. Он позволяет проводить автоматизированные юнит- и интеграционные тесты.

Из этой статьи вы узнаете:


Что такое JUnit

JUnit — фреймворк для автоматического юнит-тестирования приложений. Он содержит специальные функции и правила, которые позволяют легко писать и запускать тесты, то есть проверять, что каждый блок кода, или модуль, ответственный за определённую функцию программы, работает как надо. Такой вид тестирования называют модульным, или юнит-тестированием.

Последняя версия фреймворка — JUnit 5. Она состоит из трёх модулей: JUnit Platform, JUnit Jupiter и JUnit Vintage.

JUnit Platform — основной модуль для управления тестами.

JUnit Jupiter — модуль, который использует новые возможности Java 8. Он предоставляет API на основе аннотаций и позволяет работать с модульными и динамическими тестами.

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

JUnit удобен тем, что разработчик может гибко указывать условия тестирования. Например, объединять тесты в группы, распределяя их по функциональности, тестируемым модулям или уровню критичности, прописывать условия запуска для каждого блока кода и анализировать результаты по отдельности. Всё это облегчает работу программиста или QA-инженера.

Аннотации в JUnit

Аннотации в JUnit — это специальные метки, которые Java-разработчик размещает перед методами в тестовом классе. Они позволяют настраивать процесс тестирования, указывая фреймворку, как именно их следует обрабатывать. Например, можно явно указать, какие из методов являются тестовыми случаями, какие из них выполнять перед тестами и после и так далее.

Вот несколько базовых аннотаций.

@Test. Эту аннотацию ставим перед методами, которые относятся к тестовым случаям. JUnit поймёт, что их следует выполнять в качестве теста, а по завершении проверить результат.

@Before. Используется для методов, которые должны быть выполнены перед каждым тестовым случаем. Например, если у нас есть несколько тестов, которые требуют одних и тех же начальных условий, мы можем обозначить метод с аннотацией @Before, задав необходимые условия тестирования один раз.

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

@BeforeClass, @AfterClass. Методы с аннотацией @BeforeClass выполняются перед запуском первого теста в классе, а методы с аннотацией @AfterClass — после завершения всех тестов в классе.

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

@BeforeEach и @AfterEach. Аналоги @Before и @After в JUnit 4.

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

Вот как аннотации выглядят в коде:

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.AfterEach;

public class MyTest {

    @BeforeEach
    public void setUp() {
        // Метод, выполняющийся перед каждым тестовым случаем
    }

    @AfterEach
    public void tearDown() {
        // Метод, выполняющийся после каждого тестового случая
    }

    @Test
    public void testSomething() {
        // Тестовый случай
    }

    @Test
    public void testAnotherThing() {
        // Другой тестовый случай
    }
}

JUnit 4 и JUnit 5

JUnit 4 и JUnit 5 — это две последние версии фреймворка JUnit, которые сейчас распространены в разработке. Между собой они различаются функциональностью, синтаксисом и возможностями.

JUnit 4

Тестовые методы в JUnit 4 помечаются аннотацией @Test. Для определения методов, выполняющихся до и после тестового случая, используют аннотации @Before и @After.

В этой версии фреймворка поддерживаются параметризованные тесты с использованием аннотации @RunWith(Parameterized.class). Это значит, что в тест можно передать параметры, необходимые для тестирования.

Тесты, написанные на JUnit 4, могут выполняться в JUnit 5 с использованием JUnit Vintage.

JUnit 5

В пятой версии фреймворка появились новые модули: Jupiter — для тестирования с использованием возможностей Java 8, и Platform — модуль для запуска тестов.

Также в JUnit 5 появилась возможность писать собственные расширения для тестов и запускать их по аннотации @ExtendWith. По аналогии с JUnit 4 поддерживаются параметризованные тесты с аннотацией @ParameterizedTest.

В примерах с кодом мы будем использовать JUnit 5 — это современная версия фреймворка, которая поддерживает Java 8 и JUnit 4.

Устанавливаем JUnit

Всё просто — добавляем необходимую зависимость в конфигурационный файл сборщика.

Для Maven:

  • Зайдите в файл pom.xml.
  • Найдите секцию <dependencies>.
  • Добавьте внутрь блок:
<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter-api</artifactId>
    <version>5.8.2</version> <!-- Версия может быть другой, актуальную версию смотрите на сайте JUnit -->
    <scope>test</scope>
</dependency>
  • Сохраните изменения.

Для Gradle:

  • Зайдите в build.gradle.
  • Найдите секцию dependencies.
  • Добавьте внутрь блок с кодом:
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.2'

Важно, что при работе с Gradle необходимо указать версию фреймворка. Мы рекомендуем использовать наиболее актуальную. Посмотреть её можно на главной странице сайта под заголовком Latest Release.

  • Сохраните изменения.

Как работает JUnit

Напишем на Java простой калькулятор:

public class Calculator {

    public int add(int a, int b) {
        return a + b;
    }

    public int subtract(int a, int b) {
        return a - b;
    }

    public int multiply(int a, int b) {
        return a * b;
    }

    public int divide(int a, int b) {
        if (b == 0) {
            throw new IllegalArgumentException("Cannot divide by zero");
        }
        return a / b;
    }
}

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

import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;

public class CalculatorTest {

    @Test
    public void testAddition() {
        Calculator calculator = new Calculator();
        int result = calculator.add(3, 5);
        assertEquals(8, result);
    }

    @Test
    public void testSubtraction() {
        Calculator calculator = new Calculator();
        int result = calculator.subtract(10, 4);
        assertEquals(6, result);
    }

    @Test
    public void testMultiplication() {
        Calculator calculator = new Calculator();
        int result = calculator.multiply(6, 3);
        assertEquals(18, result);
    }

    @Test
    public void testDivision() {
        Calculator calculator = new Calculator();
        int result = calculator.divide(10, 2);
        assertEquals(5, result);
    }

    @Test
    public void testDivisionByZero() {
        Calculator calculator = new Calculator();
        assertThrows(IllegalArgumentException.class, () -> {
            calculator.divide(10, 0);
        });
    }
}

Разберем его:

import org.junit.jupiter.api.Test; — здесь мы импортировали аннотацию Test из фреймворка JUnit. Она помечает методы как тестовые случаи, определяя их выполнение во время запуска тестов.

import static org.junit.jupiter.api.Assertions.*; — импортировали статические методы утверждений (assertions) из класса Assert — assertEquals(expected, actual). Они сравнивают ожидаемые и фактические результаты тестов. Если результаты не совпадают, то тест считается не пройденным.

public class CalculatorTest {… } — определили класс для наших тестов.

Далее мы прописали тестовые методы, например testAddition(), testSubtraction(), testMultiplication(), public void testDivision(). Внутри каждого метода тестируем конкретную арифметическую операцию. Для этого мы сравниваем результат работы калькулятора с заранее подобранным правильным ответом с помощью assertEquals.

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

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

Test-driven development

Test-driven development (TDD) — это подход к разработке программ, при котором разработчик сначала описывает тесты для функции, которую хочет создать, а затем пишет сам код, который проходит эти тесты.

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

Упрощённо TDD можно представить в виде нескольких шагов:

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

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

Создаём приложение по принципам test‑driven development

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

Работать будем в среде разработки IntelliJ IDEA с Maven на борту. Как создать и инициализировать проект в Maven и подключить JUnit, смотрите выше. А мы перейдём к коду.

Пишем тест

Создайте файл для тестов в папке test\java. У нас он будет называться NumberUtilTest.java.

Скриншот: Skillbox Media

Напишите тест для функции вычисления факториала, по аналогии с тестированием калькулятора:

import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;

public class NumberUtilTest {
    @Test
    public void testFactorial() {
        NumberUtil util = new NumberUtil();
        int result = util.factorial(5); // Определяем факториал числа 5
        assertEquals(120, result); // Сравниваем результат с правильным ответом
    }
}

Основываясь на тесте, пропишите в папке main/java класс с названием NumberUtil. Класс пока что оставьте пустым:

Скриншот: Skillbox Media

Запускаем тест и дополняем код

Запустим наш тест:

Скриншот: Skillbox Media

Тест не пройден. Пока так и должно быть — наш класс ничего не содержит:

Скриншот: Skillbox Media

Теперь реализуйте функцию factorial в классе NumberUtil, чтобы тест прошёл успешно:

public class NumberUtil {

    public int factorial(int n) {
        if (n < 0) {
            throw new IllegalArgumentException("Факториал не может быть рассчитан для отрицательных чисел");
        }
        if (n == 0 || n == 1) {
            return 1;
        }
        return n * factorial(n - 1);
    }
}

Запустите тест снова. Если вы всё сделали правильно, то он пройдёт успешно:

Скриншот: Skillbox Media

Поздравляем! Вы написали приложение, используя метод TDD для тестирования и разработки.

Что дальше?

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

Углубиться в тему можно с помощью книг:

Больше интересного про код — в нашем телеграм-канале. Подписывайтесь!

Проверьте свой английский. Бесплатно ➞
Нескучные задания: small talk, поиск выдуманных слов — и не только. Подробный фидбэк от преподавателя + персональный план по повышению уровня.
Пройти тест
Понравилась статья?
Да

Пользуясь нашим сайтом, вы соглашаетесь с тем, что мы используем cookies 🍪

Ссылка скопирована