← Назад к вопросам

Как тестировал свои задачи

1.8 Middle🔥 231 комментариев
#Тестирование

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Как я тестировал свои задачи: подход опытного разработчика

Тестирование — это не завершающий этап разработки, а неотъемлемая часть процесса. Расскажу, как я структурирую свой процесс тестирования для разных типов задач.

Философия: Test-Driven Development (TDD)

Лучший подход, который я использую — это TDD:

  1. RED → Написать падающий тест
  2. GREEN → Написать код, чтобы тест прошёл
  3. REFACTOR → Улучшить код, сохраняя тесты зелёными

Это гарантирует, что код покрыт тестами с самого начала и соответствует требованиям.

Уровни тестирования

1. Unit-тесты (70% времени)

Это основной уровень тестирования — проверяю отдельные методы и функции изолированно:

// Класс для тестирования
public class UserService {
    private UserRepository repository;
    private EmailValidator validator;
    
    public User registerUser(String email, String name) {
        if (!validator.isValid(email)) {
            throw new InvalidEmailException("Email is not valid");
        }
        if (repository.existsByEmail(email)) {
            throw new EmailAlreadyExistsException("Email already registered");
        }
        return repository.save(new User(email, name));
    }
}

// Unit-тесты (без БД)
@ExtendWith(MockitoExtension.class)
class UserServiceUnitTest {
    
    @Mock
    private UserRepository repository;
    
    @Mock
    private EmailValidator validator;
    
    @InjectMocks
    private UserService userService;
    
    @Test
    void shouldThrowWhenEmailInvalid() {
        // Arrange
        when(validator.isValid("invalid")).thenReturn(false);
        
        // Act & Assert
        assertThrows(InvalidEmailException.class, () ->
            userService.registerUser("invalid", "John")
        );
        
        // Verify repository was NOT called
        verify(repository, never()).save(any());
    }
    
    @Test
    void shouldThrowWhenEmailExists() {
        // Arrange
        when(validator.isValid("john@example.com")).thenReturn(true);
        when(repository.existsByEmail("john@example.com")).thenReturn(true);
        
        // Act & Assert
        assertThrows(EmailAlreadyExistsException.class, () ->
            userService.registerUser("john@example.com", "John")
        );
    }
    
    @Test
    void shouldSaveUserWhenValid() {
        // Arrange
        when(validator.isValid("john@example.com")).thenReturn(true);
        when(repository.existsByEmail("john@example.com")).thenReturn(false);
        
        User savedUser = new User(1L, "john@example.com", "John");
        when(repository.save(any(User.class))).thenReturn(savedUser);
        
        // Act
        User result = userService.registerUser("john@example.com", "John");
        
        // Assert
        assertThat(result.getId()).isEqualTo(1L);
        assertThat(result.getEmail()).isEqualTo("john@example.com");
        
        verify(repository).save(any(User.class));
    }
}

2. Integration-тесты (20% времени)

Проверяю взаимодействие между компонентами, обычно с использованием реальной БД:

@SpringBootTest
class UserRepositoryIntegrationTest {
    
    @Autowired
    private UserRepository repository;
    
    @Autowired
    private TestEntityManager tem;
    
    @Test
    @Transactional
    void shouldSaveAndRetrieveUser() {
        // Arrange
        User user = new User("john@example.com", "John Doe");
        
        // Act
        User saved = repository.save(user);
        tem.flush();
        tem.clear();
        
        User retrieved = repository.findByEmail("john@example.com").orElseThrow();
        
        // Assert
        assertThat(retrieved.getId()).isEqualTo(saved.getId());
        assertThat(retrieved.getName()).isEqualTo("John Doe");
    }
    
    @Test
    @Transactional
    void shouldNotAllowDuplicateEmails() {
        // Arrange
        repository.save(new User("john@example.com", "John"));
        
        // Act & Assert
        User duplicate = new User("john@example.com", "Another John");
        assertThrows(DataIntegrityViolationException.class, () ->
            repository.save(duplicate)
        );
    }
}

3. End-to-End тесты (10% времени)

Проверяю полный цикл запроса через контроллер:

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class UserControllerE2ETest {
    
    @Autowired
    private TestRestTemplate restTemplate;
    
    @Autowired
    private UserRepository repository;
    
    @BeforeEach
    void cleanUp() {
        repository.deleteAll();
    }
    
    @Test
    void shouldRegisterUserSuccessfully() {
        // Arrange
        RegisterUserRequest request = new RegisterUserRequest(
            "john@example.com",
            "John Doe"
        );
        
        // Act
        ResponseEntity<UserResponse> response = restTemplate.postForEntity(
            "/api/v1/users/register",
            request,
            UserResponse.class
        );
        
        // Assert
        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CREATED);
        assertThat(response.getBody().getEmail()).isEqualTo("john@example.com");
        
        // Verify database
        User savedUser = repository.findByEmail("john@example.com").orElseThrow();
        assertThat(savedUser.getName()).isEqualTo("John Doe");
    }
    
    @Test
    void shouldReturn400WhenEmailExists() {
        // Arrange
        repository.save(new User("john@example.com", "John"));
        
        RegisterUserRequest request = new RegisterUserRequest(
            "john@example.com",
            "Another John"
        );
        
        // Act
        ResponseEntity<ErrorResponse> response = restTemplate.postForEntity(
            "/api/v1/users/register",
            request,
            ErrorResponse.class
        );
        
        // Assert
        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
    }
}

Мой процесс тестирования для новой задачи

Шаг 1: Понимание требований

Сначала я выясняю:

  • Какое поведение нужно реализовать?
  • Какие edge cases существуют?
  • Какие зависимости нужно мокировать?

Шаг 2: Написание тестов (RED)

@Test
void shouldCalculateDiscountFor100Items() {
    // Arrange
    OrderService service = new OrderService();
    Order order = new Order(100, BigDecimal.TEN);  // 100 items по 10 каждый
    
    // Act
    BigDecimal total = service.calculateTotal(order);
    
    // Assert
    // 10% скидка на 100+ предметов: 100 * 10 * 0.9 = 900
    assertThat(total).isEqualTo(BigDecimal.valueOf(900));
}

Шаг 3: Реализация кода (GREEN)

public class OrderService {
    public BigDecimal calculateTotal(Order order) {
        BigDecimal subtotal = BigDecimal.valueOf(order.getQuantity())
            .multiply(order.getPrice());
        
        // 10% скидка на 100+ предметов
        if (order.getQuantity() >= 100) {
            return subtotal.multiply(BigDecimal.valueOf(0.9));
        }
        
        return subtotal;
    }
}

Шаг 4: Дополнительные тесты (больше покрытия)

@Test
void shouldNotApplyDiscountFor99Items() {
    OrderService service = new OrderService();
    Order order = new Order(99, BigDecimal.TEN);
    BigDecimal total = service.calculateTotal(order);
    assertThat(total).isEqualTo(BigDecimal.valueOf(990));  // Без скидки
}

@Test
void shouldApplyDiscountFor1000Items() {
    OrderService service = new OrderService();
    Order order = new Order(1000, BigDecimal.TEN);
    BigDecimal total = service.calculateTotal(order);
    assertThat(total).isEqualTo(BigDecimal.valueOf(9000));  // 10% скидка
}

Параметризованные тесты (для множественных случаев)

class OrderServiceParameterizedTest {
    
    @ParameterizedTest
    @CsvSource({
        "50, 100, 500",       // 50 items * 10 = 500 (no discount)
        "99, 100, 990",       // 99 items * 10 = 990 (no discount)
        "100, 100, 900",      // 100 items * 10 * 0.9 = 900 (10% discount)
        "200, 100, 1800",     // 200 items * 10 * 0.9 = 1800 (10% discount)
    })
    void shouldCalculateTotalCorrectly(int quantity, BigDecimal price, 
                                      BigDecimal expected) {
        OrderService service = new OrderService();
        Order order = new Order(quantity, price);
        BigDecimal result = service.calculateTotal(order);
        assertThat(result).isEqualByComparingTo(expected);
    }
}

Тестирование исключений

@Test
void shouldThrowWhenQuantityNegative() {
    OrderService service = new OrderService();
    Order order = new Order(-10, BigDecimal.TEN);
    
    assertThrows(IllegalArgumentException.class, () ->
        service.calculateTotal(order),
        "Quantity cannot be negative"
    );
}

// Или с assertThatThrownBy (AssertJ)
@Test
void shouldThrowWhenPriceNull() {
    OrderService service = new OrderService();
    Order order = new Order(10, null);
    
    assertThatThrownBy(() -> service.calculateTotal(order))
        .isInstanceOf(NullPointerException.class)
        .hasMessage("Price cannot be null");
}

Тестирование асинхронного кода

@Test
void shouldProcessOrderAsync() throws InterruptedException, ExecutionException {
    OrderService service = new OrderService();
    Order order = new Order(100, BigDecimal.TEN);
    
    CompletableFuture<BigDecimal> result = service.calculateTotalAsync(order);
    
    // Ждём результат с timeout
    BigDecimal total = result.get(2, TimeUnit.SECONDS);
    assertThat(total).isEqualByComparingTo(BigDecimal.valueOf(900));
}

Тестирование многопоточности

@Test
void shouldBeThreadSafe() throws InterruptedException {
    OrderService service = new OrderService();
    List<Order> orders = new ArrayList<>();
    
    for (int i = 0; i < 100; i++) {
        orders.add(new Order(100, BigDecimal.TEN));
    }
    
    ExecutorService executor = Executors.newFixedThreadPool(10);
    List<Future<?>> futures = new ArrayList<>();
    
    for (Order order : orders) {
        futures.add(executor.submit(() -> {
            service.calculateTotal(order);
        }));
    }
    
    for (Future<?> future : futures) {
        future.get();  // Ждём завершения
    }
    
    executor.shutdown();
    // Если не было исключений, тест прошёл
}

Покрытие тестами (Coverage)

Анализирую покрытие кода:

# Запуск с отчётом о покрытии
mvn clean test jacoco:report

# Откроется target/site/jacoco/index.html
# Стремлюсь к >= 80% покрытию

Мои инструменты

<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter</artifactId>
    <version>5.9.2</version>
    <scope>test</scope>
</dependency>

<dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-core</artifactId>
    <version>5.2.0</version>
    <scope>test</scope>
</dependency>

<dependency>
    <groupId>org.assertj</groupId>
    <artifactId>assertj-core</artifactId>
    <version>3.24.1</version>
    <scope>test</scope>
</dependency>

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <version>3.1.0</version>
    <scope>test</scope>
</dependency>

Запуск тестов

# Все тесты
mvn test

# Конкретный класс
mvn test -Dtest=UserServiceTest

# Конкретный метод
mvn test -Dtest=UserServiceTest#shouldSaveUser

# С фильтром
mvn test -Dtest=*Service*

# В watch режиме
mvn test -f pom.xml -Dbasedir=. test

Шпаргалка: когда писать какие тесты

КомпонентUnitIntegrationE2E
Service (бизнес-логика)
Repository (JDBC)
Controller (API)
Utility функции
Фильтры, interceptors

Мой контрольный список перед commit'ом

  • ✅ Все юнит-тесты проходят
  • ✅ Не менее 80% покрытия кода
  • ✅ Интеграционные тесты для репозиториев
  • ✅ E2E тесты для критичного функционала
  • ✅ Нет падающих тестов
  • ✅ Тесты быстрые (< 10 сек)
  • ✅ Код готов для ревью

Тестирование — это не дополнительная работа, это инвестиция в качество и скорость разработки. Хорошо протестированный код проще менять и меньше багов.

Как тестировал свои задачи | PrepBro