Тестирование в Java: Unit, Integration, E2E — Продвинутый уровень
В современной Java-разработке тестирование — это не просто формальность, а фундаментальная практика, обеспечивающая стабильность и предсказуемость кода. Базовые тесты, такие как простые @Test с Assert.assertEquals, давно уступили место сложным многослойным стратегиям. В этой статье мы разберем продвинутые техники для каждого уровня пирамиды тестирования: Unit, Integration и End-to-End (E2E). Вы узнаете, как писать поддерживаемые, быстрые и надежные тесты, которые превратят ваш код в хорошо отлаженный механизм.
1. Продвинутые Unit-тесты: Моки, Стабы и Параметризация
Unit-тесты проверяют изолированный кусок кода (обычно один класс или метод). На продвинутом уровне мы уходим от простых сценариев к сложным взаимодействиям и граничным случаям. Ключевой инструмент здесь — библиотека Mockito.
1.1. Использование ArgumentCaptor и сложных ответов (Answer)
Часто необходимо не просто проверить, что метод был вызван, но и какие аргументы ему были переданы. Для этого используется ArgumentCaptor.
// Пример: Проверка, что в сервис отправлено правильное сообщение
@ExtendWith(MockitoExtension.class)
public class NotificationServiceTest {
@Mock
private EmailSender emailSender;
@InjectMocks
private NotificationService notificationService;
@Test
void shouldSendEmailWithCorrectContent() {
// Arrange
User user = new User("test@example.com", "Test User");
// Act
notificationService.sendWelcomeEmail(user);
// Assert
ArgumentCaptor<EmailMessage> captor = ArgumentCaptor.forClass(EmailMessage.class);
verify(emailSender).send(captor.capture());
EmailMessage capturedMessage = captor.getValue();
assertEquals("test@example.com", capturedMessage.getTo());
assertTrue(capturedMessage.getBody().contains("Test User"));
}
}
Для динамического создания ответов на основе входных данных используйте интерфейс Answer:
@Test
void testDynamicResponse() {
when(dataRepository.findById(anyLong())).thenAnswer(invocation -> {
Long id = invocation.getArgument(0);
return new DataEntity(id, "Entity with ID: " + id);
});
assertEquals("Entity with ID: 5", dataService.getDataById(5L).getName());
}
1.2. Параметризованные тесты (JUnit 5)
Параметризация позволяет запускать один и тот же тест с разными наборами данных, избегая дублирования кода. JUnit 5 предоставляет мощные аннотации, такие как @CsvSource и @MethodSource.
@ParameterizedTest
@CsvSource({
"10, 20, 30",
"0, 0, 0",
"-5, 5, 0"
})
void testSum(int a, int b, int expectedSum) {
Calculator calculator = new Calculator();
assertEquals(expectedSum, calculator.add(a, b));
}
2. Интеграционное тестирование: Работа с базой данных и внешними сервисами
Интеграционные тесты проверяют взаимодействие между компонентами системы, например, с реальной базой данных или очередями. Главный вызов — управление состоянием окружения. Здесь на помощь приходит Testcontainers.
2.1. Testcontainers для изолированной БД
Testcontainers поднимает Docker-контейнеры с необходимым ПО (PostgreSQL, Redis, Kafka) прямо во время тестов, гарантируя чистоту и воспроизводимость.
@SpringBootTest
@Testcontainers
class UserRepositoryIntegrationTest {
@Container
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15")
.withDatabaseName("testdb")
.withUsername("test")
.withPassword("test");
@DynamicPropertySource
static void configureProperties(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", postgres::getJdbcUrl);
registry.add("spring.datasource.username", postgres::getUsername);
registry.add("spring.datasource.password", postgres::getPassword);
}
@Autowired
private UserRepository userRepository;
@Test
void shouldSaveAndFindUser() {
User user = new User("john.doe@example.com", "John Doe");
userRepository.save(user);
Optional<User> foundUser = userRepository.findByEmail("john.doe@example.com");
assertTrue(foundUser.isPresent());
assertEquals("John Doe", foundUser.get().getName());
}
}
2.2. Тестирование REST API с MockMvc и WebTestClient
Для тестирования контроллеров без поднятия полного сервера используйте MockMvc (Spring MVC) или WebTestClient (WebFlux).
@SpringBootTest
@AutoConfigureMockMvc
class UserControllerTest {
@Autowired
private MockMvc mockMvc;
@Test
void shouldReturnUserById() throws Exception {
mockMvc.perform(get("/api/users/1")
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(jsonPath("$.name").value("John Doe"));
}
}
3. End-to-End (E2E) тестирование: Сквозные сценарии
E2E-тесты проверяют систему целиком, имитируя действия реального пользователя через UI. Java-экосистема предлагает мощные инструменты, такие как Selenium WebDriver и Playwright.
3.1. Структура E2E теста с Selenium и Page Object Model
Паттерн Page Object Model (POM) — это золотой стандарт для E2E-тестов. Он абстрагирует детали HTML-страницы, делая тесты читаемыми и устойчивыми к изменениям верстки.
// Page Object для страницы логина
public class LoginPage {
private WebDriver driver;
@FindBy(id = "username")
private WebElement usernameInput;
@FindBy(id = "password")
private WebElement passwordInput;
@FindBy(id = "login-button")
private WebElement loginButton;
public LoginPage(WebDriver driver) {
this.driver = driver;
PageFactory.initElements(driver, this);
}
public void login(String username, String password) {
usernameInput.sendKeys(username);
passwordInput.sendKeys(password);
loginButton.click();
}
}
// Сам тест
public class LoginE2ETest {
private WebDriver driver;
private LoginPage loginPage;
@BeforeEach
void setUp() {
driver = new ChromeDriver();
driver.get("https://example.com/login");
loginPage = new LoginPage(driver);
}
@Test
void testSuccessfulLogin() {
loginPage.login("admin", "