Encodera

Спасибо, что читаете!

Пожертвуйте сайту, мы стараемся ради вас.

Оглавление

Тестирование с JUnit

Модульный тест (unit test) — небольшая программа, которая проверяет, что один кусочек кода работает правильно: вызывает метод с известными данными и сравнивает результат с ожидаемым. Тесты запускают после каждого изменения, и если что-то сломалось, вы узнаете об этом сразу. Самая популярная библиотека для тестов на Java — JUnit; сейчас актуальна версия 6, а её API почти не отличается от JUnit 5.

Подключение JUnit

Тесты не запускают как одиночный файл: JUnit — внешняя библиотека, и обычно её подключают через систему сборки. В Maven зависимость добавляют в pom.xml:

<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter</artifactId>
    <version>6.1.3</version>
    <scope>test</scope>
</dependency>

В Gradle — в build.gradle.kts:

dependencies {
    testImplementation("org.junit.jupiter:junit-jupiter:6.1.3")
    testRuntimeOnly("org.junit.platform:junit-platform-launcher")
}
 
tasks.test {
    useJUnitPlatform()
}

Код программы лежит в папке src/main/java, а тесты — в src/test/java, в том же пакете.

Код для проверки

Будем тестировать простую корзину покупок:

// src/main/java/shop/Cart.java
package shop;
 
import java.util.ArrayList;
import java.util.List;
 
public class Cart {
    private final List<Double> prices = new ArrayList<>();
 
    public void add(double price) {
        if (price <= 0) {
            throw new IllegalArgumentException("Цена должна быть положительной: " + price);
        }
        prices.add(price);
    }
 
    public int size() {
        return prices.size();
    }
 
    public double total() {
        return prices.stream().mapToDouble(Double::doubleValue).sum();
    }
 
    public double totalWithDiscount(int percent) {
        return total() * (100 - percent) / 100;
    }
}

Первый тест

Тест — это класс с методами, помеченными аннотацией @Test. Внутри метод готовит данные, вызывает проверяемый код и сравнивает результат с ожидаемым через assertEquals(ожидаемое, фактическое):

// src/test/java/shop/CartTest.java
package shop;
 
import static org.junit.jupiter.api.Assertions.assertEquals;
 
import org.junit.jupiter.api.Test;
 
class CartTest {
    @Test
    void totalAddsPrices() {
        Cart cart = new Cart();      // подготовка
        cart.add(10.0);
        cart.add(5.5);
 
        double total = cart.total(); // действие
 
        assertEquals(15.5, total);   // проверка
    }
}

Если значения совпали, тест пройден. Если нет, assertEquals выбросит ошибку с понятным сообщением, и тест будет отмечен как упавший. Имя метода стоит делать говорящим — по нему видно, что именно сломалось.

Проверки и подготовка

Кроме assertEquals часто нужны assertTrue, assertFalse, assertNull и assertThrows — последний проверяет, что код выбросил нужное исключение, и возвращает его. Метод с аннотацией @BeforeEach выполняется перед каждым тестом, поэтому каждый тест получает свежую пустую корзину:

// src/test/java/shop/CartTest.java
package shop;
 
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
 
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
 
class CartTest {
    private Cart cart;
 
    @BeforeEach
    void setUp() {
        cart = new Cart();
    }
 
    @Test
    void newCartIsEmpty() {
        assertEquals(0, cart.size());
    }
 
    @Test
    void totalAddsPrices() {
        cart.add(10.0);
        cart.add(5.5);
        assertEquals(15.5, cart.total());
    }
 
    @Test
    void rejectsNegativePrice() {
        var e = assertThrows(IllegalArgumentException.class, () -> cart.add(-3));
        assertEquals("Цена должна быть положительной: -3.0", e.getMessage());
    }
 
    @ParameterizedTest
    @CsvSource({"0, 200.0", "10, 180.0", "50, 100.0"})
    void discount(int percent, double expected) {
        cart.add(200.0);
        assertEquals(expected, cart.totalWithDiscount(percent));
    }
}

Последний метод — параметризованный тест: @ParameterizedTest запускает его несколько раз, а @CsvSource поставляет данные — каждая строка становится одним запуском с параметрами percent и expected. Так одна проверка покрывает много случаев без копирования кода.

Запуск тестов

В проекте тесты запускают командой mvn test или gradle test, а в IntelliJ IDEA — зелёной кнопкой рядом с классом теста. Без системы сборки можно взять консольный запуск JUnit — файл junit-platform-console-standalone-6.1.3.jar из Maven Central:

javac -d out -cp junit-platform-console-standalone-6.1.3.jar src/main/java/shop/Cart.java src/test/java/shop/CartTest.java
java -jar junit-platform-console-standalone-6.1.3.jar execute --class-path out --select-class shop.CartTest

Результат (сокращённо, время выполнения у вас будет другим):

├─ JUnit Jupiter ✔
│  └─ CartTest ✔
│     ├─ rejectsNegativePrice() ✔
│     ├─ discount(int, double) ✔
│     │  ├─ [1] "0", "200.0" ✔
│     │  ├─ [2] "10", "180.0" ✔
│     │  └─ [3] "50", "100.0" ✔
│     ├─ newCartIsEmpty() ✔
│     └─ totalAddsPrices() ✔
 
Test run finished after 469 ms
[         6 tests successful      ]
[         0 tests failed          ]

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

Частая ошибка

Дробные числа double хранятся неточно, и прямое сравнение подводит. Добавим в CartTest такой тест:

// src/test/java/shop/CartTest.java
@Test
void smallPrices() {
    cart.add(0.1);
    cart.add(0.2);
    assertEquals(0.3, cart.total());
}

Результат:

├─ smallPrices() ✘ expected: <0.3> but was: <0.30000000000000004>

Код корзины верный, а тест падает: 0.1 + 0.2 в double даёт 0.30000000000000004. Для дробных чисел передавайте третьим аргументом допустимую погрешность: assertEquals(0.3, cart.total(), 0.0001). А деньги в настоящих программах лучше хранить в BigDecimal или в целых центах.

Спасибо, что читаете!

Пожертвуйте сайту, мы стараемся ради вас.