Разделы портала

Онлайн-тренинги

.
Мутационное тестирование: не только юнит-тесты
29.06.2026 00:00

Автор: Баз Дейкстра (Bas Dijkstra)
Оригинал статьи
Перевод: Ольга Алифанова

Я уже несколько раз писал о мутационном тестировании в этом блоге, и даже частенько провожу воркшоп по мутационному тестированию.

Одно из заблуждений, которое иногда всплывает — либо явно в вопросах, либо неявно, когда я слышу, как о мутационном тестировании говорит кто-то еще, — заключается в том, что мутационное тестирование работает только для юнит-тестов. Это не так. Мутационное тестирование можно использовать и для оценки качества других типов тестов.

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

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

Наше тестируемое приложение

Чтобы показать, что мутационное тестирование работает и для других типов тестов, я написал API на Java с использованием Spring Boot, включающий слои controller, service и repository, с подключением к in-memory базе данных H2.

API позволяет создавать, получать и удалять банковские счета, а также выполнять разные типы операций: пополнение счёта, снятие средств и начисление процентов. Вот реализация операции пополнения в контроллере и соответствующий код сервисного слоя, чтобы было понятно, как это выглядит:

// From AccountController.java
@PostMapping(path = "/{id}/deposit/{amount}", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<Account> depositToAccount(@PathVariable("id") Long id, @PathVariable("amount") double amount) {
 
return ResponseEntity.status(HttpStatus.OK).body(accountService.processDeposit(id, amount));
}
// From AccountService.java
@Transactional
public Account processDeposit(Long id, double amount) {
 
if (amount <= 0) {
throw new BadRequestException("Amount must be greater than 0, but was " + amount);
}
 
var accountPersisted = getAccount(id);
 
accountPersisted.setBalance(accountPersisted.getBalance() + amount);
 
return accountRepository.save(accountPersisted);
}

Разумеется, есть и соответствующий набор тестов, проверяющий реализацию этого API. Поскольку, повторюсь, моя цель — показать, что мутационное тестирование работает не только для юнит-тестов, используется набор приёмочных тестов, написанных с помощью REST Assured. Эти тесты поднимают локальный экземпляр API вместе с базой данных в памяти на машине, где выполняются тесты, поэтому время выполнения измеряется миллисекундами.

Вот пример такого теста. Обратите внимание: «сырой» код REST Assured вынесен в клиент, чтобы тесты было проще читать, писать и поддерживать.

@Test
public void depositIntoCheckingAccount_whenRetrieved_shouldShowUpdatedBalance() {
 
// Создание нового текущего счета
AccountDto account = new AccountDto(AccountType.CHECKING);
int accountId = this.accountClient.createAccount(account);
 
// Перечислить 10 долларов / евро / смурфов на счет
Response response = this.accountClient.depositToAccount(accountId, 10);
 
// Проверить, что депозит корректно обработан, и баланс изменился
Assertions.assertEquals(200, response.getStatusCode());
Assertions.assertEquals(10.0F, (Float) response.path("balance"));
}

Метод createAccount() в клиентском классе, который выполняет HTTP POST-запрос для создания нового аккаунта, выглядит так:

public int createAccount(AccountDto account) {
 
return given()
.spec(super.requestSpec())
.body(account)
.post("/account")
.then()
.statusCode(201)
.extract().path("id");
}


Аналогично метод depositToAccount() выполняет ещё один HTTP-запрос с использованием REST Assured. После прочтения статьи можно посмотреть кодовую базу, чтобы лучше понять структуру.

Запуск тестов и мутационного тестирования

В этом примере используется PIT как инструмент мутационного тестирования. Сначала запускаются тесты, чтобы убедиться, что все они проходят — нет смысла вычислять mutation score для падающих тестов. Используется команда mvn clean test.

[INFO] Tests run: 9, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 7.847 s -- in com.ontestautomation.mutationbank.MutationBankApplicationTests
[INFO]
[INFO] Results:
[INFO]
[INFO] Tests run: 9, Failures: 0, Errors: 0, Skipped: 0
[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  13.518 s
[INFO] Finished at: 2025-09-03T15:31:22+02:00
[INFO] ------------------------------------------------------------------------

Затем запускается мутационное тестирование с помощью PIT, используя команду mvn org.pitest:pitest-maven:mutationCoverage:

================================================================================
- Statistics
================================================================================
>> Line Coverage (for mutated classes only): 51/59 (86%)
>> Generated 55 mutations Killed 35 (64%)
>> Mutations with no coverage 6. Test strength 71%
>> Ran 66 tests (1.2 tests per mutation)
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  56.060 s
[INFO] Finished at: 2025-09-03T15:35:34+02:00
[INFO] ------------------------------------------------------------------------

Как можно видеть, прогон мутационного тестирования занял меньше минуты, что вполне приемлемо. Да, это небольшая кодовая база, но даже в этом случае набор приёмочных тестов (9 тестов) был выполнен 55 раз менее чем за минуту. Это подтверждает, что мутационное тестирование полезно не только для юнит-тестов.

Анализ отчётов

Если посмотреть на результаты мутационного тестирования и сосредоточиться на логике метода processDeposit() и созданных PIT мутациях, можно увидеть следующее:


Очевидно, часть бизнес-логики недостаточно покрыта тестами. В частности, нет проверки, что попытка внести неположительное значение на счёт приводит к выбросу BadRequestException (что, в свою очередь, даёт HTTP 400).

Добавление нового теста для улучшения mutation score

Чтобы «убить» мутации, которые до сих пор выжили, добавим тест, проверяющий попытку внести неположительное значение. Также покрывается граничное значение — добавляется тест с пополнением на 0:

@Test
public void depositZeroIntoAccount_shouldReturnHttp400() {
AccountDto account = new AccountDto(AccountType.CHECKING);
int accountId = this.accountClient.createAccount(account);
Response response = this.accountClient.depositToAccount(accountId, 0);
Assertions.assertEquals(400, response.getStatusCode());
Response getResponse = this.accountClient.getAccount(accountId);
Assertions.assertEquals(0.0F, (Float) getResponse.path("balance"));
}

После запуска тестов (новый тест тоже проходит) и повторного запуска мутационного тестирования видно, что время всё ещё меньше минуты, хотя тестов стало больше — 10 вместо 9:

================================================================================
- Statistics
================================================================================
>> Line Coverage (for mutated classes only): 52/59 (88%)
>> Generated 55 mutations Killed 37 (67%)
>> Mutations with no coverage 6. Test strength 76%
>> Ran 64 tests (1.16 tests per mutation)
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  57.084 s
[INFO] Finished at: 2025-09-03T15:53:37+02:00
[INFO] ------------------------------------------------------------------------

Если посмотреть отчёт, видно, что mutation score для этой части API тоже улучшился:


Это определённо шаг в правильном направлении: теперь у нас есть тест, который не только улучшает покрытие кода, но и гарантированно упадёт, если разработчик случайно изменит логику пополнения и разрешит вносить ноль или отрицательные значения. Отлично!

Попробуйте сами

Если хочется попробовать мутационное тестирование на той же кодовой базе, она доступна на GitHub. В ней есть всё: тестируемый API, приёмочные тесты и инструмент мутационного

Обсудить в форуме