Тестирование в Java: от модульных до сквозных тестов
Качественное тестирование — основа надежного программного обеспечения. В мире Java существует устоявшаяся пирамида тестирования, которая включает три основных уровня: unit (модульные), integration (интеграционные) и e2e (сквозные) тесты. В этой статье мы разберем каждый из них, приведем практические примеры на Java и расскажем, как правильно выстроить стратегию тестирования в вашем проекте.
1. Unit-тестирование: основа пирамиды
Unit-тесты проверяют отдельные компоненты программы в изоляции — как правило, один класс или метод. Они выполняются быстро, не требуют внешних зависимостей (БД, API) и должны покрывать большинство бизнес-логики.
Инструменты для unit-тестов в Java
- JUnit 5 — стандартный фреймворк для написания тестов.
- Mockito — библиотека для создания мок-объектов.
- AssertJ — fluent-ассерты для читаемых проверок.
Пример unit-теста с JUnit 5 и Mockito
Допустим, у нас есть сервис OrderService, который зависит от PaymentGateway. Мы хотим протестировать логику расчета скидки, не вызывая реальный платежный шлюз.
// Класс для тестирования
public class OrderService {
private final PaymentGateway paymentGateway;
public OrderService(PaymentGateway paymentGateway) {
this.paymentGateway = paymentGateway;
}
public double calculateDiscount(double amount) {
if (amount > 1000) {
return amount * 0.1; // 10% скидка
}
return 0;
}
}
// Unit-тест
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import static org.junit.jupiter.api.Assertions.assertEquals;
@ExtendWith(MockitoExtension.class)
public class OrderServiceTest {
@Mock
private PaymentGateway paymentGateway;
@InjectMocks
private OrderService orderService;
@Test
public void testCalculateDiscount_AmountAbove1000_Returns10Percent() {
double result = orderService.calculateDiscount(1500);
assertEquals(150.0, result, 0.001);
}
@Test
public void testCalculateDiscount_AmountBelow1000_ReturnsZero() {
double result = orderService.calculateDiscount(500);
assertEquals(0.0, result);
}
}
Лучшие практики: используйте @BeforeEach для инициализации общих данных, избегайте тестирования приватных методов напрямую, следуйте правилу "одна проверка на один тест".
2. Интеграционное тестирование: проверка взаимодействия
Интеграционные тесты проверяют, как несколько модулей работают вместе — например, взаимодействие сервиса с базой данных, очередями сообщений или внешними API. В отличие от unit-тестов, здесь используются реальные или in-memory зависимости.
Инструменты для интеграционных тестов
- Spring Boot Test — для тестирования Spring-приложений.
- Testcontainers — для поднятия Docker-контейнеров (БД, Kafka).
- H2 Database — in-memory база для быстрых тестов.
Пример интеграционного теста с Spring Boot
Тестируем REST-контроллер с реальным подключением к БД (в тесте используем H2).
// Контроллер
@RestController
@RequestMapping("/api/users")
public class UserController {
private final UserRepository userRepository;
public UserController(UserRepository userRepository) {
this.userRepository = userRepository;
}
@GetMapping("/{id}")
public User getUser(@PathVariable Long id) {
return userRepository.findById(id)
.orElseThrow(() -> new RuntimeException("User not found"));
}
}
// Интеграционный тест
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.http.ResponseEntity;
import static org.assertj.core.api.Assertions.assertThat;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class UserControllerIntegrationTest {
@Autowired
private TestRestTemplate restTemplate;
@Test
public void testGetUser_ExistingUser_ReturnsUserData() {
// Предполагаем, что пользователь с ID=1 существует (через data.sql)
ResponseEntity<User> response = restTemplate.getForEntity("/api/users/1", User.class);
assertThat(response.getStatusCode().is2xxSuccessful()).isTrue();
assertThat(response.getBody().getName()).isEqualTo("John Doe");
}
}
Важно: используйте @Transactional для отката изменений после каждого теста, чтобы не загрязнять базу данных.
3. E2E-тестирование: проверка с точки зрения пользователя
Сквозные (end-to-end) тесты имитируют поведение реального пользователя: открывают браузер, заполняют формы, нажимают кнопки. Они самые медленные, но дают максимальную уверенность в работоспособности системы в целом.
Инструменты для E2E-тестов
- Selenium WebDriver — классический инструмент для автоматизации браузера.
- Playwright для Java — современная альтернатива с лучшей поддержкой.
- Cypress (чаще для JS, но можно использовать через API).
Пример E2E-теста с Selenium
Тестируем форму логина на веб-приложении.
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.chrome.ChromeOptions;
import static org.junit.jupiter.api.Assertions.assertEquals;
public class LoginE2ETest {
private WebDriver driver;
@BeforeEach
public void setUp() {
ChromeOptions options = new ChromeOptions();
options.addArguments("--headless"); // без GUI
driver = new ChromeDriver(options);
driver.get("https://example.com/login");
}
@Test
public void testLogin_ValidCredentials_RedirectsToDashboard() {
driver.findElement(By.id("username")).sendKeys("testuser");
driver.findElement(By.id("password")).sendKeys("password123");
driver.findElement(By.id("login-btn")).click();
String currentUrl = driver.getCurrentUrl();
assertEquals("https://example.com/dashboard", currentUrl);
}
@AfterEach
public void tearDown() {
if (driver != null) {
driver.quit();
}
}
}
Совет: