Тестирование с 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 или в целых центах.