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

Что такое юнит тестирование?

1.3 Junior🔥 221 комментариев
#Тестирование

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

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

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

Юнит тестирование в Java

Что такое юнит тест

Юнит тест (Unit Test) — это автоматизированный тест, который проверяет одну "единицу" кода в изоляции. Единица обычно — это один метод или одна функция. Цель: убедиться, что код работает корректно на входах и ожидает корректные выходы.

Зачем нужны юнит тесты

  1. Рано ловят баги — перед production
  2. Документация — тесты показывают, как использовать код
  3. Рефакторинг с уверенностью — переписываешь код, не боясь сломать
  4. Design validation — если метод сложно тестировать, его дизайн плохой
  5. Regression prevention — старые баги не вернутся

Популярные фреймворки

  • JUnit — стандарт (JUnit 4, JUnit 5)
  • Mockito — мокирование зависимостей
  • AssertJ — удобные assertions
  • Hamcrest — матчеры

Установка

<!-- JUnit 5 (современный) -->
<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter</artifactId>
    <version>5.9.2</version>
    <scope>test</scope>
</dependency>

<!-- Mockito -->
<dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-core</artifactId>
    <version>5.2.1</version>
    <scope>test</scope>
</dependency>

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

Пример: код для тестирования

public class Calculator {
    
    public int add(int a, int b) {
        return a + b;
    }
    
    public int divide(int a, int b) {
        if (b == 0) {
            throw new IllegalArgumentException("Cannot divide by zero");
        }
        return a / b;
    }
    
    public boolean isEven(int number) {
        return number % 2 == 0;
    }
}

Основная структура теста

import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;

public class CalculatorTest {
    
    // Arrange — подготовка
    // Act — действие
    // Assert — проверка
    
    @Test
    public void testAddTwoPositiveNumbers() {
        // Arrange
        Calculator calculator = new Calculator();
        int a = 5;
        int b = 3;
        
        // Act
        int result = calculator.add(a, b);
        
        // Assert
        assertEquals(8, result);
    }
    
    @Test
    public void testAddNegativeNumbers() {
        Calculator calculator = new Calculator();
        assertEquals(-5, calculator.add(-3, -2));
    }
    
    @Test
    public void testAddZero() {
        Calculator calculator = new Calculator();
        assertEquals(5, calculator.add(5, 0));
    }
}

Жизненный цикл теста

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.AfterAll;

public class TestLifecycle {
    
    // Статический — один раз для ВСЕХ тестов класса
    @BeforeAll
    static void setupAll() {
        System.out.println("Один раз в начале");
    }
    
    // Перед КАЖДЫМ тестом
    @BeforeEach
    void setup() {
        System.out.println("Перед тестом");
    }
    
    @Test
    void test1() {
        System.out.println("Тест 1");
    }
    
    @Test
    void test2() {
        System.out.println("Тест 2");
    }
    
    // После КАЖДОГО теста
    @AfterEach
    void teardown() {
        System.out.println("После теста");
    }
    
    // Статический — один раз для ВСЕХ тестов класса
    @AfterAll
    static void teardownAll() {
        System.out.println("Один раз в конце");
    }
}

/* Вывод:
Один раз в начале
Перед тестом
Тест 1
После теста
Перед тестом
Тест 2
После теста
Один раз в конце
*/

Assertions

@Test
public void testAssertions() {
    // Равенство
    assertEquals(4, 2 + 2);
    assertNotEquals(4, 2 + 1);
    
    // True/False
    assertTrue(5 > 3);
    assertFalse(5 < 3);
    
    // null
    String str = null;
    assertNull(str);
    assertNotNull("hello");
    
    // То же самое
    Object obj1 = new Object();
    Object obj2 = obj1;
    assertSame(obj1, obj2)  // Одно и то же объект
    
    // Исключения
    assertThrows(IllegalArgumentException.class, () -> {
        throw new IllegalArgumentException("Error");
    });
    
    // Содержит
    String message = "Hello World";
    assertTrue(message.contains("Hello"));
}

Более удобные assertions с AssertJ

import static org.assertj.core.api.Assertions.*;

@Test
public void testWithAssertJ() {
    String text = "Hello World";
    
    // Цепочка assertions (более читаемо)
    assertThat(text)
        .isNotEmpty()
        .hasLength(11)
        .contains("Hello")
        .startsWith("Hello")
        .endsWith("World");
    
    List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
    assertThat(numbers)
        .isNotEmpty()
        .hasSize(5)
        .contains(1, 3, 5)
        .doesNotContain(0);
    
    User user = new User("John", 30);
    assertThat(user)
        .hasFieldOrPropertyWithValue("name", "John")
        .hasFieldOrPropertyWithValue("age", 30);
}

Мокирование с Mockito

import org.mockito.Mock;
import org.mockito.InjectMocks;
import static org.mockito.Mockito.*;

public class UserServiceTest {
    
    @Mock
    private UserRepository userRepository;  // Mock зависимость
    
    @InjectMocks
    private UserService userService;  // Инжектить mock'и
    
    @BeforeEach
    public void setup() {
        MockitoAnnotations.openMocks(this);
    }
    
    @Test
    public void testGetUserById() {
        // Arrange: настроить мок
        User mockUser = new User(1, "John", "john@example.com");
        when(userRepository.findById(1))
            .thenReturn(Optional.of(mockUser));
        
        // Act
        User result = userService.getUserById(1);
        
        // Assert
        assertNotNull(result);
        assertEquals("John", result.getName());
        
        // Проверить, что метод был вызван
        verify(userRepository).findById(1);
    }
    
    @Test
    public void testGetUserNotFound() {
        // Arrange
        when(userRepository.findById(999))
            .thenReturn(Optional.empty());
        
        // Act & Assert
        assertThrows(UserNotFoundException.class, () -> {
            userService.getUserById(999);
        });
    }
}

Параметризованные тесты

import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import org.junit.jupiter.params.provider.CsvSource;

public class ParameterizedTests {
    
    // Один параметр
    @ParameterizedTest
    @ValueSource(ints = {1, 3, 5, 7, 9})
    public void testOddNumbers(int number) {
        assertTrue(number % 2 != 0);
    }
    
    // Несколько параметров (CSV)
    @ParameterizedTest
    @CsvSource({
        "2, 2, 4",
        "5, 3, 8",
        "-1, 1, 0"
    })
    public void testAdd(int a, int b, int expected) {
        Calculator calc = new Calculator();
        assertEquals(expected, calc.add(a, b));
    }
}

Сценарий: Service с зависимостями

// Класс для тестирования
public class OrderService {
    private final OrderRepository orderRepository;
    private final PaymentService paymentService;
    private final EmailService emailService;
    
    public OrderService(OrderRepository orderRepository,
                        PaymentService paymentService,
                        EmailService emailService) {
        this.orderRepository = orderRepository;
        this.paymentService = paymentService;
        this.emailService = emailService;
    }
    
    public Order createOrder(Order order) {
        // Валидация
        if (order.getItems().isEmpty()) {
            throw new IllegalArgumentException("Order must have items");
        }
        
        // Сохранение
        Order savedOrder = orderRepository.save(order);
        
        // Обработка платежа
        paymentService.process(savedOrder);
        
        // Отправка email
        emailService.sendConfirmation(savedOrder);
        
        return savedOrder;
    }
}

// Юнит тест
@ExtendWith(MockitoExtension.class)  // JUnit 5 + Mockito
public class OrderServiceTest {
    
    @Mock
    private OrderRepository orderRepository;
    
    @Mock
    private PaymentService paymentService;
    
    @Mock
    private EmailService emailService;
    
    @InjectMocks
    private OrderService orderService;
    
    @Test
    public void testCreateOrderSuccess() {
        // Arrange
        Order order = new Order();
        order.addItem(new Item("Book", 100));
        order.addItem(new Item("Pen", 50));
        
        Order savedOrder = new Order(1, order.getItems(), OrderStatus.PENDING);
        
        when(orderRepository.save(order)).thenReturn(savedOrder);
        
        // Act
        Order result = orderService.createOrder(order);
        
        // Assert
        assertEquals(1, result.getId());
        assertEquals(OrderStatus.PENDING, result.getStatus());
        
        // Проверить вызовы
        verify(orderRepository).save(order);
        verify(paymentService).process(savedOrder);
        verify(emailService).sendConfirmation(savedOrder);
    }
    
    @Test
    public void testCreateOrderWithEmptyItems() {
        // Arrange
        Order order = new Order();  // Нет items
        
        // Act & Assert
        assertThrows(IllegalArgumentException.class, () -> {
            orderService.createOrder(order);
        });
        
        // Verify что никто не был вызван
        verify(orderRepository, never()).save(any());
        verify(paymentService, never()).process(any());
    }
}

Best Practices

  1. Одна assertion за тест (или несколько связанных):
// ❌ Плохо: множество независимых проверок
@Test
public void testEverything() {
    assertEquals(4, 2 + 2);
    assertTrue(5 > 3);
    assertNull(null);
}

// ✅ Хорошо: одна ответственность
@Test
public void testAddition() { assertEquals(4, 2 + 2); }

@Test
public void testComparison() { assertTrue(5 > 3); }
  1. Избегай зависимости между тестами:
// ❌ Плохо: test2 зависит от test1
int sharedValue;

@Test
void test1() { sharedValue = 5; }

@Test
void test2() { assertEquals(5, sharedValue); }

// ✅ Хорошо: каждый тест независим
@Test
void test1() { assertEquals(5, getValue()); }

@Test
void test2() { assertEquals(5, getValue()); }
  1. Понятные имена тестов:
// ❌ Плохо
@Test
void test1() { }

// ✅ Хорошо
@Test
void shouldThrowExceptionWhenDividingByZero() { }

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

# Maven
mvn test
mvn test -Dtest=CalculatorTest  # Конкретный класс
mvn test -Dtest=CalculatorTest#testAdd  # Конкретный метод

# Gradle
gradle test
gradle test --tests CalculatorTest

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

<plugin>
    <groupId>org.jacoco</groupId>
    <artifactId>jacoco-maven-plugin</artifactId>
    <version>0.8.8</version>
</plugin>
mvn clean test jacoco:report
# Отчёт: target/site/jacoco/index.html

Вывод

Юнит тестирование — это критичный навык для Java разработчика. Хорошо протестированный код — это уверенность в production, быстрая разработка и меньше ночных вызовов.