Unit-тесты являются важной частью любого достаточно большого проекта. Хочу поделиться с вами небольшой детективной историей, связанной с неочевидным массовым их падением.
Начинается она с того, что в проекте в результате определенного безобидного коммита упало порядка 150 тестов, набор падающих тестов при этом не являлся стабильным. Тесты не были связаны между собой, выполнение тестов происходило последовательно. В качестве источника данных для тестов служит in-memory база данных h2. Падение подавляющего большинства из этих 150 тестов сопровождалось ошибкой в логе: «Cannot get a connection, pool error Timeout waiting for idle object». Следует сказать, что размер пула коннектов при выполнении тестов в проекте равен 1.
Небольшое лирическое отступление: в коде проекта периодически используется отвязка транзакции от потока, далее выполнение кода в отдельной транзакции и, наконец, обратная привязка транзакции. Для такого рода случаев написан вспомогательный класс, использование которого выглядит примерно так:
В результате анализа было выявлено, что ошибка начинает проявляться после провалившегося теста, содержащего в себе вызов кода в транзакции:
Заглянем внутрь класса TransactionRunner, вызов метода приводит к следующему коду:
Итак, в чем же здесь проблема? А проблема заключается в том, что AssertionError возникающий в результате выполнения кода теста не наследуется от Exception, а значит вложенная транзакция ни откатывается, ни коммитится. Так как размер пула коннектов равен единице — получаем ту самую ошибку «Cannot get a connection, pool error Timeout waiting for idle object» при попытке получить объект Connection последующими тестами.
Мораль: необходимо размещать assert'ы в тестах с осторожностью, а в случае неочевидных и, в особенности, массовых падений одним из вариантов решения является проверка, учитывает ли обработка исключений объекты не наследующиеся от Exception.
Случай показался мне достойным фиксации, возможно кому-нибудь пригодится этот опыт.
Начинается она с того, что в проекте в результате определенного безобидного коммита упало порядка 150 тестов, набор падающих тестов при этом не являлся стабильным. Тесты не были связаны между собой, выполнение тестов происходило последовательно. В качестве источника данных для тестов служит in-memory база данных h2. Падение подавляющего большинства из этих 150 тестов сопровождалось ошибкой в логе: «Cannot get a connection, pool error Timeout waiting for idle object». Следует сказать, что размер пула коннектов при выполнении тестов в проекте равен 1.
Небольшое лирическое отступление: в коде проекта периодически используется отвязка транзакции от потока, далее выполнение кода в отдельной транзакции и, наконец, обратная привязка транзакции. Для такого рода случаев написан вспомогательный класс, использование которого выглядит примерно так:
TransactionRunner.run(dbDataManager(), new MethodTransaction() {
@Override
public ExecutionResult runInTransaction() throws Exception {
// код, который необходимо выполнить в отдельной транзакции
return result;
}
);
В результате анализа было выявлено, что ошибка начинает проявляться после провалившегося теста, содержащего в себе вызов кода в транзакции:
TransactionRunner.run(dbDataManager(), new MethodTransaction() {
@Override
public ExecutionResult runInTransaction() throws Exception {
// ... рабочий код
// assert который валится
assert( 1, result.getSomeNotEqualOneIntValue() );
return result;
}
);
Заглянем внутрь класса TransactionRunner, вызов метода приводит к следующему коду:
protected ExecutionResult run() throws CommonException {
Transaction outerTr = getThreadTransaction();
bindThreadTransaction(null);
try {
beginTransaction();
try {
setResult(transactionCode.runInTransaction());
} catch (Exception e) {
dbDataManager().rollbackTransaction();
if (transaction.onException(this, e))
throw e;
}
dbDataManager().commitTransaction();
return getResult();
} catch (Exception e) {
throw ExceptionUtil.createCommonException(e);
} finally {
bindThreadTransaction(outerTr);
}
}
Итак, в чем же здесь проблема? А проблема заключается в том, что AssertionError возникающий в результате выполнения кода теста не наследуется от Exception, а значит вложенная транзакция ни откатывается, ни коммитится. Так как размер пула коннектов равен единице — получаем ту самую ошибку «Cannot get a connection, pool error Timeout waiting for idle object» при попытке получить объект Connection последующими тестами.
Мораль: необходимо размещать assert'ы в тестах с осторожностью, а в случае неочевидных и, в особенности, массовых падений одним из вариантов решения является проверка, учитывает ли обработка исключений объекты не наследующиеся от Exception.
Случай показался мне достойным фиксации, возможно кому-нибудь пригодится этот опыт.