0 added
0 removed
Original
2026-01-01
Modified
2026-03-10
1
<p>Spring Boot предоставляет широкий набор инструментов для интеграционного тестирования приложения с использованием IoC-контейнера. Применяя те или иные аннотации, мы можем варьировать, какие наши или спринговые компоненты окажутся в контексте и каким образом они будут сконфигурированы.</p>
1
<p>Spring Boot предоставляет широкий набор инструментов для интеграционного тестирования приложения с использованием IoC-контейнера. Применяя те или иные аннотации, мы можем варьировать, какие наши или спринговые компоненты окажутся в контексте и каким образом они будут сконфигурированы.</p>
2
<p>Например, есть аннотация @SpringBootTest, которая, если мы специально не ограничим область ее действия, может поднять контекст целиком. Также существуют более узкоспециальные аннотации, отвечающие за создание только определенных компонентов, обычно относящихся к тому или иному слою приложения. Одной из таких аннотаций является @JdbcTest и именно о том, как она работает, пойдет речь далее.</p>
2
<p>Например, есть аннотация @SpringBootTest, которая, если мы специально не ограничим область ее действия, может поднять контекст целиком. Также существуют более узкоспециальные аннотации, отвечающие за создание только определенных компонентов, обычно относящихся к тому или иному слою приложения. Одной из таких аннотаций является @JdbcTest и именно о том, как она работает, пойдет речь далее.</p>
3
<h3>Особенности @JdbcTest</h3>
3
<h3>Особенности @JdbcTest</h3>
4
<p>Про данную аннотацию известно следующее: - добавляет в контекст компоненты для работы с БД: - DataSource; - JdbcTemplate; - NamedParameterJdbcTemplate; ... - в начале каждого теста открывает транзакцию; - и откатывает в конце; - если над тестовым классом или методом явно прописать @Transactional(propagation = Propagation.NOT_SUPPORTED), то транзакция перед тестом открываться не будет.</p>
4
<p>Про данную аннотацию известно следующее: - добавляет в контекст компоненты для работы с БД: - DataSource; - JdbcTemplate; - NamedParameterJdbcTemplate; ... - в начале каждого теста открывает транзакцию; - и откатывает в конце; - если над тестовым классом или методом явно прописать @Transactional(propagation = Propagation.NOT_SUPPORTED), то транзакция перед тестом открываться не будет.</p>
5
<p>В текущей заметке мы постараемся выяснить, за счет чего работают последние три пункта списка.</p>
5
<p>В текущей заметке мы постараемся выяснить, за счет чего работают последние три пункта списка.</p>
6
<h3>Общие принципы работы спринговых аннотаций</h3>
6
<h3>Общие принципы работы спринговых аннотаций</h3>
7
<p>Для тестирования мы будем использовать JUnit пятой версии. И вначале попробуем разобраться, как получается, что фреймворк для тестирования, который по идее ничего не знает ни о каком спринге, начинает взаимодействовать с его аннотациями, внедрять что-то в тестовые классы и т. д. Т. е. откуда в тестах берется спринг.</p>
7
<p>Для тестирования мы будем использовать JUnit пятой версии. И вначале попробуем разобраться, как получается, что фреймворк для тестирования, который по идее ничего не знает ни о каком спринге, начинает взаимодействовать с его аннотациями, внедрять что-то в тестовые классы и т. д. Т. е. откуда в тестах берется спринг.</p>
8
<p>Мы не зря выбрали для экспериментов именно пятый JUnit (и Spring Boot 2.3.1). В прошлых версиях, чтобы аннотации от спринг возымели эффект, надо было явно указать @RunWith(SpringRunner.class). Это уже могло навести на некоторые мысли о том, как все работает. В современной версии достаточно указать только @SpringBootTest или @JdbcTest, и все заведется магическим образом, само.</p>
8
<p>Мы не зря выбрали для экспериментов именно пятый JUnit (и Spring Boot 2.3.1). В прошлых версиях, чтобы аннотации от спринг возымели эффект, надо было явно указать @RunWith(SpringRunner.class). Это уже могло навести на некоторые мысли о том, как все работает. В современной версии достаточно указать только @SpringBootTest или @JdbcTest, и все заведется магическим образом, само.</p>
9
<p>Или все же не магически? Давайте откроем, любой проект, использующий spring-boot-starter-test в среде разработки "IntelliJ IDEA" (достаточно Community версии), найдем аннотацию @JdbcTest и посмотрим на ее код. Также желательно загрузить исходные коды, когда IDEA это предложит (Download Sources), если они не были загружены до этого.</p>
9
<p>Или все же не магически? Давайте откроем, любой проект, использующий spring-boot-starter-test в среде разработки "IntelliJ IDEA" (достаточно Community версии), найдем аннотацию @JdbcTest и посмотрим на ее код. Также желательно загрузить исходные коды, когда IDEA это предложит (Download Sources), если они не были загружены до этого.</p>
10
<p>Итак. Открыв код аннотации мы видим примерно следующее:</p>
10
<p>Итак. Открыв код аннотации мы видим примерно следующее:</p>
11
@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 { // И другой код... }<p>Тут много всего интересного, но на данный момент нам нужно обратить внимание только на одну строку:</p>
11
@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 { // И другой код... }<p>Тут много всего интересного, но на данный момент нам нужно обратить внимание только на одну строку:</p>
12
@ExtendWith(SpringExtension.class)<p>Это как раз то, что позволяет подключить спринг в тесты. @ExtendWith - аннотация от JUnit 5, SpringExtension - класс спринга, который реализует несколько интерфейсов тестового фреймворка. Давайте заглянем внутрь:</p>
12
@ExtendWith(SpringExtension.class)<p>Это как раз то, что позволяет подключить спринг в тесты. @ExtendWith - аннотация от JUnit 5, SpringExtension - класс спринга, который реализует несколько интерфейсов тестового фреймворка. Давайте заглянем внутрь:</p>
13
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) { // Код... } }<p>Мы видим несколько методов, которые унаследованы от интерфесов коллбеков JUnit, каждый из которых с помощью getTestContextManager получает объект типа TestContextManager и вызывает тот или иной его метод, чье название говорит само за себя.</p>
13
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) { // Код... } }<p>Мы видим несколько методов, которые унаследованы от интерфесов коллбеков JUnit, каждый из которых с помощью getTestContextManager получает объект типа TestContextManager и вызывает тот или иной его метод, чье название говорит само за себя.</p>
14
<p>Если взглянуть на конструкторы TestContextManager:</p>
14
<p>Если взглянуть на конструкторы TestContextManager:</p>
15
public class TestContextManager { public TestContextManager(Class<?> testClass) { this(BootstrapUtils.resolveTestContextBootstrapper( BootstrapUtils.createBootstrapContext(testClass)) ); } public TestContextManager(TestContextBootstrapper testContextBootstrapper) { this.testContext = testContextBootstrapper.buildTestContext(); registerTestExecutionListeners(testContextBootstrapper.getTestExecutionListeners()); } }<p>то в BootstrapUtils.resolveTestContextBootstrapper(...), мы увидим знакомое слово, которое нам уже встречалось. А именно - TestContextBootstrapper. И действительно. Мы его видели над аннотацией @JdbcTest:</p>
15
public class TestContextManager { public TestContextManager(Class<?> testClass) { this(BootstrapUtils.resolveTestContextBootstrapper( BootstrapUtils.createBootstrapContext(testClass)) ); } public TestContextManager(TestContextBootstrapper testContextBootstrapper) { this.testContext = testContextBootstrapper.buildTestContext(); registerTestExecutionListeners(testContextBootstrapper.getTestExecutionListeners()); } }<p>то в BootstrapUtils.resolveTestContextBootstrapper(...), мы увидим знакомое слово, которое нам уже встречалось. А именно - TestContextBootstrapper. И действительно. Мы его видели над аннотацией @JdbcTest:</p>
16
// Код... @BootstrapWith(JdbcTestContextBootstrapper.class) // Еще код... public @interface JdbcTest { // И еще код... }<p>Собственно, это как раз тот класс, который создаст для теста контекст спринга. Само создание происходит в строке this.testContext = testContextBootstrapper.buildTestContext(); второго конструктора класса TestContextManager.</p>
16
// Код... @BootstrapWith(JdbcTestContextBootstrapper.class) // Еще код... public @interface JdbcTest { // И еще код... }<p>Собственно, это как раз тот класс, который создаст для теста контекст спринга. Само создание происходит в строке this.testContext = testContextBootstrapper.buildTestContext(); второго конструктора класса TestContextManager.</p>
17
<p>Т. о. мы разобрались, откуда в тестах берется контекст спринг. Предлагаю зафиксировать все что нам уже известно: - JUnit находит над классом аннотацию @ExtendWith; - в классе, что указан в качестве аргумента аннотации (в нашем случае это SpringExtension.class), фреймворк вызывает методы-коллбеки своих интерфейсов, который данный класс реализует; - в коллбеках создается экземпляр класса TestContextManager; - в его конструкторе происходит вычисление класса, что будет создавать контекст спринга: - в BootstrapUtils.resolveTestContextBootstrapper(...) определяется, какая аннотация висит над тестовым классом (у нас @JdbcTest); - над ней ищется @BootstrapWith и берется класс, указанный в качестве аргумента; - с помощью найденного класса (в нашем случае JdbcTestContextBootstrapper) создается контекст.</p>
17
<p>Т. о. мы разобрались, откуда в тестах берется контекст спринг. Предлагаю зафиксировать все что нам уже известно: - JUnit находит над классом аннотацию @ExtendWith; - в классе, что указан в качестве аргумента аннотации (в нашем случае это SpringExtension.class), фреймворк вызывает методы-коллбеки своих интерфейсов, который данный класс реализует; - в коллбеках создается экземпляр класса TestContextManager; - в его конструкторе происходит вычисление класса, что будет создавать контекст спринга: - в BootstrapUtils.resolveTestContextBootstrapper(...) определяется, какая аннотация висит над тестовым классом (у нас @JdbcTest); - над ней ищется @BootstrapWith и берется класс, указанный в качестве аргумента; - с помощью найденного класса (в нашем случае JdbcTestContextBootstrapper) создается контекст.</p>
18
<h3>А что с транзакциями?</h3>
18
<h3>А что с транзакциями?</h3>
19
<p>Теперь пришло время выяснить, почему перед тестом открываются транзакции. Тут все просто. Над @JdbcTest висит @Transactional. Вот он:</p>
19
<p>Теперь пришло время выяснить, почему перед тестом открываются транзакции. Тут все просто. Над @JdbcTest висит @Transactional. Вот он:</p>
20
// Код... @Transactional // Еще код... public @interface JdbcTest { // И еще код... }<p>Если не учитывать, что @Transactional работает только для объектов в контексте (а класс с тестом там не лежит), то создание транзакции перед каждым методом класса с @Transactional является для спринга вполне штатной ситуацией. А вот то, что она откатывается - нет. Займемся выяснением причин такого поведения.</p>
20
// Код... @Transactional // Еще код... public @interface JdbcTest { // И еще код... }<p>Если не учитывать, что @Transactional работает только для объектов в контексте (а класс с тестом там не лежит), то создание транзакции перед каждым методом класса с @Transactional является для спринга вполне штатной ситуацией. А вот то, что она откатывается - нет. Займемся выяснением причин такого поведения.</p>
21
<p>Если вернуться к классу TestContextManager, можно увидеть в нем следующий код:</p>
21
<p>Если вернуться к классу TestContextManager, можно увидеть в нем следующий код:</p>
22
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) { // Код... } } // Код... } }<p>Т. е. сначала регистрируются какие-то TestExecutionListener-ы, а потом прогоняются в обработчике коллбека для JUnit. Выше показан частичный код методов, которые будут вызваться перед и после каждого теста соответственно.</p>
22
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) { // Код... } } // Код... } }<p>Т. е. сначала регистрируются какие-то TestExecutionListener-ы, а потом прогоняются в обработчике коллбека для JUnit. Выше показан частичный код методов, которые будут вызваться перед и после каждого теста соответственно.</p>
23
<p>TestExecutionListener - это интерфейс и одной из его реализаций является TransactionalTestExecutionListener. Вот интересный кусочек кода этого класса:</p>
23
<p>TestExecutionListener - это интерфейс и одной из его реализаций является TransactionalTestExecutionListener. Вот интересный кусочек кода этого класса:</p>
24
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); } } }<p>Сразу видно, что если у аннотации @Transactional, аргумент propagation будет равен NOT_SUPPORTED, то транзакция даже не будет создана. А вот вот вызов метода isRollback при создании TransactionContext очень похож на то место, где наше расследование завершится. Этот метод определяет, нужно ли откатывать транзакцию после завершения метода теста.</p>
24
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); } } }<p>Сразу видно, что если у аннотации @Transactional, аргумент propagation будет равен NOT_SUPPORTED, то транзакция даже не будет создана. А вот вот вызов метода isRollback при создании TransactionContext очень похож на то место, где наше расследование завершится. Этот метод определяет, нужно ли откатывать транзакцию после завершения метода теста.</p>
25
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; } // Код... }<p>Ага. Если над тестовым методом найдена аннотация @Rollback, то будет использоваться значение ее аргумента value (еще один способ изменить поведение @Transactional над тестом). Если же такая аннотация не найдена, то ориентируемся на результат isDefaultRollback(testContext).</p>
25
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; } // Код... }<p>Ага. Если над тестовым методом найдена аннотация @Rollback, то будет использоваться значение ее аргумента value (еще один способ изменить поведение @Transactional над тестом). Если же такая аннотация не найдена, то ориентируемся на результат isDefaultRollback(testContext).</p>
26
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; } // Код...<p>Тут примерно то же самое. Только @Rollback ищется над классом теста. Если аннотация не найдена, возвращается true. Т. е. откатываем транзакцию.</p>
26
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; } // Код...<p>Тут примерно то же самое. Только @Rollback ищется над классом теста. Если аннотация не найдена, возвращается true. Т. е. откатываем транзакцию.</p>
27
<h3>Подытожим:</h3>
27
<h3>Подытожим:</h3>
28
<ul><li>когда контекст создан, TestContextManager регистрирует набор TestExecutionListener-ов;</li>
28
<ul><li>когда контекст создан, TestContextManager регистрирует набор TestExecutionListener-ов;</li>
29
<li>при вызове метода-коллбека у каждого лисенера вызывается метод, соответствующий текущему событию жизненного цикла теста;</li>
29
<li>при вызове метода-коллбека у каждого лисенера вызывается метод, соответствующий текущему событию жизненного цикла теста;</li>
30
<li>один из таких лисенеров - это TransactionalTestExecutionListener;</li>
30
<li>один из таких лисенеров - это TransactionalTestExecutionListener;</li>
31
<li>у которого перед каждым тестом вызывается метод beforeTestMethod, где создается транзакция;</li>
31
<li>у которого перед каждым тестом вызывается метод beforeTestMethod, где создается транзакция;</li>
32
<li>если над тестовым классом или методом теста висит аннотация @Rollback, то будет ли откатываться транзакция после теста, зависит от значения, что передали в аргумент аннотации;</li>
32
<li>если над тестовым классом или методом теста висит аннотация @Rollback, то будет ли откатываться транзакция после теста, зависит от значения, что передали в аргумент аннотации;</li>
33
<li>если же данная аннотация не найдена, то транзакция по умолчанию будет откачена;</li>
33
<li>если же данная аннотация не найдена, то транзакция по умолчанию будет откачена;</li>
34
<li>и произойдет это при ее завершении внутри метода afterTestMethod класса TransactionalTestExecutionListener, что будет вызван для события завершения теста.</li>
34
<li>и произойдет это при ее завершении внутри метода afterTestMethod класса TransactionalTestExecutionListener, что будет вызван для события завершения теста.</li>
35
</ul><p>Вот и все)</p>
35
</ul><p>Вот и все)</p>
36
36