- Писать тестовый класс с
@TestиassertEquals,assertThrows,assertAll - Запускать один тест для многих наборов данных через
@ParameterizedTest - Подменять зависимость с помощью Mockito и проверять вызовы через
verify
Ты немного изменил метод, и программа по-прежнему запускается. Но не сломалось ли что-то в десяти других местах? Каждый раз проверять всё вручную невозможно. Поэтому профессиональные команды пишут автоматические тесты — небольшие методы, которые вызывают твой код и сравнивают результат с ожидаемым. Сотни тестов выполняются за несколько секунд и ловят ошибку раньше пользователя.
Что такое тест?
Модульный тест (unit test) проверяет один небольшой фрагмент кода — обычно один метод — отдельно от остальных. Идея проста: ты знаешь ожидаемый результат, вызываешь метод и сравниваешь одно с другим. Ниже — крошечный тестовый «фреймворк» без всяких библиотек. В третьей проверке ожидание нарочно неверное, чтобы ты увидел, как выглядит упавший тест.
import java.util.List;
public class Main {
static int passed = 0, failed = 0;
static void check(String name, Object expected, Object actual) {
if (expected.equals(actual)) {
passed++;
System.out.println("PASS " + name);
} else {
failed++;
System.out.println("FAIL " + name + ": expected " + expected + " but was " + actual);
}
}
public static void main(String[] args) {
check("average of 85, 90, 95", 90.0, Grades.average(List.of(85, 90, 95)));
check("95 is an A", "A", Grades.letter(95));
check("79 is a B", "B", Grades.letter(79));
System.out.println(passed + " passed, " + failed + " failed");
}
}
class Grades {
static double average(List<Integer> scores) {
if (scores.isEmpty()) throw new IllegalArgumentException("No scores");
return scores.stream().mapToInt(Integer::intValue).average().getAsDouble();
}
static String letter(int score) {
if (score < 0 || score > 100) throw new IllegalArgumentException("Score out of range: " + score);
return score >= 90 ? "A" : score >= 80 ? "B" : score >= 70 ? "C" : score >= 50 ? "D" : "F";
}
}PASS average of 85, 90, 95 PASS 95 is an A FAIL 79 is a B: expected B but was C 2 passed, 1 failed
JUnit делает то же самое на профессиональном уровне: сам находит тестовые методы, запускает каждый отдельно, собирает ошибки и выдаёт аккуратный отчёт в IDE, Maven или Gradle. Достаточно зависимости org.junit.jupiter:junit-jupiter с областью test из прошлого урока. Основная часть JUnit 5 называется Jupiter, поэтому его пакеты начинаются с org.junit.jupiter.
Первые тесты на JUnit 5
import static org.junit.jupiter.api.Assertions.*;
import java.util.List;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
class GradesTest {
@Test
void averageOfThreeScores() {
List<Integer> scores = List.of(85, 90, 95); // Arrange
double result = Grades.average(scores); // Act
assertEquals(90.0, result, 0.001); // Assert
}
@Test
@DisplayName("An empty list is rejected")
void emptyListThrows() {
var ex = assertThrows(IllegalArgumentException.class, () -> Grades.average(List.of()));
assertEquals("No scores", ex.getMessage());
}
@Test
void lettersAtBoundaries() {
assertAll(
() -> assertEquals("A", Grades.letter(90)),
() -> assertEquals("B", Grades.letter(80)),
() -> assertEquals("F", Grades.letter(49)));
}
}src/test/java/GradesTest.java — класс Grades тот же, что вышеНад каждым тестовым методом стоит аннотация @Test; метод может не быть public и ничего не возвращает. Первый тест следует схеме AAA: Arrange (подготовь данные), Act (вызови метод), Assert (проверь результат). assertEquals(expected, actual, delta) сравнивает дробные числа с точностью 0.001, ведь в Java 0.1 + 0.2 — это не ровно 0.3, а 0.30000000000000004. assertThrows проверяет, что выброшено исключение, и возвращает его, чтобы можно было проверить и сообщение. assertAll выполняет все проверки и не останавливается на первой ошибке.
| Аннотация | Что делает |
|---|---|
@Test | помечает метод как тест |
@DisplayName("...") | задаёт понятное имя в отчёте |
@BeforeEach / @AfterEach | выполняется перед / после каждого теста |
@BeforeAll / @AfterAll | выполняется один раз на весь класс (static-метод) |
@Disabled | временно отключает тест |
@ParameterizedTest | запускает тест для каждой строки данных |
Для каждого теста JUnit создаёт новый объект, поэтому тесты не влияют друг на друга и могут выполняться в любом порядке. Тесты запускают зелёной кнопкой ▶ в IDE или командой mvn test / ./gradlew test в терминале. Представь, что кто-то случайно заменил в методе letter условие score >= 80 на score > 80:
mvn test[INFO] Running GradesTest [ERROR] Tests run: 3, Failures: 1, Errors: 0, Skipped: 0 [ERROR] Failures: [ERROR] GradesTest.lettersAtBoundaries:25 Multiple Failures (1 failure) org.opentest4j.AssertionFailedError: expected: <B> but was: <C> [INFO] BUILD FAILURE
import org.junit.Test; // JUnit 4
import static org.junit.Assert.assertEquals;
public class GradesTest {
@Test(expected = IllegalArgumentException.class)
public void emptyListThrows() {
Grades.average(List.of());
}
}import org.junit.jupiter.api.Test; // JUnit 5
import static org.junit.jupiter.api.Assertions.*;
class GradesTest {
@Test
void emptyListThrows() {
var ex = assertThrows(IllegalArgumentException.class, () -> Grades.average(List.of()));
assertEquals("No scores", ex.getMessage());
}
}public, а у исключения можно проверить и сообщениеПараметризованные тесты
Чтобы повторить одну и ту же проверку с разными данными, не нужно копировать тест. @ParameterizedTest запускает тест отдельно для каждого набора данных: @CsvSource передаёт каждую строку со значениями через запятую в параметры метода, а @ValueSource подходит для простого списка значений. В отчёте каждый случай виден отдельно, например 85 -> B. Эти аннотации находятся в модуле junit-jupiter-params, который уже входит в зависимость junit-jupiter.
@Test void letterFor95() { assertEquals("A", Grades.letter(95)); }
@Test void letterFor85() { assertEquals("B", Grades.letter(85)); }
@Test void letterFor72() { assertEquals("C", Grades.letter(72)); }
@Test void letterFor49() { assertEquals("F", Grades.letter(49)); }
// one more case = one more copy-pasted method...@ParameterizedTest(name = "{0} -> {1}")
@CsvSource({ "95, A", "85, B", "72, C", "49, F" })
void letterForScore(int score, String expected) {
assertEquals(expected, Grades.letter(score));
}
@ParameterizedTest
@ValueSource(ints = { -1, 101, 150 })
void outOfRangeScoresAreRejected(int score) {
assertThrows(IllegalArgumentException.class, () -> Grades.letter(score));
}Mockito: подмена зависимостей
Настоящий класс часто зависит от других объектов: базы данных, почтового сервера, внешнего API. Обращаться к ним по-настоящему в модульном тесте медленно и ненадёжно. Решение — передавать зависимость через конструктор и в тесте заменять её моком (поддельным объектом). ReportService ниже получает баллы из интерфейса ScoreRepository и не знает, читает ли тот базу данных или это подделка.
interface ScoreRepository {
List<Integer> findScores(String student); // e.g. reads a database
}
class ReportService {
private final ScoreRepository repository;
ReportService(ScoreRepository repository) {
this.repository = repository;
}
String report(String student) {
return student + ": " + Grades.average(repository.findScores(student));
}
}import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.*;
import java.util.List;
import org.junit.jupiter.api.Test;
class ReportServiceTest {
@Test
void reportUsesScoresFromRepository() {
ScoreRepository repo = mock(ScoreRepository.class);
when(repo.findScores("Aysel")).thenReturn(List.of(90, 80));
ReportService service = new ReportService(repo);
String report = service.report("Aysel");
assertEquals("Aysel: 85.0", report);
verify(repo).findScores("Aysel");
verifyNoMoreInteractions(repo);
}
}org.mockito:mockito-junit-jupiter (область test)mock(...) создаёт пустую поддельную копию интерфейса. when(...).thenReturn(...) учит её: «если спросят про Aysel, верни 90 и 80» — это называется заглушкой (stub). verify проверяет, что сервис вызвал репозиторий ровно один раз и с правильным аргументом, а verifyNoMoreInteractions подтверждает, что лишних вызовов не было. В больших тестовых классах то же самое делают аннотациями: над классом пишут @ExtendWith(MockitoExtension.class), над полем — @Mock.
Главное
- Модульный тест проверяет небольшой фрагмент кода отдельно: сравнивает ожидаемый результат с фактическим.
- JUnit 5:
@Test,assertEquals(expected, actual),assertThrows,assertAll; тесты лежат вsrc/test/java. - Схема AAA: Arrange → Act → Assert; каждый тест работает на новом объекте и не зависит от других.
@ParameterizedTest+@CsvSource/@ValueSourceзапускает один тест со многими наборами данных.- Mockito подменяет зависимости:
mock,when(...).thenReturn(...),verify; передача зависимости через конструктор упрощает тестирование.
Проверь себя
Вопросов: 10. Каждый правильный ответ приносит XP.
assertEquals(0.3, 0.1 + 0.2) падает?