На собесе по тестированию спрашивают не аннотации наизусть, а понимание: что такое юнит-тест, где мок, а где стаб, как тестировать код со временем и случайностью, почему тесты «мигают».
Пирамида тестов
- Юнит-тесты: один класс или функция, без сети и БД, миллисекунды. Их больше всего.
- Интеграционные: несколько компонентов вместе, часто с настоящей БД в Testcontainers.
- End-to-end: система целиком через API или UI. Мало, дорогие и медленные.
JUnit 5
class PriceCalculatorTest {
private PriceCalculator calc;
@BeforeEach
void setUp() { calc = new PriceCalculator(); }
@Test
void appliesDiscountForLargeOrders() {
// given
var order = new Order(BigDecimal.valueOf(10_000));
// when
var price = calc.total(order);
// then
assertEquals(new BigDecimal("9000.00"), price);
}
@Test
void rejectsNegativeAmount() {
var ex = assertThrows(IllegalArgumentException.class,
() -> calc.total(new Order(BigDecimal.valueOf(-1))));
assertEquals("amount < 0", ex.getMessage());
}
}
- Жизненный цикл:
@BeforeAll/@AfterAllодин раз на класс (статические методы по умолчанию),@BeforeEach/@AfterEachвокруг каждого теста. JUnit по умолчанию создаёт новый экземпляр класса на каждый тест, поэтому поля не делят состояние между тестами. @ParameterizedTestс@ValueSource,@CsvSource,@MethodSource: один тест на много входов.@Nestedгруппирует сценарии,@DisplayNameдаёт читаемые имена,@Disabledвыключает.assertAllпроверяет несколько условий и показывает все провалы сразу.assertThrowsвозвращает исключение, его можно проверить дальше.- Порядок тестов не гарантирован, и тесты не должны от него зависеть.
Многие команды используют AssertJ: assertThat(list).hasSize(3).contains("a") читается лучше.
Mockito
Зависимости класса подменяют, чтобы тестировать его изолированно.
@ExtendWith(MockitoExtension.class)
class OrderServiceTest {
@Mock PaymentClient payments;
@Mock OrderRepository repo;
@InjectMocks OrderService service;
@Test
void paysAndSavesOrder() {
when(payments.charge(any(), eq(500L))).thenReturn(new Receipt("r1"));
service.checkout(new Cart(500L));
verify(repo).save(argThat(o -> o.receiptId().equals("r1")));
verifyNoMoreInteractions(payments);
}
}
@Mockсоздаёт заглушку: методы возвращают значения по умолчанию (null, 0, пустые коллекции, пустойOptional).when(...).thenReturn(...)задаёт поведение (стаб),thenThrowдля ошибок.verify(mock).method(...)проверяет вызов (мок в узком смысле),times(2),never().ArgumentCaptorвытаскивает переданный аргумент для проверок.@Spyоборачивает настоящий объект: по умолчанию вызываются реальные методы. Для spy стаб пишут черезdoReturn(x).when(spy).method(), иначеwhen(spy.method())сначала вызовет реальный метод.- Матчеры: если один аргумент через
any(), остальные тоже должны быть матчерами (eq(500L)), иначеInvalidUseOfMatchersException. - Со строгим режимом
MockitoExtensionпадает на неиспользованных стабах (UnnecessaryStubbingException): это помогает держать тесты чистыми. finalклассы и статические методы мокаются (inline mock maker стал по умолчанию в Mockito 5, статика черезmockStatic), но часто это признак, что зависимость стоит вынести за интерфейс.
Хороший тест
- Проверяет поведение, а не реализацию: не нужно
verifyна каждый внутренний вызов. - Детерминирован: время через
Clock, случайность через сид или внедрённый генератор, никакогоThread.sleepдля ожидания асинхронщины (лучше Awaitility). - Независим от других тестов и порядка.
- Один сценарий на тест, понятное имя.
Что спросят на собесе
- Чем юнит-тест отличается от интеграционного? Что такое пирамида тестов?
- Мок, стаб, spy: в чём разница?
- Как протестировать метод, который использует
LocalDateTime.now()? - Как тестировать исключения и параметризованные случаи в JUnit 5?
- Почему тесты бывают нестабильными и как это лечить?