@Transactional не откатывается при использовании TestContainers

Проблема, которую я вижу, заключается в том, что @Transactional вызывает создание и откат транзакции, однако данные переносятся из одного теста в другой.

Используемые версии:

implementation('org.springframework.boot:spring-boot-starter-data-jpa')
testCompile "org.testcontainers:testcontainers:1.12.1"
testCompile "org.testcontainers:mysql:1.12.1"

Код:

@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest
@ContextConfiguration(initializers = {Java8RepoTests.Initializer.class})
@Slf4j
@Transactional 
public class Java8RepoTests {
    @ClassRule
    public static MySQLContainer mysql = new MySQLContainer();

    @BeforeClass
    public static void setUpBeforeClass() throws Exception {
        logger.info("setUpBeforeClass()");
        mysql.start();
    }

    @AfterClass
    public static void tearDownAfterClass() throws Exception {
        logger.info("tearDownAfterClass()");
        mysql.stop();
    }

    @Autowired
    private EntityManagerFactory entityManagerFactory;

    private EntityManager em;

    @Before
    public void setUp() throws Exception {
        logger.info("setUp()");
        em = entityManagerFactory.createEntityManager();
    }

    @After
    public void tearDown() throws Exception {
        logger.info("tearDown()");
        em.close();
    }

    @Autowired UserRepository repository;

    @Test
    public void testOne() {
        int count = Iterables.size(repository.findAll());
        // count = 0 at this point

        User user = new User();
        user = FillWithTestData(user);
        User targetUser = repository.save(user);

        count = Iterables.size(repository.findAll());   
        // count = 1
    }

    @Test
    public void testTwo() {         
        int count = Iterables.size(repository.findAll());
        // count = 1 at this point, should be zero!

        User user = new User();
        User targetUser= repository.save(user);
        // Exception thrown that user already exists
    }

    static class Initializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {
        @Override
        public void initialize(ConfigurableApplicationContext configurableApplicationContext) {
            TestPropertyValues.of(
                    "spring.datasource.url=" + mysql.getJdbcUrl(),
                    "spring.datasource.username=" + mysql.getUsername(),
                    "spring.datasource.password=" + mysql.getPassword()
                    ).applyTo(configurableApplicationContext.getEnvironment());

        }
    }
}

Вывод журнала:

2019-09-16 13:55:01.074  INFO 21456 --- [           main] j.LocalContainerEntityManagerFactoryBean : Initialized JPA EntityManagerFactory for persistence unit 'default'
2019-09-16 13:55:07.936  INFO 21456 --- [           main] o.s.s.c.ThreadPoolTaskExecutor           : Initializing ExecutorService 'applicationTaskExecutor'
2019-09-16 13:55:08.372  WARN 21456 --- [           main] aWebConfiguration$JpaWebMvcConfiguration : spring.jpa.open-in-view is enabled by default. Therefore, database queries may be performed during view rendering. Explicitly configure spring.jpa.open-in-view to disable this warning
2019-09-16 13:55:13.312  INFO 21456 --- [           main] o.s.t.c.t.TransactionContext             : Began transaction (1) for test context [DefaultTestContext@7a22302c testClass = Java8RepoTests, testInstance = com.test.api.Java8RepoTests@4af50246, testMethod = testOne@Java8RepoTests, testException = [null], mergedContextConfiguration = [WebMergedContextConfiguration@45482f82 testClass = Java8RepoTests, locations = '{}', classes = '{class com.test.api.ApiApplication}', contextInitializerClasses = '[class com.test.api.Java8RepoTests$Initializer]', activeProfiles = '{}', propertySourceLocations = '{}', propertySourceProperties = '{org.springframework.boot.test.context.SpringBootTestContextBootstrapper=true}', contextCustomizers = set[org.springframework.boot.test.autoconfigure.properties.PropertyMappingContextCustomizer@0, org.springframework.boot.test.autoconfigure.web.servlet.WebDriverContextCustomizerFactory$Customizer@5af3afd9, org.springframework.boot.test.context.filter.ExcludeFilterContextCustomizer@5ffead27, org.springframework.boot.test.json.DuplicateJsonObjectContextCustomizerFactory$DuplicateJsonObjectContextCustomizer@1fa268de, org.springframework.boot.test.mock.mockito.MockitoContextCustomizer@0, org.springframework.boot.test.web.client.TestRestTemplateContextCustomizer@222545dc], resourceBasePath = 'src/main/webapp', contextLoader = 'org.springframework.boot.test.context.SpringBootContextLoader', parent = [null]], attributes = map['org.springframework.test.context.web.ServletTestExecutionListener.activateListener' -> true, 'org.springframework.test.context.web.ServletTestExecutionListener.populatedRequestContextHolder' -> true, 'org.springframework.test.context.web.ServletTestExecutionListener.resetRequestContextHolder' -> true]]; transaction manager [org.springframework.orm.jpa.JpaTransactionManager@448fa659]; rollback [true]
Hibernate: select user0_.id as id1_11_, user0_.created_date as created_2_11_, user0_.updated_date as updated_3_11_, user0_.created_by as created_4_11_, user0_.last_modified_by as last_mod5_11_, user0_.active as active6_11_, user0_.email as email7_11_, user0_.enabled as enabled8_11_, user0_.first_name as first_na9_11_, user0_.is_using2fa as is_usin10_11_, user0_.last_name as last_na11_11_, user0_.password as passwor12_11_ from users user0_
Hibernate: insert into users (created_date, updated_date, created_by, last_modified_by, active, email, enabled, first_name, is_using2fa, last_name, password) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
Hibernate: select user0_.id as id1_11_, user0_.created_date as created_2_11_, user0_.updated_date as updated_3_11_, user0_.created_by as created_4_11_, user0_.last_modified_by as last_mod5_11_, user0_.active as active6_11_, user0_.email as email7_11_, user0_.enabled as enabled8_11_, user0_.first_name as first_na9_11_, user0_.is_using2fa as is_usin10_11_, user0_.last_name as last_na11_11_, user0_.password as passwor12_11_ from users user0_
Hibernate: select user0_.id as id1_11_0_, user0_.created_date as created_2_11_0_, user0_.updated_date as updated_3_11_0_, user0_.created_by as created_4_11_0_, user0_.last_modified_by as last_mod5_11_0_, user0_.active as active6_11_0_, user0_.email as email7_11_0_, user0_.enabled as enabled8_11_0_, user0_.first_name as first_na9_11_0_, user0_.is_using2fa as is_usin10_11_0_, user0_.last_name as last_na11_11_0_, user0_.password as passwor12_11_0_, credential1_.user_id as user_id1_9_1_, credential2_.id as credenti2_9_1_, credential2_.id as id1_2_2_, credential2_.created_date as created_2_2_2_, credential2_.updated_date as updated_3_2_2_, credential2_.created_by as created_4_2_2_, credential2_.last_modified_by as last_mod5_2_2_, credential2_.cloud_provider_id as cloud_pr8_2_2_, credential2_.access_key as access_k6_2_2_, credential2_.owner_id as owner_id9_2_2_, credential2_.secret_key as secret_k7_2_2_, credential2_.virtual_datacenter_id as virtual10_2_2_, cloudprovi3_.id as id1_1_3_, cloudprovi3_.created_date as created_2_1_3_, cloudprovi3_.updated_date as updated_3_1_3_, cloudprovi3_.name as name4_1_3_, user4_.id as id1_11_4_, user4_.created_date as created_2_11_4_, user4_.updated_date as updated_3_11_4_, user4_.created_by as created_4_11_4_, user4_.last_modified_by as last_mod5_11_4_, user4_.active as active6_11_4_, user4_.email as email7_11_4_, user4_.enabled as enabled8_11_4_, user4_.first_name as first_na9_11_4_, user4_.is_using2fa as is_usin10_11_4_, user4_.last_name as last_na11_11_4_, user4_.password as passwor12_11_4_, virtualdat5_.id as id1_13_5_, virtualdat5_.created_date as created_2_13_5_, virtualdat5_.updated_date as updated_3_13_5_, virtualdat5_.created_by as created_4_13_5_, virtualdat5_.last_modified_by as last_mod5_13_5_, virtualdat5_.blueprint_id as blueprin7_13_5_, virtualdat5_.name as name6_13_5_, virtualdat5_.organization_id as organiza8_13_5_, virtualdat5_.owner_id as owner_id9_13_5_, roles6_.user_id as user_id1_10_6_, role7_.id as role_id2_10_6_, role7_.id as id1_7_7_, role7_.created_date as created_2_7_7_, role7_.updated_date as updated_3_7_7_, role7_.title as title4_7_7_ from users user0_ left outer join user_credentials credential1_ on user0_.id=credential1_.user_id left outer join credentials credential2_ on credential1_.credential_id=credential2_.id left outer join cloud_providers cloudprovi3_ on credential2_.cloud_provider_id=cloudprovi3_.id left outer join users user4_ on credential2_.owner_id=user4_.id left outer join virtual_datacenters virtualdat5_ on credential2_.virtual_datacenter_id=virtualdat5_.id left outer join user_role roles6_ on user0_.id=roles6_.user_id left outer join role role7_ on roles6_.role_id=role7_.id where user0_.id=?
2019-09-16 13:55:14.781  INFO 21456 --- [           main] o.s.t.c.t.TransactionContext             : Rolled back transaction for test: [DefaultTestContext@7a22302c testClass = Java8RepoTests, testInstance = com.test.api.Java8RepoTests@4af50246, testMethod = testOne@Java8RepoTests, testException = [null], mergedContextConfiguration = [WebMergedContextConfiguration@45482f82 testClass = Java8RepoTests, locations = '{}', classes = '{class com.test.api.ApiApplication}', contextInitializerClasses = '[class com.test.api.Java8RepoTests$Initializer]', activeProfiles = '{}', propertySourceLocations = '{}', propertySourceProperties = '{org.springframework.boot.test.context.SpringBootTestContextBootstrapper=true}', contextCustomizers = set[org.springframework.boot.test.autoconfigure.properties.PropertyMappingContextCustomizer@0, org.springframework.boot.test.autoconfigure.web.servlet.WebDriverContextCustomizerFactory$Customizer@5af3afd9, org.springframework.boot.test.context.filter.ExcludeFilterContextCustomizer@5ffead27, org.springframework.boot.test.json.DuplicateJsonObjectContextCustomizerFactory$DuplicateJsonObjectContextCustomizer@1fa268de, org.springframework.boot.test.mock.mockito.MockitoContextCustomizer@0, org.springframework.boot.test.web.client.TestRestTemplateContextCustomizer@222545dc], resourceBasePath = 'src/main/webapp', contextLoader = 'org.springframework.boot.test.context.SpringBootContextLoader', parent = [null]], attributes = map['org.springframework.test.context.web.ServletTestExecutionListener.activateListener' -> true, 'org.springframework.test.context.web.ServletTestExecutionListener.populatedRequestContextHolder' -> true, 'org.springframework.test.context.web.ServletTestExecutionListener.resetRequestContextHolder' -> true]]
2019-09-16 13:55:14.794  INFO 21456 --- [           main] o.s.t.c.t.TransactionContext             : Began transaction (1) for test context [DefaultTestContext@7a22302c testClass = Java8RepoTests, testInstance = com.test.api.Java8RepoTests@34a99d8, testMethod = testTwo@Java8RepoTests, testException = [null], mergedContextConfiguration = [WebMergedContextConfiguration@45482f82 testClass = Java8RepoTests, locations = '{}', classes = '{class com.test.api.ApiApplication}', contextInitializerClasses = '[class com.test.api.Java8RepoTests$Initializer]', activeProfiles = '{}', propertySourceLocations = '{}', propertySourceProperties = '{org.springframework.boot.test.context.SpringBootTestContextBootstrapper=true}', contextCustomizers = set[org.springframework.boot.test.autoconfigure.properties.PropertyMappingContextCustomizer@0, org.springframework.boot.test.autoconfigure.web.servlet.WebDriverContextCustomizerFactory$Customizer@5af3afd9, org.springframework.boot.test.context.filter.ExcludeFilterContextCustomizer@5ffead27, org.springframework.boot.test.json.DuplicateJsonObjectContextCustomizerFactory$DuplicateJsonObjectContextCustomizer@1fa268de, org.springframework.boot.test.mock.mockito.MockitoContextCustomizer@0, org.springframework.boot.test.web.client.TestRestTemplateContextCustomizer@222545dc], resourceBasePath = 'src/main/webapp', contextLoader = 'org.springframework.boot.test.context.SpringBootContextLoader', parent = [null]], attributes = map['org.springframework.test.context.web.ServletTestExecutionListener.activateListener' -> true, 'org.springframework.test.context.web.ServletTestExecutionListener.populatedRequestContextHolder' -> true, 'org.springframework.test.context.web.ServletTestExecutionListener.resetRequestContextHolder' -> true]]; transaction manager [org.springframework.orm.jpa.JpaTransactionManager@448fa659]; rollback [true]
Hibernate: select user0_.id as id1_11_, user0_.created_date as created_2_11_, user0_.updated_date as updated_3_11_, user0_.created_by as created_4_11_, user0_.last_modified_by as last_mod5_11_, user0_.active as active6_11_, user0_.email as email7_11_, user0_.enabled as enabled8_11_, user0_.first_name as first_na9_11_, user0_.is_using2fa as is_usin10_11_, user0_.last_name as last_na11_11_, user0_.password as passwor12_11_ from users user0_
Hibernate: select roles0_.user_id as user_id1_10_0_, roles0_.role_id as role_id2_10_0_, role1_.id as id1_7_1_, role1_.created_date as created_2_7_1_, role1_.updated_date as updated_3_7_1_, role1_.title as title4_7_1_ from user_role roles0_ inner join role role1_ on roles0_.role_id=role1_.id where roles0_.user_id=?
Hibernate: select credential0_.user_id as user_id1_9_0_, credential0_.credential_id as credenti2_9_0_, credential1_.id as id1_2_1_, credential1_.created_date as created_2_2_1_, credential1_.updated_date as updated_3_2_1_, credential1_.created_by as created_4_2_1_, credential1_.last_modified_by as last_mod5_2_1_, credential1_.cloud_provider_id as cloud_pr8_2_1_, credential1_.access_key as access_k6_2_1_, credential1_.owner_id as owner_id9_2_1_, credential1_.secret_key as secret_k7_2_1_, credential1_.virtual_datacenter_id as virtual10_2_1_, cloudprovi2_.id as id1_1_2_, cloudprovi2_.created_date as created_2_1_2_, cloudprovi2_.updated_date as updated_3_1_2_, cloudprovi2_.name as name4_1_2_, user3_.id as id1_11_3_, user3_.created_date as created_2_11_3_, user3_.updated_date as updated_3_11_3_, user3_.created_by as created_4_11_3_, user3_.last_modified_by as last_mod5_11_3_, user3_.active as active6_11_3_, user3_.email as email7_11_3_, user3_.enabled as enabled8_11_3_, user3_.first_name as first_na9_11_3_, user3_.is_using2fa as is_usin10_11_3_, user3_.last_name as last_na11_11_3_, user3_.password as passwor12_11_3_, virtualdat4_.id as id1_13_4_, virtualdat4_.created_date as created_2_13_4_, virtualdat4_.updated_date as updated_3_13_4_, virtualdat4_.created_by as created_4_13_4_, virtualdat4_.last_modified_by as last_mod5_13_4_, virtualdat4_.blueprint_id as blueprin7_13_4_, virtualdat4_.name as name6_13_4_, virtualdat4_.organization_id as organiza8_13_4_, virtualdat4_.owner_id as owner_id9_13_4_, blueprint5_.id as id1_0_5_, blueprint5_.created_date as created_2_0_5_, blueprint5_.updated_date as updated_3_0_5_, blueprint5_.name as name4_0_5_, organizati6_.id as id1_3_6_, organizati6_.created_date as created_2_3_6_, organizati6_.updated_date as updated_3_3_6_, organizati6_.created_by as created_4_3_6_, organizati6_.last_modified_by as last_mod5_3_6_, organizati6_.name as name6_3_6_, organizati6_.user_id as user_id7_3_6_, user7_.id as id1_11_7_, user7_.created_date as created_2_11_7_, user7_.updated_date as updated_3_11_7_, user7_.created_by as created_4_11_7_, user7_.last_modified_by as last_mod5_11_7_, user7_.active as active6_11_7_, user7_.email as email7_11_7_, user7_.enabled as enabled8_11_7_, user7_.first_name as first_na9_11_7_, user7_.is_using2fa as is_usin10_11_7_, user7_.last_name as last_na11_11_7_, user7_.password as passwor12_11_7_ from user_credentials credential0_ inner join credentials credential1_ on credential0_.credential_id=credential1_.id inner join cloud_providers cloudprovi2_ on credential1_.cloud_provider_id=cloudprovi2_.id inner join users user3_ on credential1_.owner_id=user3_.id inner join virtual_datacenters virtualdat4_ on credential1_.virtual_datacenter_id=virtualdat4_.id inner join blueprints blueprint5_ on virtualdat4_.blueprint_id=blueprint5_.id inner join organizations organizati6_ on virtualdat4_.organization_id=organizati6_.id inner join users user7_ on virtualdat4_.owner_id=user7_.id where credential0_.user_id=?
Hibernate: insert into users (created_date, updated_date, created_by, last_modified_by, active, email, enabled, first_name, is_using2fa, last_name, password) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
Hibernate: select user0_.id as id1_11_, user0_.created_date as created_2_11_, user0_.updated_date as updated_3_11_, user0_.created_by as created_4_11_, user0_.last_modified_by as last_mod5_11_, user0_.active as active6_11_, user0_.email as email7_11_, user0_.enabled as enabled8_11_, user0_.first_name as first_na9_11_, user0_.is_using2fa as is_usin10_11_, user0_.last_name as last_na11_11_, user0_.password as passwor12_11_ from users user0_
Hibernate: select user0_.id as id1_11_, user0_.created_date as created_2_11_, user0_.updated_date as updated_3_11_, user0_.created_by as created_4_11_, user0_.last_modified_by as last_mod5_11_, user0_.active as active6_11_, user0_.email as email7_11_, user0_.enabled as enabled8_11_, user0_.first_name as first_na9_11_, user0_.is_using2fa as is_usin10_11_, user0_.last_name as last_na11_11_, user0_.password as passwor12_11_ from users user0_
2019-09-16 13:55:14.886  INFO 21456 --- [           main] o.s.t.c.t.TransactionContext             : Rolled back transaction for test: [DefaultTestContext@7a22302c testClass = Java8RepoTests, testInstance = com.test.api.Java8RepoTests@34a99d8, testMethod = testTwo@Java8RepoTests, testException = java.lang.AssertionError: expected:<1> but was:<2>, mergedContextConfiguration = [WebMergedContextConfiguration@45482f82 testClass = Java8RepoTests, locations = '{}', classes = '{class com.test.api.ApiApplication}', contextInitializerClasses = '[class com.test.api.Java8RepoTests$Initializer]', activeProfiles = '{}', propertySourceLocations = '{}', propertySourceProperties = '{org.springframework.boot.test.context.SpringBootTestContextBootstrapper=true}', contextCustomizers = set[org.springframework.boot.test.autoconfigure.properties.PropertyMappingContextCustomizer@0, org.springframework.boot.test.autoconfigure.web.servlet.WebDriverContextCustomizerFactory$Customizer@5af3afd9, org.springframework.boot.test.context.filter.ExcludeFilterContextCustomizer@5ffead27, org.springframework.boot.test.json.DuplicateJsonObjectContextCustomizerFactory$DuplicateJsonObjectContextCustomizer@1fa268de, org.springframework.boot.test.mock.mockito.MockitoContextCustomizer@0, org.springframework.boot.test.web.client.TestRestTemplateContextCustomizer@222545dc], resourceBasePath = 'src/main/webapp', contextLoader = 'org.springframework.boot.test.context.SpringBootContextLoader', parent = [null]], attributes = map['org.springframework.test.context.web.ServletTestExecutionListener.activateListener' -> true, 'org.springframework.test.context.web.ServletTestExecutionListener.populatedRequestContextHolder' -> true, 'org.springframework.test.context.web.ServletTestExecutionListener.resetRequestContextHolder' -> true]]
2019-09-16 13:55:16.077  INFO 21456 --- [       Thread-9] o.s.s.c.ThreadPoolTaskExecutor           : Shutting down ExecutorService 'applicationTaskExecutor'
2019-09-16 13:55:16.087  INFO 21456 --- [       Thread-9] j.LocalContainerEntityManagerFactoryBean : Closing JPA EntityManagerFactory for persistence unit 'default'

Выходные данные журнала показывают, что транзакция создана и откатывается. Однако данные из testOne все еще находятся в базе данных, когда выполняется testTwo.

Не знаю, куда идти отсюда. Результаты Google показывают некоторые варианты приведенного выше тестового кода, которые я исследовал безрезультатно.


person rboarman    schedule 16.09.2019    source источник
comment
что такое providesFindOneWithOptional   -  person Toerktumlare    schedule 16.09.2019
comment
Для краткости я переименовал ProvidesFindOneWithOptional в testOne. Это просто название тестовой функции. Спасибо, что указали на это.   -  person rboarman    schedule 16.09.2019
comment
трудно понять, что есть что в журнале, если вы переименовываете вещи   -  person Toerktumlare    schedule 16.09.2019
comment
Ваш код не соответствует журналу, вы выбираете, чтобы проверить количество записей, затем вставляете, а затем проверяете количество записей. Это делает его, выберите, вставьте, выберите. так что явно что-то не так в предоставленной вами информации. С другой стороны, транзакция откатывается только при возникновении исключения, поэтому ваш первый тест проходит успешно, и эти данные будут распространяться на второй тест. Лучше всего начинать каждый тест с очистки, затем вставлять его данные, а затем утверждать, что если тест не пройден, вы можете проверить состояние теста и причины его провала. Итак, подчистить, вставить, утвердить.   -  person Toerktumlare    schedule 16.09.2019
comment
@ThomasAndolf Я обновил вывод журнала, чтобы точно показать, что было создано, включая запросы. Я также не думаю, что вы правы насчет отката. В документации Spring (раздел 3.2.3 — Управление транзакциями) указано, что данные будут откатываться после каждого теста. Кроме того, вывод журнала показывает, что откат был инициирован. docs.spring.io/spring/ docs/current/spring-framework-reference/   -  person rboarman    schedule 17.09.2019
comment
вы не указали четко, что хотите, чтобы тесты автоматически откатывались для вас. Есть несколько вещей, которые могут быть неправильными. В документации указано несколько вещей, которые вы должны прочитать Spring Boot provides a @SpringBootTest annotation, which can be used as an alternative to the standard spring-test @ContextConfiguration annotation when you need Spring Boot features. Итак, вы выбираете одну, вы не используете обе.   -  person Toerktumlare    schedule 17.09.2019
comment
Они предоставляют вам полный пример в документах, которые вы должны посмотреть на docs.spring.io/spring/docs/current/spring-framework-reference/   -  person Toerktumlare    schedule 17.09.2019
comment
вы создаете EntityManager вне Spring, хотя и не используете его, вероятно, вы используете его в своем собственном коде. Удалите установку/разборку для EntityManager. Если вам нужно, просто введите его с помощью @PersistenceCOntext, чтобы вы использовали то же, что и транзакция.   -  person M. Deinum    schedule 17.09.2019
comment
Также вы используете MySQL, который по умолчанию использует таблицы MyISAM (если вы позволяете hibernate генерировать таблицы), которые не поддерживают транзакции. См. stackoverflow.com/questions/57904111/ о том, как переключиться на таблицы InnoDB.   -  person M. Deinum    schedule 17.09.2019
comment
@M.Deinum M.Deinum Переключение на InnoDB сработало. Если вы переключите свой комментарий на ответ, я приму его.   -  person rboarman    schedule 17.09.2019
comment
@ThomasAndolf Спасибо за ваши комментарии. Они помогли мне очистить код.   -  person rboarman    schedule 17.09.2019