Тайна JdbcTest в JUnit 5
Spring Boot предоставляет широкий набор инструментов для интеграционного тестирования приложения с использованием IoC-контейнера. Применяя те или иные аннотации, мы можем варьировать, какие наши или спринговые компоненты окажутся в контексте и каким образом они будут сконфигурированы.
Например, есть аннотация
Особенности @JdbcTest
Про данную аннотацию известно следующее:
— добавляет в контекст компоненты для работы с БД:
- DataSource;
- JdbcTemplate;
- NamedParameterJdbcTemplate;
...
— в начале каждого теста открывает транзакцию;
— и откатывает в конце;
— если над тестовым классом или методом явно прописать
В текущей заметке мы постараемся выяснить, за счет чего работают последние три пункта списка.
Общие принципы работы спринговых аннотаций
Для тестирования мы будем использовать JUnit пятой версии. И вначале попробуем разобраться, как получается, что фреймворк для тестирования, который по идее ничего не знает ни о каком спринге, начинает взаимодействовать с его аннотациями, внедрять что-то в тестовые классы и т. д. Т. е. откуда в тестах берется спринг.
Мы не зря выбрали для экспериментов именно пятый JUnit (и Spring Boot 2.3.1). В прошлых версиях, чтобы аннотации от спринг возымели эффект, надо было явно указать
Или все же не магически? Давайте откроем, любой проект, использующий
Итак. Открыв код аннотации мы видим примерно следующее:
@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited @BootstrapWith(JdbcTestContextBootstrapper.class) @ExtendWith(SpringExtension.class) @OverrideAutoConfiguration(enabled = false) @TypeExcludeFilters(JdbcTypeExcludeFilter.class) @Transactional @AutoConfigureCache @AutoConfigureJdbc @AutoConfigureTestDatabase @ImportAutoConfiguration public @interface JdbcTest { // И другой код... }
Тут много всего интересного, но на данный момент нам нужно обратить внимание только на одну строку:
@ExtendWith(SpringExtension.class)
Это как раз то, что позволяет подключить спринг в тесты.
public class SpringExtension implements BeforeAllCallback, AfterAllCallback, TestInstancePostProcessor, BeforeEachCallback, AfterEachCallback, BeforeTestExecutionCallback, AfterTestExecutionCallback, ParameterResolver { @Override public void beforeAll(ExtensionContext context) throws Exception { getTestContextManager(context).beforeTestClass(); } // Другие методы коллбеков от интерфесов JUnit... private static TestContextManager getTestContextManager(ExtensionContext context) { // Код... } }
Мы видим несколько методов, которые унаследованы от интерфесов коллбеков JUnit, каждый из которых с помощью
Если взглянуть на конструкторы
public class TestContextManager { public TestContextManager(Class<?> testClass) { this(BootstrapUtils.resolveTestContextBootstrapper( BootstrapUtils.createBootstrapContext(testClass)) ); } public TestContextManager(TestContextBootstrapper testContextBootstrapper) { this.testContext = testContextBootstrapper.buildTestContext(); registerTestExecutionListeners(testContextBootstrapper.getTestExecutionListeners()); } }
то в
// Код... @BootstrapWith(JdbcTestContextBootstrapper.class) // Еще код... public @interface JdbcTest { // И еще код... }
Собственно, это как раз тот класс, который создаст для теста контекст спринга. Само создание происходит в строке
Т. о. мы разобрались, откуда в тестах берется контекст спринг. Предлагаю зафиксировать все что нам уже известно:
— JUnit находит над классом аннотацию
А что с транзакциями?
Теперь пришло время выяснить, почему перед тестом открываются транзакции. Тут все просто. Над
// Код... @Transactional // Еще код... public @interface JdbcTest { // И еще код... }
Если не учитывать, что
Если вернуться к классу
public class TestContextManager { // Код... public TestContextManager(TestContextBootstrapper testContextBootstrapper) { // Код... registerTestExecutionListeners(testContextBootstrapper.getTestExecutionListeners()); } // Код... public void beforeTestMethod(Object testInstance, Method testMethod) throws Exception { // Код... for (TestExecutionListener testExecutionListener : getTestExecutionListeners()) { try { testExecutionListener.beforeTestMethod(getTestContext()); } catch (Throwable ex) { // Код... } } } // Код... public void afterTestMethod(Object testInstance, Method testMethod, @Nullable Throwable exception) throws Exception { // Код... for (TestExecutionListener testExecutionListener : getReversedTestExecutionListeners()) { try { testExecutionListener.afterTestMethod(getTestContext()); } catch (Throwable ex) { // Код... } } // Код... } }
Т. е. сначала регистрируются какие-то
public class TransactionalTestExecutionListener extends AbstractTestExecutionListener { @Override public void beforeTestMethod(final TestContext testContext) throws Exception { // Код... if (transactionAttribute != null) { if (transactionAttribute.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NOT_SUPPORTED) { return; } } // Код... if (tm != null) { txContext = new TransactionContext(testContext, tm, transactionAttribute, isRollback(testContext)); runBeforeTransactionMethods(testContext); txContext.startTransaction(); TransactionContextHolder.setCurrentTransactionContext(txContext); } } }
Сразу видно, что если у аннотации
public class TransactionalTestExecutionListener extends AbstractTestExecutionListener { // Код... protected final boolean isRollback(TestContext testContext) throws Exception { boolean rollback = isDefaultRollback(testContext); Rollback rollbackAnnotation = AnnotatedElementUtils .findMergedAnnotation(testContext.getTestMethod(), Rollback.class); if (rollbackAnnotation != null) { boolean rollbackOverride = rollbackAnnotation.value(); // Код... rollback = rollbackOverride; } else { // Код... } return rollback; } // Код... }
Ага. Если над тестовым методом найдена аннотация
public class TransactionalTestExecutionListener extends AbstractTestExecutionListener { // Код... protected final boolean isDefaultRollback(TestContext testContext) throws Exception { Class<?> testClass = testContext.getTestClass(); Rollback rollback = AnnotatedElementUtils.findMergedAnnotation(testClass, Rollback.class); boolean rollbackPresent = (rollback != null); if (rollbackPresent) { boolean defaultRollback = rollback.value(); // Код... return defaultRollback; } // else return true; } // Код...
Тут примерно то же самое. Только
Подытожим:
- когда контекст создан,
TestContextManager регистрирует наборTestExecutionListener -ов; - при вызове метода-коллбека у каждого лисенера вызывается метод, соответствующий текущему событию жизненного цикла теста;
- один из таких лисенеров — это
TransactionalTestExecutionListener ; - у которого перед каждым тестом вызывается метод
beforeTestMethod , где создается транзакция; - если над тестовым классом или методом теста висит аннотация
@Rollback , то будет ли откатываться транзакция после теста, зависит от значения, что передали в аргумент аннотации; - если же данная аннотация не найдена, то транзакция по умолчанию будет откачена;
- и произойдет это при ее завершении внутри метода
afterTestMethod классаTransactionalTestExecutionListener , что будет вызван для события завершения теста.
Вот и все)