【问题标题】:Don't want to use DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD with TestContainers不想在 TestContainers 中使用 DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD
【发布时间】:2022-09-30 11:57:59
【问题描述】:

我正在使用 testcontainersspring-boot 进行集成测试,但在初始化脚本时遇到了问题。我有 2 个脚本:schema.sqldata.sql

当我使用DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD时,它工作得很好,但在每次测试后重新运行一个新容器并不是一个好主意。当然,这会使测试变得非常缓慢。

另一方面,当我使用 DirtiesContext.ClassMode.AFTER_CLASS 时,我有这个例外:

org.springframework.jdbc.datasource.init.ScriptStatementFailedException: 执行类路径资源的 SQL 脚本语句 #1 失败 [sql/mariadb/schema.sql]:删除表如果存在client;嵌套的 例外是 java.sql.SQLIntegrityConstraintViolationException: (conn=4) 无法删除或更新父行:外键 约束失败 org.springframework.jdbc.datasource.init.ScriptUtils.executeSqlScript(ScriptUtils.java:282) ~[spring-jdbc-5.3.13.jar:5.3.13] at ... 原因: java.sql.SQLIntegrityConstraintViolationException: (conn=4) 不能 删除或更新父行:外键约束失败... 造成的: org.mariadb.jdbc.internal.util.exceptions.MariaDbSqlException:不能 删除或更新父行:外键约束失败

基类:

@Testcontainers
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@ActiveProfiles(\"it\")
@Sql({\"/sql/mariadb/schema.sql\", \"/sql/mariadb/data.sql\"})
@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS)
public abstract class BaseIntegrationTest implements WithAssertions {

    @Container
    protected static MariaDBContainer<?> CONTAINER = new MariaDBContainer<>(\"mariadb:10.6.5\");

    @Autowired
    protected ObjectMapper mapper;

    @Autowired
    protected WebTestClient webTestClient;
}

集成测试:

class ClientControllerITest extends BaseIntegrationTest {

    @Test
    void integrationTest_For_FindAll() {
        webTestClient.get()
                .uri(ApplicationDataFactory.API_V1 + \"/clients\")
                .exchange()
                .expectStatus().isOk()
                .expectBody(Success.class)
                .consumeWith(result -> {
                    assertThat(Objects.requireNonNull(result.getResponseBody()).getData()).isNotEmpty();
                });
    }

    @Test
    void integrationTest_For_FindById() {
        webTestClient.get()
                .uri(ApplicationDataFactory.API_V1 + \"/clients/{ID}\", CLIENT_ID)
                .exchange()
                .expectStatus().isOk()
                .expectBody(Success.class)
                .consumeWith(result -> {
                    var clients = mapper.convertValue(Objects.requireNonNull(result.getResponseBody()).getData(),
                            new TypeReference<List<ClientDto>>() {
                            });
                    var foundClient = clients.get(0);
                    assertAll(
                            () -> assertThat(foundClient.getId()).isEqualTo(CLIENT_ID),
                            () -> assertThat(foundClient.getFirstName()).isEqualTo(CLIENT_FIRST_NAME),
                            () -> assertThat(foundClient.getLastName()).isEqualTo(CLIENT_LAST_NAME),
                            () -> assertThat(foundClient.getTelephone()).isEqualTo(CLIENT_TELEPHONE),
                            () -> assertThat(foundClient.getGender()).isEqualTo(CLIENT_GENDER_MALE.name())
                    );
                });
    }

    @Test
    void integrationTest_For_Create() {
        var newClient = createNewClientDto();

        webTestClient.post()
                .uri(ApplicationDataFactory.API_V1 + \"/clients\")
                .accept(MediaType.APPLICATION_JSON)
                .bodyValue(newClient)
                .exchange()
                .expectStatus().isOk()
                .expectBody(Success.class)
                .consumeWith(result -> {
                    var clients = mapper.convertValue(Objects.requireNonNull(result.getResponseBody()).getData(),
                            new TypeReference<List<ClientDto>>() {
                            });
                    var createdClient = clients.get(0);
                    assertAll(
                            () -> assertThat(createdClient.getId()).isEqualTo(newClient.getId()),
                            () -> assertThat(createdClient.getFirstName()).isEqualTo(newClient.getFirstName()),
                            () -> assertThat(createdClient.getLastName()).isEqualTo(newClient.getLastName()),
                            () -> assertThat(createdClient.getTelephone()).isEqualTo(newClient.getTelephone()),
                            () -> assertThat(createdClient.getGender()).isEqualTo(newClient.getGender())
                    );
                });
    }

    @Test
    void integrationTest_For_Update() {
        var clientToUpdate = createNewClientDto();
        clientToUpdate.setFirstName(CLIENT_EDITED_FIRST_NAME);

        webTestClient.put()
                .uri(ApplicationDataFactory.API_V1 + \"/clients\")
                .accept(MediaType.APPLICATION_JSON)
                .bodyValue(clientToUpdate)
                .exchange()
                .expectStatus().isOk()
                .expectBody(Success.class)
                .consumeWith(result -> {
                    var clients = mapper.convertValue(Objects.requireNonNull(result.getResponseBody()).getData(),
                            new TypeReference<List<ClientDto>>() {
                            });
                    var updatedClient = clients.get(0);
                    assertAll(
                            () -> assertThat(updatedClient.getId()).isEqualTo(clientToUpdate.getId()),
                            () -> assertThat(updatedClient.getFirstName()).isEqualTo(clientToUpdate.getFirstName()),
                            () -> assertThat(updatedClient.getLastName()).isEqualTo(clientToUpdate.getLastName()),
                            () -> assertThat(updatedClient.getTelephone()).isEqualTo(clientToUpdate.getTelephone()),
                            () -> assertThat(updatedClient.getGender()).isEqualTo(clientToUpdate.getGender())
                    );

                });
    }

    @Test
    void integrationTest_For_Delete() {
        webTestClient.delete()
                .uri(ApplicationDataFactory.API_V1 + \"/clients/{ID}\", CLIENT_ID)
                .exchange()
                 .expectStatus().isOk();
    }
}

架构.sql:

DROP TABLE IF EXISTS `client`;
CREATE TABLE `client` (
  `id` bigint(20) NOT NULL,
  `first_name` varchar(255) DEFAULT NULL,
  `last_name` varchar(255) DEFAULT NULL,
  `gender` varchar(255) DEFAULT NULL,
  `telephone` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

数据.sql

INSERT INTO client (id, first_name, last_name, gender, telephone) VALUES(1, \'XXX\', \'XXX\', \'MALE\', \'XXX-XXX-XXXX\');
INSERT INTO client (id, first_name, last_name, gender, telephone) VALUES(2, \'XXX\', \'XXX\', \'MALE\', \'XXX-XXX-XXXX\');

我错过了什么?一个建议将非常受欢迎。

  • 请注意,这不是特定于 Testcontainers 的问题,而是关于在处理测试污染时使用相同的数据库进行多个测试。

标签: java spring testing mariadb testcontainers


【解决方案1】:

默认情况下,@Sql 将被执行,BEFORE_TEST_METHOD。所以这是在每个测试方法之前运行的。换句话说,在运行任何测试之前,都会执行该 SQL。但是当然,通过重复使用同一个数据库进行多次测试,如果 sql 脚本不是“幂等的”,换句话说,如果 sql 脚本不能安全地两次应用于同一个数据库,这可能会出错.

为了使其发挥作用,有多种可能性,例如:

  1. AFTER_TEST_METHOD 添加一个进行清理。这将基本上删除该测试添加的所有内容(包括之前@Sql 添加的内容)。这样,您的数据库在每次测试运行后都会被“清理”,并且每次运行都可以再次安全地应用相同的 sql 脚本。

  2. 或者让它安全地执行多次。这取决于脚本,但如果您可以编写 SQL 以允许您运行它两次而不会出错,那么这也可以工作。

  3. 如果不使用@Sql,您还可以在测试配置中配置DatabasePopulator bean。这样,您的 SQL 代码只会在创建整个应用程序上下文时运行一次。

  4. 您也可以尝试在您的测试中使用@Transactional,这将使Spring 围绕您的测试执行包装一个事务并在之后将其回滚。不幸的是,我目前不知道这与@Sql 一起玩有多好,可能有效,也可能无效,但我会给它 75% 的工作机会。当然,它有一些含义(实际上最终没有将任何内容写入数据库,如果您在代码中使用事务,则可能会出现复杂情况)。

    所有这些方法都可能解决您的问题。

【讨论】:

  • 感谢您的回复,但我不太明白我必须在这里做什么:So this is done all the time. To make it work, I suggest adding one for the AFTER_TEST_METHOD to cleanup. 我应该创建一个 sql 脚本来清理数据库吗?
  • 我试图让它更清楚一点。
  • 创建清理 sql 脚本或为每个测试类创建一个 schema.sqldata.sql 是一种好习惯,例如:schema_client.sqldata_client.sql
  • 真的取决于您的用例,我没有完美的答案。您可能会尝试的另一种可能性是在您的测试中使用 @Transactional。这样,Spring 将围绕它包装一个事务并在测试后将其回滚,这样就不会“真正”将任何内容写入数据库。不确定@Sql 是如何受到影响的,不过,您必须尝试一下。当然,如果您在内部严重依赖交易,它可能会产生副作用。
【解决方案2】:

答案放在@TestContainer注解中

      // will be shared between test methods
      @Container
      private static final MySQLContainer MY_SQL_CONTAINER = new MySQLContainer();
 
      // will be started before and stopped after each test method
      @Container
      private PostgreSQLContainer postgresqlContainer = new PostgreSQLContainer()
              .withDatabaseName("foo")
              .withUsername("foo")
              .withPassword("secret");

【讨论】:

    猜你喜欢
    • 2021-06-14
    • 2021-03-15
    • 2021-12-22
    • 2020-12-19
    • 2021-06-04
    • 2020-10-14
    • 2022-09-23
    • 2022-12-29
    • 2021-12-01
    相关资源
    最近更新 更多