不想将 DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD 与 TestContainers 一起使用
Don't want to use DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD with TestContainers
我正在对 testcontainers
和 spring-boot
进行集成测试,我在初始化脚本时遇到问题。我有 2 个脚本:schema.sql
和 data.sql
.
当我使用 DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD
时它工作正常,但在每次测试后重新运行一个新容器并不是一个好主意。当然,这会使测试变得非常慢。
另一方面,当我使用 DirtiesContext.ClassMode.AFTER_CLASS
时,我有这个例外:
org.springframework.jdbc.datasource.init.ScriptStatementFailedException:
Failed to execute SQL script statement #1 of class path resource
[sql/mariadb/schema.sql]: DROP TABLE IF EXISTS client
; nested
exception is java.sql.SQLIntegrityConstraintViolationException:
(conn=4) Cannot delete or update a parent row: a foreign key
constraint fails at
org.springframework.jdbc.datasource.init.ScriptUtils.executeSqlScript(ScriptUtils.java:282)
~[spring-jdbc-5.3.13.jar:5.3.13] at ... Caused by:
java.sql.SQLIntegrityConstraintViolationException: (conn=4) Cannot
delete or update a parent row: a foreign key constraint fails ...
Caused by:
org.mariadb.jdbc.internal.util.exceptions.MariaDbSqlException: Cannot
delete or update a parent row: a foreign key constraint fails
基地class:
@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();
}
}
schema.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;
data.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');
我错过了什么?非常欢迎您提出建议。
将执行 @Sql
,默认为 BEFORE_TEST_METHOD
。所以这是每个测试方法之前的运行。换句话说,在任何测试 运行 之前,执行 SQL。但是,当然,通过 re-using 同一个数据库进行多次测试,如果 sql 脚本不是“幂等的”,这可能会 运行 出错,换句话说,如果 sql 脚本不能安全地应用于同一个数据库两次。
要使其生效,有多种可能性,例如:
为 AFTER_TEST_METHOD
添加一个以进行清理。然后,这将基本上删除该测试添加的所有内容(包括之前 @Sql
添加的内容)。这样,您的数据库在每次测试 运行 后都会被“清理”,并且每个 运行 都可以再次安全地应用相同的 sql 脚本。
或者让它安全的执行多次。这取决于脚本,但是如果您可以以允许您 运行 两次而不会出错的方式编写 SQL,那么这也可以。
在不使用 @Sql
的情况下,您还可以在测试配置中配置一个 DatabasePopulator
bean。这样,当创建整个应用程序上下文时,您的 SQL 代码只会 运行 一次。
您也可以尝试在您的测试中使用 @Transactional
,这将使 Spring 围绕您的测试执行包装一个事务,然后将其回滚。不幸的是,我目前不知道这与 @Sql
一起玩得有多好,可能有用,也可能不会,但我认为它有 75% 的工作机会。当然,它有一些含义(实际上最终没有任何内容写入数据库,如果您在代码中使用事务,可能会出现复杂情况)。
所有这些方法可能都会解决您的问题。
我正在对 testcontainers
和 spring-boot
进行集成测试,我在初始化脚本时遇到问题。我有 2 个脚本:schema.sql
和 data.sql
.
当我使用 DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD
时它工作正常,但在每次测试后重新运行一个新容器并不是一个好主意。当然,这会使测试变得非常慢。
另一方面,当我使用 DirtiesContext.ClassMode.AFTER_CLASS
时,我有这个例外:
org.springframework.jdbc.datasource.init.ScriptStatementFailedException: Failed to execute SQL script statement #1 of class path resource [sql/mariadb/schema.sql]: DROP TABLE IF EXISTS
client
; nested exception is java.sql.SQLIntegrityConstraintViolationException: (conn=4) Cannot delete or update a parent row: a foreign key constraint fails at org.springframework.jdbc.datasource.init.ScriptUtils.executeSqlScript(ScriptUtils.java:282) ~[spring-jdbc-5.3.13.jar:5.3.13] at ... Caused by: java.sql.SQLIntegrityConstraintViolationException: (conn=4) Cannot delete or update a parent row: a foreign key constraint fails ... Caused by: org.mariadb.jdbc.internal.util.exceptions.MariaDbSqlException: Cannot delete or update a parent row: a foreign key constraint fails
基地class:
@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();
}
}
schema.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;
data.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');
我错过了什么?非常欢迎您提出建议。
将执行 @Sql
,默认为 BEFORE_TEST_METHOD
。所以这是每个测试方法之前的运行。换句话说,在任何测试 运行 之前,执行 SQL。但是,当然,通过 re-using 同一个数据库进行多次测试,如果 sql 脚本不是“幂等的”,这可能会 运行 出错,换句话说,如果 sql 脚本不能安全地应用于同一个数据库两次。
要使其生效,有多种可能性,例如:
为
AFTER_TEST_METHOD
添加一个以进行清理。然后,这将基本上删除该测试添加的所有内容(包括之前@Sql
添加的内容)。这样,您的数据库在每次测试 运行 后都会被“清理”,并且每个 运行 都可以再次安全地应用相同的 sql 脚本。或者让它安全的执行多次。这取决于脚本,但是如果您可以以允许您 运行 两次而不会出错的方式编写 SQL,那么这也可以。
在不使用
@Sql
的情况下,您还可以在测试配置中配置一个DatabasePopulator
bean。这样,当创建整个应用程序上下文时,您的 SQL 代码只会 运行 一次。您也可以尝试在您的测试中使用
@Transactional
,这将使 Spring 围绕您的测试执行包装一个事务,然后将其回滚。不幸的是,我目前不知道这与@Sql
一起玩得有多好,可能有用,也可能不会,但我认为它有 75% 的工作机会。当然,它有一些含义(实际上最终没有任何内容写入数据库,如果您在代码中使用事务,可能会出现复杂情况)。
所有这些方法可能都会解决您的问题。