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

Решал ли проблемы с производительностью при тестировании

2.4 Senior🔥 111 комментариев
#REST API и микросервисы#Тестирование

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

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

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

# Проблемы с производительностью при тестировании и их решение

Проблемы производительности при тестировании — частая задача разработчика. Это требует умения профилировать код, находить узкие места и применять оптимизации. Расскажу о реальных ситуациях и решениях.

1. Классические проблемы в Unit тестах

Проблема 1: Неэффективное создание тестовых данных

// ПЛОХО: создание 1000 объектов для каждого теста
public class UserServiceTest {
    
    @Test
    public void testFindUser() {
        List<User> users = new ArrayList<>();
        for (int i = 0; i < 1000; i++) {
            users.add(new User("User" + i, "email" + i + "@test.com"));
        }
        
        User result = userService.findByEmail("email500@test.com");
        assertEquals("User500", result.getName());
    }
    // Тест выполняется 500ms вместо 50ms
}

// ХОРОШО: подготовка данных один раз
public class UserServiceTest {
    private List<User> testUsers;
    
    @BeforeAll
    static void setupAllTests() {
        // Создаём данные один раз для всех тестов
        testUsers = generateTestUsers(1000);
    }
    
    @Test
    public void testFindUser() {
        // Используем готовые данные
        User result = userService.findByEmail("email500@test.com");
        assertEquals("User500", result.getName());
    }
    // Тест выполняется 50ms
}

Проблема 2: Медленные интеграционные тесты с БД

// ПЛОХО: для каждого теста создание БД с нуля
public class UserRepositoryTest {
    
    @BeforeEach
    public void setup() {
        // Это выполняется перед КАЖДЫМ тестом
        Database.dropAllTables();
        Database.createTables();
        Database.insertTestData();
    }
    
    @Test
    public void testFindById() { }
    
    @Test
    public void testFindByEmail() { }
    
    @Test
    public void testUpdate() { }
    // Три теста × 3 сек на setup = 9 секунд
}

// ХОРОШО: используем @DirtiesContext и транзакции
@SpringBootTest
@DataJpaTest
public class UserRepositoryTest {
    
    @BeforeAll
    static void setupAllTests() {
        Database.insertTestData();  // один раз
    }
    
    @Test
    @Transactional  // откатываем изменения после теста
    public void testFindById() { }
    
    @Test
    @Transactional
    public void testFindByEmail() { }
    
    @Test
    @Transactional
    public void testUpdate() { }
    // Три теста × 0.1 сек = 0.3 секунды
}

2. Проблемы при мокировании

Проблема 3: Дорогостоящее мокирование

// ПЛОХО: создание mock объектов для каждого теста
public class PaymentServiceTest {
    
    @Test
    public void testProcessPayment() {
        UserRepository userRepo = Mockito.mock(UserRepository.class);
        OrderRepository orderRepo = Mockito.mock(OrderRepository.class);
        NotificationService notifService = Mockito.mock(NotificationService.class);
        Logger logger = Mockito.mock(Logger.class);
        
        Mockito.when(userRepo.findById(1)).thenReturn(new User(1, "John"));
        Mockito.when(orderRepo.findById(1)).thenReturn(new Order(1, 100.0));
        // ... ещё много setup
        
        PaymentService service = new PaymentService(userRepo, orderRepo, notifService, logger);
        // наконец, тестируем
    }
}

// ХОРОШО: используем аннотации и setup один раз
@ExtendWith(MockitoExtension.class)
public class PaymentServiceTest {
    
    @Mock
    private UserRepository userRepo;
    
    @Mock
    private OrderRepository orderRepo;
    
    @Mock
    private NotificationService notifService;
    
    @InjectMocks
    private PaymentService paymentService;
    
    @BeforeAll
    static void setup() {
        Mockito.when(userRepo.findById(1)).thenReturn(new User(1, "John"));
        Mockito.when(orderRepo.findById(1)).thenReturn(new Order(1, 100.0));
    }
    
    @Test
    public void testProcessPayment() {
        // Моки уже подготовлены
        paymentService.processPayment(1, 1);
    }
}

3. Проблемы с параллельным выполнением тестов

// ПЛОХО: тесты конфликтуют при параллельном выполнении
public class StaticDataTest {
    private static boolean initialized = false;  // общее состояние
    
    @Test
    public void testA() {
        initialized = true;
        Thread.sleep(100);
        assert initialized;  // может быть false если testB запущен одновременно
    }
    
    @Test
    public void testB() {
        initialized = false;
    }
}

// ХОРОШО: каждый тест независим
public class StaticDataTest {
    @Test
    public void testA() {
        boolean initialized = true;
        assert initialized;
    }
    
    @Test
    public void testB() {
        boolean initialized = false;
        assert !initialized;
    }
}

// Конфигурация для параллельного запуска
// junit-platform.properties
junit.jupiter.execution.parallel.enabled=true
junit.jupiter.execution.parallel.mode.default=concurrent
junit.jupiter.execution.parallel.mode.classes.default=concurrent

4. Профилирование тестов

// Профилирование простых тестов
public class PerformanceTest {
    
    @Test
    public void testPerformance() {
        long startTime = System.nanoTime();
        
        // Выполняем операцию
        List<Integer> numbers = generateNumbers(1_000_000);
        int sum = numbers.stream().mapToInt(Integer::intValue).sum();
        
        long duration = (System.nanoTime() - startTime) / 1_000_000;  // миллисекунды
        System.out.println("Duration: " + duration + "ms");
        
        assertTrue(duration < 1000, "Operation took too long");
    }
}

// JMH бенчмарк для точных измерений
import org.openjdk.jmh.annotations.*;

@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
@State(Scope.Thread)
public class SumBenchmark {
    
    private List<Integer> numbers;
    
    @Setup
    public void setup() {
        numbers = generateNumbers(1_000_000);
    }
    
    @Benchmark
    public long sumWithStream() {
        return numbers.stream().mapToLong(Integer::longValue).sum();
    }
    
    @Benchmark
    public long sumWithLoop() {
        long sum = 0;
        for (int i : numbers) {
            sum += i;
        }
        return sum;
    }
}

// Запуск: java -jar jmh-runner.jar SumBenchmark

5. Решение: кэширование результатов между тестами

public class ExpensiveOperationTest {
    private static final Map<String, List<User>> CACHE = new HashMap<>();
    
    private List<User> loadTestUsers() {
        // Кэшируем результат
        return CACHE.computeIfAbsent("users", key -> {
            System.out.println("Loading users from expensive source...");
            return loadFromDatabase();
        });
    }
    
    @Test
    public void testA() {
        List<User> users = loadTestUsers();  // загружается
        assertEquals(1000, users.size());
    }
    
    @Test
    public void testB() {
        List<User> users = loadTestUsers();  // из кэша
        assertTrue(users.stream().anyMatch(u -> u.getName().equals("User500")));
    }
}

6. Асинхронные тесты

// ПЛОХО: ждём timeout по умолчанию
public class AsyncTest {
    
    @Test
    public void testAsyncOperation() throws InterruptedException {
        CompletableFuture<String> result = asyncService.fetchData();
        String value = result.get(5, TimeUnit.SECONDS);  // может ждать 5 сек
        assertEquals("data", value);
    }
}

// ХОРОШО: используем специальные методы для async тестов
public class AsyncTest {
    
    @Test
    void testAsyncOperation() {
        asyncService.fetchData()
            .thenAccept(result -> assertEquals("data", result))
            .exceptionally(ex -> {
                fail("Operation failed", ex);
                return null;
            });
    }
    
    @Test
    void testAsyncWithAwaitility() {
        Awaitility.await()
            .atMost(Duration.ofSeconds(5))
            .until(() -> asyncService.fetchData(), notNullValue());
    }
}

7. Оптимизация тестового окружения

// application-test.properties
spring.h2.console.enabled=false
spring.jpa.show-sql=false
spring.datasource.hikari.maximum-pool-size=5
junit.jupiter.execution.parallel.enabled=true
junit.jupiter.execution.parallel.mode.default=concurrent

// Конфигурация для тестов
@Configuration
public class TestConfig {
    
    @Bean(name = "testDataSource")
    public DataSource testDataSource() {
        // H2 in-memory БД намного быстрее
        return new EmbeddedDatabaseBuilder()
            .setType(H2)
            .addScript("schema.sql")
            .addScript("test-data.sql")
            .build();
    }
}

Ключевые выводы по оптимизации тестов

  1. Минимизируйте I/O — используйте in-memory БД (H2, SQLite)
  2. Переиспользуйте данные — @BeforeAll вместо @BeforeEach
  3. Параллельное выполнение — включайте когда возможно
  4. Мокируйте дорогие операции — Real Objects для быстрых
  5. Профилируйте регулярно — используйте JMH для бенчмарков
  6. Избегайте static state — каждый тест должен быть независим
  7. Используйте @Transactional — для отката изменений БД
  8. Правильный threshold для timeout — не делайте слишком большим

Проблемы производительности при тестировании решаются комбинацией правильной архитектуры, выбора инструментов и постоянного мониторинга.

Решал ли проблемы с производительностью при тестировании | PrepBro