JavaRush /Курсы /Модуль 5. Spring /Лекция 263: Практика: написание Unit-тестов для сервисов

Лекция 263: Практика: написание Unit-тестов для сервисов

Модуль 5. Spring
27 уровень , 2 лекция
Открыта

В этой лекции мы сосредоточимся на практическом аспекте: написании unit-тестов для сервисов. Мы создадим тесты для ключевых компонентов бизнес-логики одного из микросервисов, используя уже знакомые инструменты.


Подготовка к написанию Unit-тестов

Unit-тесты — это база тестирования при разработке. Они проверяют небольшие участки кода, изолированные от внешних зависимостей. Например, вы хотите протестировать метод сервиса, который подсчитывает скидку, не привлекая базу данных, API другого сервиса или сторонние библиотеки.

Цели:

  1. Изолировать тестируемый код.
  2. Проверить корректность бизнес-логики.
  3. Убедиться, что граничные условия и исключительные ситуации обработаны правильно.

Окружение для тестирования

Для начала убедитесь, что ваш проект настроен для тестирования. Проверьте, что у вас есть следующие зависимости:

pom.xml для Maven:


<dependencies>
    <!-- JUnit 5 -->
    <dependency>
        <groupId>org.junit.jupiter</groupId>
        <artifactId>junit-jupiter</artifactId>
        <scope>test</scope>
    </dependency>

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

    <!-- Spring Boot Test -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
        <exclusions>
            <exclusion>
                <groupId>org.mockito</groupId>
                <artifactId>mockito-core</artifactId>
            </exclusion>
        </exclusions>
    </dependency>
</dependencies>

build.gradle для Gradle:


dependencies {
    // JUnit 5
    testImplementation 'org.junit.jupiter:junit-jupiter'

    // Mockito
    testImplementation 'org.mockito:mockito-core'

    // Spring Boot Test
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

Пример приложения: магазин скидок

Возьмём в качестве примера микросервис для обработки скидок. У нас есть сервис DiscountService, который вычисляет размер скидки на основе категории пользователя и количества купленного товара.

Вот сам сервис:


import org.springframework.stereotype.Service;

@Service
public class DiscountService {

    public double calculateDiscount(String userCategory, int itemCount) {
        if (itemCount <= 0) {
            throw new IllegalArgumentException("Item count must be greater than 0");
        }

        switch (userCategory.toLowerCase()) {
            case "vip":
                return itemCount * 0.2; // 20% скидка
            case "regular":
                return itemCount > 5 ? itemCount * 0.1 : 0.0; // 10% скидка, если покупок > 5
            default:
                return 0.0; // Без скидки
        }
    }
}

Шаг 1: Настройка тестового класса

Следуя соглашениям, создадим тестовый класс DiscountServiceTest. Убедитесь, что он лежит в директории src/test/java.


package com.example.service;

import org.junit.jupiter.api.Test;

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

/**
 * Тесты для DiscountService.
 */
public class DiscountServiceTest {

    private final DiscountService discountService = new DiscountService();

    @Test
    void testCalculateDiscountForVipUser() {
        double discount = discountService.calculateDiscount("VIP", 10);
        assertEquals(2.0, discount, "VIP user should get 20% discount");
    }

    @Test
    void testCalculateDiscountForRegularUser() {
        double discount = discountService.calculateDiscount("regular", 6);
        assertEquals(0.6, discount, "Regular user should get 10% discount for more than 5 items");
    }

    @Test
    void testCalculateDiscountForUnknownUser() {
        double discount = discountService.calculateDiscount("guest", 5);
        assertEquals(0.0, discount, "Guest users should not get a discount");
    }

    @Test
    void testCalculateDiscountWithZeroItems() {
        assertThrows(IllegalArgumentException.class, () ->
            discountService.calculateDiscount("VIP", 0),
            "Should throw exception when item count is zero or less"
        );
    }
}

Обсуждение тестов

  1. Позитивные сценарии: тесты проверяют корректный результат для "VIP" и "regular" пользователей.
  2. Негативные сценарии: тест с "guest" категорией и тест с нулевым количеством товаров.
  3. Исключения: проверка, что для недопустимых параметров выбрасывается исключение IllegalArgumentException.

Шаг 2: Добавление взаимодействия с Mock (Mockito)

Теперь предположим, что DiscountService зависит от другого сервиса, например, UserRepository, который возвращает информацию о пользователе. Мы можем использовать Mockito для создания мока.

Внедрим зависимость:


@Service
public class DiscountService {

    private final UserRepository userRepository;

    public DiscountService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    public double calculateDiscount(String userId, int itemCount) {
        if (itemCount <= 0) {
            throw new IllegalArgumentException("Item count must be greater than 0");
        }

        String userCategory = userRepository.getUserCategory(userId);
        switch (userCategory.toLowerCase()) {
            case "vip":
                return itemCount * 0.2;
            case "regular":
                return itemCount > 5 ? itemCount * 0.1 : 0.0;
            default:
                return 0.0;
        }
    }
}

Тестируем с Mockito:


import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;

import static org.mockito.Mockito.*;
import static org.junit.jupiter.api.Assertions.*;

public class DiscountServiceTest {

    private UserRepository userRepository;
    private DiscountService discountService;

    @BeforeEach
    void setUp() {
        userRepository = mock(UserRepository.class);
        discountService = new DiscountService(userRepository);
    }

    @Test
    void testCalculateDiscountWithMockedUser() {
        when(userRepository.getUserCategory("123")).thenReturn("VIP");

        double discount = discountService.calculateDiscount("123", 10);

        assertEquals(2.0, discount);
        verify(userRepository, times(1)).getUserCategory("123");
    }

    @Test
    void testCalculateDiscountForUnknownUser() {
        when(userRepository.getUserCategory("456")).thenReturn("guest");

        double discount = discountService.calculateDiscount("456", 5);

        assertEquals(0.0, discount);
        verify(userRepository, times(1)).getUserCategory(anyString());
    }
}

  1. Создание мока: мы создаём мока для UserRepository — поддельной реализации, которая не связана с реальной базой данных.
  2. Stub методов: используем метод when().thenReturn() для указания возвращаемого значения.
  3. Проверка вызовов: verify() проверяет, что метод был вызван на моке с указанными параметрами.

Основные ошибки и их устранение

  1. Неправильное мокирование: если вы забудете указать when().thenReturn(), мок будет возвращать null по умолчанию.
  2. Неиспользование verify: проверка вызовов помогает убедиться, что тестируемый метод работает с моком корректно.
  3. Необработанные исключения: если вы не проверяете исключения, тесты могут пропускать критические ошибки.

Практическое применение

Навыки написания unit-тестов полезны для любой backend-разработки. На собеседованиях часто спрашивают умение мокировать зависимости и писать тесты для сложной бизнес-логики. В реальных проектах это поможет ускорить разработку, так как ошибки будут выявляться на ранних этапах.

Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ