← Назад к вопросам
Решал ли проблемы с производительностью при тестировании
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();
}
}
Ключевые выводы по оптимизации тестов
- Минимизируйте I/O — используйте in-memory БД (H2, SQLite)
- Переиспользуйте данные — @BeforeAll вместо @BeforeEach
- Параллельное выполнение — включайте когда возможно
- Мокируйте дорогие операции — Real Objects для быстрых
- Профилируйте регулярно — используйте JMH для бенчмарков
- Избегайте static state — каждый тест должен быть независим
- Используйте @Transactional — для отката изменений БД
- Правильный threshold для timeout — не делайте слишком большим
Проблемы производительности при тестировании решаются комбинацией правильной архитектуры, выбора инструментов и постоянного мониторинга.