Введение: Почему тестирование — фундамент качественного Java-кода
Каждый разработчик Java сталкивался с ситуацией, когда после исправления одного бага появлялось три новых. Чтобы разорвать этот порочный круг, существует тестирование. Это не просто галочка в чек-листе, а профессиональная дисциплина, которая спасает нервы, деньги и репутацию продукта.
В этой статье мы разберем три ключевых уровня тестирования: unit (модульное), integration (интеграционное) и e2e (end-to-end). Мы рассмотрим их на практических примерах Java, используя популярные фреймворки: JUnit 5, Mockito, Spring Boot Test и Selenium. Вы узнаете, как построить правильную пирамиду тестирования и не перегрузить проект бесполезными тестами.
1. Unit-тестирование: Проверка атомарных единиц кода
Unit-тесты (модульные тесты) — это самый нижний и самый быстрый уровень. Они проверяют работу одного «юнита» (обычно метода или класса) в изоляции от внешних зависимостей (БД, API, файловой системы).
1.1 Инструменты и фреймворки
- JUnit 5 — стандарт де-факто для написания тестов в Java.
- Mockito — библиотека для создания заглушек (mocks) и стабов (stubs).
- AssertJ — библиотека для fluent-проверок (утверждений).
1.2 Пример Unit-теста с JUnit 5 и Mockito
Представим, что у нас есть сервис OrderService, который зависит от репозитория PaymentRepository. Мы хотим протестировать логику сервиса, не вызывая реальную БД.
// Тестируемый класс
public class OrderService {
private final PaymentRepository paymentRepository;
public OrderService(PaymentRepository paymentRepository) {
this.paymentRepository = paymentRepository;
}
public String processOrder(Order order) {
if (order.getAmount() <= 0) {
throw new IllegalArgumentException("Сумма заказа должна быть положительной");
}
boolean success = paymentRepository.pay(order);
return success ? "Успешно оплачено" : "Ошибка оплаты";
}
}
// 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.*;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
class OrderServiceTest {
@Mock
private PaymentRepository paymentRepository;
@InjectMocks
private OrderService orderService;
@Test
void testProcessOrder_Success() {
// Arrange
Order order = new Order(100.0);
when(paymentRepository.pay(order)).thenReturn(true);
// Act
String result = orderService.processOrder(order);
// Assert
assertEquals("Успешно оплачено", result);
verify(paymentRepository, times(1)).pay(order);
}
@Test
void testProcessOrder_NegativeAmount() {
// Arrange
Order order = new Order(-10.0);
// Act & Assert
assertThrows(IllegalArgumentException.class, () -> {
orderService.processOrder(order);
});
}
}
Best Practices для Unit-тестов:
- Один тест — одна проверка (принцип FIRST: Fast, Isolated, Repeatable, Self-validating, Timely).
- Используйте понятные имена:
shouldReturnSuccess_whenPaymentIsApproved. - Не тестируйте private-методы — тестируйте публичный контракт.
- Покрывайте граничные случаи: null, пустые строки, отрицательные числа.
2. Интеграционное тестирование: Проверка взаимодействия компонентов
Интеграционные тесты проверяют, как разные модули системы работают вместе: с реальной базой данных, очередями сообщений или внешними REST-сервисами. Они медленнее unit-тестов, но дают больше уверенности.
2.1 Инструменты для интеграционного тестирования Java
- Spring Boot Test — аннотации
@SpringBootTest,@DataJpaTest. - Testcontainers — запуск Docker-контейнеров (PostgreSQL, Redis) в тестах.
- H2 (in-memory DB) — легковесная БД для быстрых тестов (не подходит для production-запросов).
2.2 Пример интеграционного теста с Spring Boot и Testcontainers
Проверим, что наш репозиторий корректно сохраняет и извлекает пользователя из реальной PostgreSQL.
// Сущность
@Entity
public class User {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String email;
// getters/setters...
}
// Интеграционный тест
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import static org.assertj.core.api.Assertions.assertThat;
@DataJpaTest
@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 shouldSaveAndFindUserByEmail() {
// Arrange
User user = new User();
user.setEmail("ivan@example.com");
// Act
User savedUser = userRepository.save(user);
User foundUser = userRepository.findByEmail("ivan@example.com").orElse(null);
// Assert
assertThat(foundUser).isNotNull();
assertThat(foundUser.getId()).isGreaterThan(0);
assertThat(foundUser.getEmail()).isEqualTo("ivan@example.com");
}
}
Совет: Используйте Testcontainers вместо H2 для production-совместимости. H2 часто ведет себя иначе, чем PostgreSQL/MySQL.