data.sql vs @Sql 注释在 Spring 引导中具有不一致的行为

data.sql vs @Sql annotation has inconsistent behaviour in Spring Boot

我注意到一些非常奇怪的东西运行ge。可能就是这么设计的吧​​

我正在 运行针对本地 MySQL 数据库进行集成测试。

我想创建自己的 data.sql 文件以便可以加载数据,并且我想从相同状态的数据库开始。

简而言之,似乎每个测试方法之前的@Sql注释运行s但是data.sql文件在所有测试之前只加载一次运行?

我正在 运行 对将 employeeNumber 作为唯一列的实体 Employee 进行集成测试。

有几个测试尝试插入同一个员工两次,但它们应该会失败。

现在,这就是正在发生的事情。

如果我在 class 上保留一个 @Sql 注释,在它正常工作之前删除所有内容。 如果我依赖没有注释的 data.sql 文件,那么数据库不会清除状态,我会收到重复字段错误。这显然意味着 data.sql 文件没有正确清除状态。

有趣的是,那些失败的相同测试,如果我 运行 之后立即单独测试它们,那么它们就会通过。所以对我来说,正在发生的事情是显而易见的。 data.sql 不是每次测试后 运行ning。

问题是,这是一个错误还是正常现象?

代码如下:

application.properties(在 test/resources 内):

spring.datasource.url=jdbc:mysql://localhost:3306/StraightWallsTest
spring.datasource.username=root
spring.datasource.password=root

# Get rid of JPA Hibernate's counter intuitive confusing & ridiculous underscore when no one asked for it
spring.jpa.hibernate.naming.physical-strategy=org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
# Do not generate mappings by default
spring.jpa.hibernate.use-new-id-generator-mappings=false
# Do not print any stack trace to the client
server.error.include-stacktrace=never
# Do not generate schema
spring.jpa.hibernate.ddl-auto=none

spring.sql.init.mode=always

这是 data.sql 文件。我把所有的删除语句放在最上面,以确保所有内容都被删除。

-- MySQL dump 10.13  Distrib 8.0.28, for macos12.2 (arm64)
--
-- Host: localhost    Database: StraightWallsLocal
-- ------------------------------------------------------
-- Server version   8.0.28

/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
/*!50503 SET NAMES utf8mb4 */;
/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */;
/*!40103 SET TIME_ZONE='+00:00' */;
/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;

--
-- Dumping data for table `Department`
--
DELETE FROM`Employee`;
ALTER TABLE `Employee` AUTO_INCREMENT=1;
DELETE FROM`Department`;
ALTER TABLE `Department` AUTO_INCREMENT=1;
DELETE FROM`Role`;
ALTER TABLE `Role` AUTO_INCREMENT=1;
DELETE FROM `HolidayRequest`;
ALTER TABLE `HolidayRequest` AUTO_INCREMENT=1;
DELETE FROM `Period`;
ALTER TABLE `Period` AUTO_INCREMENT=1;


LOCK TABLES `Department` WRITE;
/*!40000 ALTER TABLE `Department` DISABLE KEYS */;
INSERT INTO `Department` VALUES (1,'Engineering'),(2,'Plumbing'),(3,'Roofing'),(4,'Carpentry'),(5,'Bricklaying'),(6,'Office');
/*!40000 ALTER TABLE `Department` ENABLE KEYS */;
UNLOCK TABLES;

--
-- Dumping data for table `Employee`
--

LOCK TABLES `Employee` WRITE;
/*!40000 ALTER TABLE `Employee` DISABLE KEYS */;
/*!40000 ALTER TABLE `Employee` ENABLE KEYS */;
UNLOCK TABLES;

--
-- Dumping data for table `HolidayRequest`
--

LOCK TABLES `HolidayRequest` WRITE;
/*!40000 ALTER TABLE `HolidayRequest` DISABLE KEYS */;
/*!40000 ALTER TABLE `HolidayRequest` ENABLE KEYS */;
UNLOCK TABLES;

--
-- Dumping data for table `Period`
--

LOCK TABLES `Period` WRITE;
/*!40000 ALTER TABLE `Period` DISABLE KEYS */;
/*!40000 ALTER TABLE `Period` ENABLE KEYS */;
UNLOCK TABLES;

--
-- Dumping data for table `Role`
--

LOCK TABLES `Role` WRITE;
/*!40000 ALTER TABLE `Role` DISABLE KEYS */;
INSERT INTO `Role` VALUES (1,'Deputy Head'),(2,'Head'),(3,'Manager'),(4,'Senior Employee'),(5,'Junior Employee');
/*!40000 ALTER TABLE `Role` ENABLE KEYS */;
UNLOCK TABLES;
/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;

/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;

-- Dump completed on 2022-04-20 23:35:41

测试用例:

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@Sql(statements = {"DELETE FROM Employee; ALTER TABLE Employee AUTO_INCREMENT=1"})
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
@IfProfileValue(name = "spring.profiles.active", value = "test")
public class EmployeeAccountApiIntegratedTest {

    @LocalServerPort
    private int port;

    @Autowired
    private TestRestTemplate rest;

    @Test
    @DisplayName("My First integrated test with Test rest template, seems to be easier than i thought")
    public void whenPostingToDbEverythingWorksAndRealDbDoesNotHit(){

        RegistrationRequest registrationRequest = new RegistrationRequest();
        registrationRequest.setFirstName("Crow");
        registrationRequest.setLastName("Ice");
        registrationRequest.setEmployeeNumber("38476");
        registrationRequest.setPassword("abcdef");
        registrationRequest.setTermsAccepted(true);

        HttpEntity<RegistrationRequest> request = new HttpEntity<RegistrationRequest>(registrationRequest);

        ResponseEntity<String> response = rest.postForEntity(
                "/register",
                request,
                String.class
        );

        assertEquals(HttpStatus.CREATED, response.getStatusCode());
    }


    @Test
    @DisplayName("given employee doesnt exist, when authentication request is made, it should return forbidden even with valid credentials")
    public void shouldReturnForbiddenBecauseEmployeeDoesntExist(){
        HttpHeaders headers = new HttpHeaders();
        headers.set("Content-Type", "application/json");
        headers.set("Accept", "text/plain");

        LoginRequest loginRequestBody = new LoginRequest();
        loginRequestBody.setEmployeeNumber("abcde");
        loginRequestBody.setPassword("abcde");

        HttpEntity<LoginRequest> loginRequest = new HttpEntity<LoginRequest>(loginRequestBody, headers);


        ResponseEntity<String> loginResponse = rest.postForEntity(
                "/login",
                loginRequestBody,
                String.class
        );

        assertEquals(HttpStatus.FORBIDDEN, loginResponse.getStatusCode());
    }

    @Test
    @DisplayName("Given employee exists and valid credentials, when authentication is requested, then it should return status ok/200")
    public void returnsOkayWhenEverythingIsValid(){
        HttpHeaders registerHeaders = new HttpHeaders();
        registerHeaders.set("Content-Type", "application/json");
        registerHeaders.set("Accept", "application/json");

        RegistrationRequest registrationRequest = new RegistrationRequest();
        registrationRequest.setFirstName("Crow");
        registrationRequest.setLastName("Ice");
        registrationRequest.setEmployeeNumber("mnopq");
        registrationRequest.setPassword("abcdef");
        registrationRequest.setTermsAccepted(true);

        HttpEntity<RegistrationRequest> request = new HttpEntity<RegistrationRequest>(registrationRequest, registerHeaders);

        ResponseEntity<String> registrationResponse = rest.postForEntity(
                "/register",
                request,
                String.class
        );

        assertEquals(HttpStatus.CREATED, registrationResponse.getStatusCode());

        HttpHeaders loginHeaders = new HttpHeaders();
        loginHeaders.set("Content-Type", "application/json");

        LoginRequest loginRequestBody = new LoginRequest();
        loginRequestBody.setEmployeeNumber(registrationRequest.getEmployeeNumber());
        loginRequestBody.setPassword(registrationRequest.getPassword());

        HttpEntity<LoginRequest> loginRequest = new HttpEntity<LoginRequest>(loginRequestBody, loginHeaders);

        ResponseEntity<String> loginResponse = rest.postForEntity(
                "/login",
                loginRequestBody,
                String.class
        );

        assertEquals(HttpStatus.OK, loginResponse.getStatusCode());
    }

    @Test
    @DisplayName("Given valid employee number but invalid password, when authentication is requested, then the employee should be forbidden")
    public void returnsForbiddenWhenEmployeeNumberIsInValid(){
        HttpHeaders registerHeaders = new HttpHeaders();
        registerHeaders.set("Content-Type", "application/json");
        registerHeaders.set("Accept", "application/json");

        RegistrationRequest registrationRequest = new RegistrationRequest();
        registrationRequest.setFirstName("Crow");
        registrationRequest.setLastName("Ice");
        registrationRequest.setEmployeeNumber("gh78q");
        registrationRequest.setPassword("abcdef");
        registrationRequest.setTermsAccepted(true);

        HttpEntity<RegistrationRequest> request = new HttpEntity<RegistrationRequest>(registrationRequest, registerHeaders);

        ResponseEntity<String> registrationResponse = rest.postForEntity(
                "/register",
                request,
                String.class
        );

        assertEquals(HttpStatus.CREATED, registrationResponse.getStatusCode());

        HttpHeaders loginHeaders = new HttpHeaders();
        loginHeaders.set("Content-Type", "application/json");

        LoginRequest loginRequestBody = new LoginRequest();
        loginRequestBody.setEmployeeNumber("kdjfi");
        loginRequestBody.setPassword(registrationRequest.getPassword());

        HttpEntity<LoginRequest> loginRequest = new HttpEntity<LoginRequest>(loginRequestBody, loginHeaders);

        ResponseEntity<String> loginResponse = rest.postForEntity(
                "/login",
                loginRequestBody,
                String.class
        );

        assertEquals(HttpStatus.FORBIDDEN, loginResponse.getStatusCode());
    }

    @Test
    @DisplayName("Given an employee already exists, when an attempt is made to register with the same employee number, then an exception should be thrown")
    public void noDuplicateEmployeeNumber(){
        HttpHeaders registerHeaders = new HttpHeaders();
        registerHeaders.set("Content-Type", "application/json");
        registerHeaders.set("Accept", "application/json");

        RegistrationRequest registrationRequest = new RegistrationRequest();
        registrationRequest.setFirstName("Crow");
        registrationRequest.setLastName("Ice");
        registrationRequest.setEmployeeNumber("gh78q");
        registrationRequest.setPassword("abcdef");
        registrationRequest.setTermsAccepted(true);

        HttpEntity<RegistrationRequest> request = new HttpEntity<RegistrationRequest>(registrationRequest, registerHeaders);

        ResponseEntity<String> registrationResponse = rest.postForEntity(
                "/register",
                request,
                String.class
        );

        assertEquals(HttpStatus.CREATED, registrationResponse.getStatusCode());


        HttpHeaders registerHeaders2 = new HttpHeaders();
        registerHeaders2.set("Content-Type", "application/json");
        registerHeaders2.set("Accept", "application/json");

        RegistrationRequest registrationRequest2 = new RegistrationRequest();
        registrationRequest2.setFirstName("Crow");
        registrationRequest2.setLastName("Ice");
        registrationRequest2.setEmployeeNumber("gh78q");
        registrationRequest2.setPassword("abcdef");
        registrationRequest2.setTermsAccepted(true);

        HttpEntity<RegistrationRequest> request2 = new HttpEntity<RegistrationRequest>(registrationRequest, registerHeaders);

        ResponseEntity<String> registrationResponse2 = rest.postForEntity(
                "/register",
                request,
                String.class
        );

        assertEquals(HttpStatus.CONFLICT, registrationResponse2.getStatusCode());

    }

    /**
     * Ideally this should be throwing a 409, But there isnt enough info about this, so leaving it as a genetic error
     */
    @Test
    @DisplayName("Given integrity constraint violation, when employee tries to register, then throw an exception")
    public void returnsHttpConflict(){
        HttpHeaders registerHeaders = new HttpHeaders();
        registerHeaders.set("Content-Type", "application/json");
        registerHeaders.set("Accept", "application/json");

        RegistrationRequest registrationRequest = new RegistrationRequest();
        registrationRequest.setFirstName("Crow");
        registrationRequest.setLastName("Ice");
        registrationRequest.setEmployeeNumber("gh78q");
        registrationRequest.setPassword("abcdef");
        registrationRequest.setTermsAccepted(true);

        HttpEntity<RegistrationRequest> request = new HttpEntity<RegistrationRequest>(registrationRequest, registerHeaders);

        ResponseEntity<String> registrationResponse = rest.postForEntity(
                "/register",
                request,
                String.class
        );

        assertEquals(HttpStatus.CREATED, registrationResponse.getStatusCode());

        ResponseEntity<String> registrationResponse2 = rest.postForEntity(
                "/register",
                request,
                String.class
        );

        assertEquals(HttpStatus.CONFLICT, registrationResponse2.getStatusCode());

    }





}

只是重申一下正在发生的事情。

如果我保留此注释:一切正常

@Sql(statements = {"DELETE FROM Employee; ALTER TABLE Employee AUTO_INCREMENT=1"})

如果我删除该注释,那么 运行 class 中的所有测试我都会得到一些可能使用相同员工编号的方法的重复错误。

这是令人困惑的地方。当我像这样注释掉 @Sql 注释时: 其中两项测试失败

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
//@Sql(statements = {"DELETE FROM Employee; ALTER TABLE Employee AUTO_INCREMENT=1"})
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
@IfProfileValue(name = "spring.profiles.active", value = "test")
public class EmployeeAccountApiIntegratedTest {

有趣的是,如果我进行相同的测试但失败了,并且 运行 该测试方法单独运行:

所以主要问题是,这正常吗,应该是这样的吗?或者这是一个错误? 如果是前者,则应妥善记录。 恕我直言,它不直观且架构不佳

如有任何建议,我们将不胜感激

@Sql 注解有执行阶段:

Sql.ExecutionPhase executionPhase() default Sql.ExecutionPhase.BEFORE_TEST_METHOD;

public static enum ExecutionPhase {
    BEFORE_TEST_METHOD,
    AFTER_TEST_METHOD;

    private ExecutionPhase() {
    }
}

如您所见,默认值为 BEFORE_TEST_METHOD。因此,在每次测试之前,此注释中的 SQL 将是 运行。当您将其注释掉时,您的删除语句将不会像您注意到的那样执行,并且您在特定测试之间有 conflicts/collisions。这是正常的,没有SQL注解,不会执行SQL语句

现在data.sql文件。来自 docs.spring.io:

Script-based DataSource initialization is performed, by default, before any JPA EntityManagerFactory beans are created. schema.sql can be used to create the schema for JPA-managed entities and data.sql can be used to populate it.

EntityManagerFactory 不是为每个测试创建的,而是为每个数据源创建的。因此 data.sql 只执行一次,有点类似于 @BefereClass@BeforeAll。这不是一个错误,这是预期的行为,但是,我同意没有很好的记录。