HTML Diff
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&lt;?&gt; 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&lt;?&gt; 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&lt;?&gt; 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&lt;?&gt; 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