Что такое модульное тестирование?
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Модульное тестирование
Модульное тестирование (Unit Testing) - это практика написания автоматизированных тестов для отдельных единиц кода (функций, методов, классов) с целью проверки их корректного поведения в изоляции от других компонентов. Это самый базовый уровень тестирования в пирамиде тестов.
Основные концепции
Единица кода (Unit) - это обычно один метод или небольшой набор связанных методов одного класса.
Изоляция - тест должен проверять только одну функцию, не зависи от БД, API или других сервисов.
Автоматизация - тесты выполняются автоматически и быстро (миллисекунды, не секунды).
Повторяемость - тест должен давать одинаковый результат каждый раз.
Структура Unit теста (AAA Pattern)
Все unit тесты следуют паттерну: Arrange → Act → Assert
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
public class CalculatorTest {
@Test
public void testAddNumbers() {
// Arrange (подготовка)
Calculator calculator = new Calculator();
int a = 5;
int b = 3;
// Act (действие)
int result = calculator.add(a, b);
// Assert (проверка)
assertEquals(8, result);
}
}
Простой пример Unit теста в Java
Тестируемый класс
public class UserValidator {
public boolean isValidEmail(String email) {
if (email == null || email.isEmpty()) {
return false;
}
return email.matches("^[A-Za-z0-9+_.-]+@(.+)$");
}
public boolean isStrongPassword(String password) {
if (password == null || password.length() < 8) {
return false;
}
boolean hasUpperCase = password.matches(".*[A-Z].*");
boolean hasLowerCase = password.matches(".*[a-z].*");
boolean hasDigit = password.matches(".*\\d.*");
return hasUpperCase && hasLowerCase && hasDigit;
}
}
Unit тесты для этого класса
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.BeforeEach;
import static org.junit.jupiter.api.Assertions.*;
public class UserValidatorTest {
private UserValidator validator;
@BeforeEach // Выполняется перед каждым тестом
public void setUp() {
validator = new UserValidator();
}
// ===== Email validation tests =====
@Test
public void testValidEmail() {
assertTrue(validator.isValidEmail("john@example.com"));
}
@Test
public void testInvalidEmailWithoutDomain() {
assertFalse(validator.isValidEmail("john@"));
}
@Test
public void testInvalidEmailWithoutAt() {
assertFalse(validator.isValidEmail("johnexample.com"));
}
@Test
public void testNullEmail() {
assertFalse(validator.isValidEmail(null));
}
@Test
public void testEmptyEmail() {
assertFalse(validator.isValidEmail(""));
}
// ===== Password validation tests =====
@Test
public void testStrongPassword() {
assertTrue(validator.isStrongPassword("MyPassword123"));
}
@Test
public void testPasswordTooShort() {
assertFalse(validator.isStrongPassword("Pass1a"));
}
@Test
public void testPasswordWithoutUpperCase() {
assertFalse(validator.isStrongPassword("password123"));
}
@Test
public void testPasswordWithoutLowerCase() {
assertFalse(validator.isStrongPassword("PASSWORD123"));
}
@Test
public void testPasswordWithoutDigit() {
assertFalse(validator.isStrongPassword("PasswordOnly"));
}
@Test
public void testNullPassword() {
assertFalse(validator.isStrongPassword(null));
}
}
Использование Mocks для изоляции
Одна из главных идей unit тестов - изолировать единицу кода. Для этого используются mock объекты:
import org.mockito.Mock;
import org.mockito.InjectMocks;
import static org.mockito.Mockito.*;
import org.junit.jupiter.api.Test;
public class OrderServiceTest {
@Mock
private PaymentGateway paymentGateway; // Mock объект
@Mock
private EmailService emailService; // Mock объект
@InjectMocks
private OrderService orderService; // Реальный объект с injected mocks
@Test
public void testOrderCreation() {
// Arrange: конфигурируем mock поведение
when(paymentGateway.processPayment(100.0))
.thenReturn(true);
// Act: вызываем реальный метод
Order order = orderService.createOrder(100.0, "product1");
// Assert: проверяем результат
assertNotNull(order);
assertEquals("product1", order.getProductId());
// Verify: проверяем, что mock был вызван правильно
verify(paymentGateway).processPayment(100.0);
verify(emailService).sendConfirmation("customer@example.com");
}
}
Пример реального сервиса с Unit тестами
// Сервис
public class BankAccountService {
private BankAccountRepository repository;
private TransactionLogger logger;
public BankAccountService(
BankAccountRepository repository,
TransactionLogger logger
) {
this.repository = repository;
this.logger = logger;
}
public void transferMoney(String from, String to, BigDecimal amount)
throws InsufficientFundsException {
if (amount.compareTo(BigDecimal.ZERO) <= 0) {
throw new IllegalArgumentException("Amount must be positive");
}
BankAccount fromAccount = repository.findById(from);
BankAccount toAccount = repository.findById(to);
if (fromAccount.getBalance().compareTo(amount) < 0) {
throw new InsufficientFundsException(
"Insufficient funds in account " + from
);
}
fromAccount.withdraw(amount);
toAccount.deposit(amount);
repository.save(fromAccount);
repository.save(toAccount);
logger.log("Transfer of " + amount + " from " + from + " to " + to);
}
}
// Unit тесты
import org.mockito.Mock;
import org.mockito.InjectMocks;
import static org.mockito.Mockito.*;
public class BankAccountServiceTest {
@Mock
private BankAccountRepository repository;
@Mock
private TransactionLogger logger;
@InjectMocks
private BankAccountService service;
@Test
public void testSuccessfulTransfer() throws Exception {
// Arrange
BankAccount from = new BankAccount("100", BigDecimal.valueOf(500));
BankAccount to = new BankAccount("200", BigDecimal.valueOf(1000));
when(repository.findById("100")).thenReturn(from);
when(repository.findById("200")).thenReturn(to);
// Act
service.transferMoney("100", "200", BigDecimal.valueOf(100));
// Assert
assertEquals(BigDecimal.valueOf(400), from.getBalance());
assertEquals(BigDecimal.valueOf(1100), to.getBalance());
// Verify mocks were called
verify(repository, times(2)).save(any(BankAccount.class));
verify(logger).log(contains("Transfer of 100"));
}
@Test
public void testInsufficientFundsException() {
// Arrange
BankAccount from = new BankAccount("100", BigDecimal.valueOf(50));
when(repository.findById("100")).thenReturn(from);
// Act & Assert
assertThrows(
InsufficientFundsException.class,
() -> service.transferMoney("100", "200", BigDecimal.valueOf(100))
);
// Verify that save was never called
verify(repository, never()).save(any());
}
@Test
public void testNegativeAmountException() {
// Act & Assert
assertThrows(
IllegalArgumentException.class,
() -> service.transferMoney("100", "200", BigDecimal.valueOf(-10))
);
}
}
Параметризованные тесты
Когда нужно проверить один метод с разными входными данными:
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import org.junit.jupiter.params.provider.CsvSource;
public class DiscountCalculatorTest {
@ParameterizedTest
@ValueSource(doubles = {10.0, 50.0, 100.0, 200.0})
public void testDiscountPercentage(double price) {
DiscountCalculator calculator = new DiscountCalculator();
double discount = calculator.calculateDiscount(price);
assertTrue(discount >= 0 && discount <= price);
}
@ParameterizedTest
@CsvSource({
"100, 10, 90", // price=100, discount=10, expected=90
"50, 5, 45", // price=50, discount=5, expected=45
"200, 20, 180" // price=200, discount=20, expected=180
})
public void testApplyDiscount(double price, double discount, double expected) {
DiscountCalculator calculator = new DiscountCalculator();
double result = calculator.applyDiscount(price, discount);
assertEquals(expected, result, 0.01);
}
}
Покрытие кода (Code Coverage)
Одна из метрик качества unit тестов:
# С JaCoCo в Maven
mvn clean test jacoco:report
# Отчёт появится в target/site/jacoco/index.html
Уровни покрытия:
- 50-60% - минимум
- 80-90% - хорошее покрытие
- 90%+ - отличное покрытие (для важного кода)
Best Practices Unit Testing
1. Тестируй behaviour, не реализацию
// ❌ Плохо - тестирует реализацию
@Test
public void testPrivateField() {
User user = new User();
// Пытаемся проверить приватное поле через reflection
}
// ✅ Хорошо - тестирует behavior
@Test
public void testUserEmailValidation() {
User user = new User("invalid-email");
assertFalse(user.isValid());
}
2. Один assert на тест (или один концепт)
// ❌ Плохо - слишком много в одном тесте
@Test
public void testUser() {
assertEquals("John", user.getName());
assertEquals(25, user.getAge());
assertEquals("john@example.com", user.getEmail());
}
// ✅ Хорошо - каждый тест проверяет одно
@Test
public void testUserName() {
assertEquals("John", user.getName());
}
3. Быстрые тесты
Unit тесты должны выполняться за миллисекунды, не секунды.
4. Независимые тесты
Тесты не должны зависеть друг от друга.
5. Хорошие имена
// ❌ Плохое имя
@Test
public void test1() {}
// ✅ Хорошее имя
@Test
public void testCalculateDiscountFor50PercentOff() {}
Инструменты для Unit Testing
- JUnit - фреймворк для тестирования
- Mockito - создание mock объектов
- AssertJ - удобные assertions
- JaCoCo - анализ покрытия кода
- TestNG - альтернатива JUnit
Пирамида тестов
/\
/ E2E\ (10%)
/------\
/Integration\ (20%)
/-----------\
/ Unit \ (70%)
/_______________\
Большинство тестов должны быть unit тесты (быстрые и дешёвые).
Модульное тестирование - это фундамент качества кода. Хорошо написанные unit тесты:
- Защищают от регрессий
- Служат документацией
- Упрощают рефакторинг
- Повышают уверенность в коде
- Экономят время на поиск багов