...

суббота, 18 августа 2018 г.

[Из песочницы] Неочевидная проблема использования assert

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

Начинается она с того, что в проекте в результате определенного безобидного коммита упало порядка 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.

Случай показался мне достойным фиксации, возможно кому-нибудь пригодится этот опыт.

Let's block ads! (Why?)

Комментариев нет:

Отправить комментарий