系列目录
创建数据库 在上一次提到的 springbootdemo
数据库中添加一个表 users
,并插入两条测试数据:
1 2 3 4 5 6 7 8 9 10 11 12 create table users ( id int auto_increment, email varchar (100 ) not null , password varchar (100 ) not null , name varchar (100 ) not null , constraint User_email_uindex unique (email), constraint User_id_uindex unique (id ) ); INSERT INTO springtest.users (id , email, password , name ) VALUES (1 , 'lyh543@outlook.com' , '123456' , 'lyh543' );INSERT INTO springtest.users (id , email, password , name ) VALUES (2 , 'test@example.com' , 'test' , 'test' );
简单写一个获取用户信息的 API 编写一个 User(模型层),并用 IDEA 自动生成构造函数、Getters 和 Setters:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 public class User { private long id; private String email, password, name; public User (int id, String email, String password, String name) { this .id = id; this .email = email; this .password = password; this .name = name; } public User (String email, String password, String name) { this .email = email; this .password = password; this .name = name; } public long getId () { return id; } public void setId (long id) { this .id = id; } public String getEmail () { return email; } public void setEmail (String email) { this .email = email; } public String getPassword () { return password; } public void setPassword (String password) { this .password = password; } public String getName () { return name; } public void setName (String name) { this .name = name; } }
编写一个 UserMapper(数据访问层):
1 2 3 4 5 6 7 @Repository public interface UserMapper { @Select ("SELECT * FROM users WHERE email = #{email}" ) User getByEmail (@Param("email" ) String email) ; }
编写一个 UserService(业务逻辑层):
1 2 3 4 5 6 7 8 9 10 11 @Repository public class UserService { @Autowired UserMapper userMapper; public User getByEmail (String email) { return userMapper.getByEmail(email); } }
最后是 UserController(视图层):
1 2 3 4 5 6 7 8 9 10 11 @RestController public class UserController { @Autowired UserService userService; @GetMapping ("/api/user/1" ) public User getUserInfo () { return userService.getByEmail("lyh543@outlook.com" ); } }
安装命令行工具 httpie,然后 http http://localhost:8080/api/user/1
:
1 2 3 4 5 6 7 8 $ sudo apt install httpie $ http http://localhost:8080/api/user/1 { "id" : 1, "email" : "lyh543@outlook.com" , "password" : "123456" , "name" : "lyh543" }
测试! 每次写完一个 API 都得运行好几次 httpie,太麻烦了。有没有运行代码、每次修改以后自动发送 HTTP 请求测试之前的 API 的工具呢?有!那就是测试!
Spring Boot 项目常见的测试形式有单元测试和集成测试。单元测试 是对每一层 (Mapper, Service, Controller) 进行测试;而像我们这种发送 HTTP 请求调用 API、需要集成所有层的测试,叫做集成测试 。
Spring Boot 单元测试可以参考 SpringBoot Test 人类使用指南- 知乎 。
添加测试依赖 我们添加以下依赖:
1 2 3 4 5 6 7 8 9 10 <dependency > <groupId > org.hsqldb</groupId > <artifactId > hsqldb</artifactId > <scope > test</scope > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-test</artifactId > <scope > test</scope > </dependency >
HSQLDB 是一个小型嵌入式数据库,我们测试的时候使用 HSQLDB,开发和测试时就不会使用同一个数据库。
需要注意的是,MySQL 默认隔离级别为可重复读,HSQLDB 不支持可重复读、默认为读提交,因此在测试的时候可能无法使用事务。
配置测试数据库 安装了 HSQLDB 依赖,我们还要进行配置,告诉 Spring Boot 在测试的时候使用 HSQLDB 而不是 MySQL。修改 /src/test/resources/application.yaml
:
1 2 3 4 5 6 spring: datasource: url: jdbc:hsqldb:mem:testdb;sql.syntax_mys=true;DB_CLOSE_DELAY=-1 username: sa password:
测试的时候还需要执行生成表结构的 SQL,Spring 也提供了接口(文档 ),在测试前会依次执行 /src/test/resource/schema.sql
和 /src/test/resource/schema.sql
。于是我们写一个 schema.sql
:
1 2 3 4 5 6 7 8 9 10 create table if not exists users ( id int auto_increment, email varchar (100 ) not null , password varchar (100 ) not null , name varchar (100 ) not null , constraint User_email_uindex unique (email), constraint User_id_uindex unique (id ) );
编写第一个测试
参考:Getting Started | Testing the Web Layer 测试 | docs.spring.io SpringBoot Test 人类使用指南- 知乎
编写一个测试类 test/UserTest.java
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 package com.lyh543.springbootdemo.test;import org.junit.jupiter.api.Test;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.boot.test.context.SpringBootTest;import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;import org.springframework.boot.test.web.client.TestRestTemplate;import org.springframework.boot.web.server.LocalServerPort;import org.springframework.transaction.annotation.Transactional;import static org.junit.jupiter.api.Assertions.*;@SpringBootTest (webEnvironment = WebEnvironment.RANDOM_PORT)public class UserTest { @Autowired private TestRestTemplate restTemplate; @Test void test () { assertNull(restTemplate.getForObject("/api/user/1" , String.class )) ; } }
编写完以后,有三个方法运行这个测试:
点击 IDEA test
左边的绿色箭头,可以运行这一个测试函数
点击 IDEA UserTest
左边的绿色箭头,可以运行这一个类的测试
运行 mvn test
命令,或点击右边的 Maven test
,可以运行整个项目的测试。
可以看到测试成功通过,因为我们数据库里什么都没有,所以页面什么也没返回。
我们使用 MyBatis Plus 往数据库塞一点数据,然后再测试。
编写第二个测试 由于测试需要会向空数据插入数据,所以我们完善一下 UserMapper
,加一个 insert
:
1 2 3 4 5 6 7 8 9 10 11 @Repository public interface UserMapper { @Select ("SELECT * FROM users WHERE email = #{email}" ) User getByEmail (@Param("email" ) String email) ; @Insert ("INSERT INTO users (email, password, name) VALUES (#{email}, #{password}, #{name})" ) @Options (useGeneratedKeys=true , keyProperty = "id" ) int insert (User user) ; }
然后我们就可以编写第二个测试函数了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 @SpringBootTest (webEnvironment = WebEnvironment.RANDOM_PORT)public class UserTest { @LocalServerPort private int port; @Autowired private TestRestTemplate restTemplate; @Autowired private UserMapper userMapper; @Test void test () { assertNull(restTemplate.getForObject("/api/user/1" , String.class )) ; } @Test public void test2 () { List<Long> userIds = new ArrayList<>(); assertNull(restTemplate.getForObject("/api/user/1" , String.class )) ; for (int i = 0 ; i < 10 ; i++) { User user = new User("lyh543@outlook.com" + Math.random(), "123456" , "lyh543" ); userMapper.insert(user); userIds.add(user.getId()); } for (Long i : userIds) { assertTrue(restTemplate .getForObject("/api/user/" + i, String.class ) .contains("lyh543")); } assertNull(restTemplate.getForObject("/api/user/" + (Collections.min(userIds) - 1 ), String.class )) ; assertNull(restTemplate.getForObject("/api/user/" + (Collections.max(userIds) + 1 ), String.class )) ; assertEquals(400 , restTemplate .getForEntity("/api/user/lyh543" , String.class ) .getStatusCodeValue ()) ; } }
运行测试类 / mvn test
,发现两个测试均成功。
自动清理数据库 上面的测试虽然成功运行了,但是好像有点问题:test2
向数据库插入了数据而没有清理。
如果我们的 test
测试是在 test2
后进行的,就会出错。(试试交换两个函数名以交换其执行顺序)
一个解决方案是使用 @Order
指定测试类中每个测试函数的顺序,但是这还需要考虑到不同测试类对数据库造成的影响。
另一个解决方案是事务。但是这种方法目前挂掉了,所以先想想别的办法吧。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 package com.lyh543.springbootdemo.test;import static org.junit.jupiter.api.Assertions.*;import com.lyh543.springbootdemo.entity.User;import com.lyh543.springbootdemo.mapper.UserMapper;import org.junit.jupiter.api.Test;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.boot.test.context.SpringBootTest;import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;import org.springframework.boot.test.web.client.TestRestTemplate;import org.springframework.boot.web.server.LocalServerPort;import org.springframework.http.HttpEntity;import org.springframework.http.ResponseEntity;import org.springframework.transaction.annotation.Transactional;@Transactional @SpringBootTest (webEnvironment = WebEnvironment.RANDOM_PORT)public class UserTest { @LocalServerPort private int port; @Autowired private TestRestTemplate restTemplate; @Autowired private UserMapper userMapper; @Test public void test () { assertNull(restTemplate.getForObject("/api/user/1" , String.class )) ; } @Test public void test2 () { List<Long> userIds = new ArrayList<>(); assertNull(restTemplate.getForObject("/api/user/1" , String.class )) ; for (int i = 0 ; i < 10 ; i++) { User user = new User("lyh543@outlook.com" + Math.random(), "123456" , "lyh543" ); userMapper.insert(user); userIds.add(user.getId()); } for (Long i : userIds) { assertTrue(restTemplate .getForObject("/api/user/" + i, String.class ) .contains("lyh543")); } assertNull(restTemplate.getForObject("/api/user/" + (Collections.min(userIds) - 1 ), String.class )) ; assertNull(restTemplate.getForObject("/api/user/" + (Collections.max(userIds) + 1 ), String.class )) ; assertEquals(400 , restTemplate .getForEntity("/api/user/lyh543" , String.class ) .getStatusCodeValue ()) ; } }
第三个解决方案,是在每次函数执行完成以后手动清空数据库。我们当然不必在每个测试函数以后都加两行代码,直接使用 @AfterEach
就行。
1 2 3 4 5 6 import static org.springframework.test.jdbc.JdbcTestUtils.*;@AfterEach public void clearDatabase () { deleteFromTables(jdbcTemplate, "users" ); }
Spring 在JdbcTestUtils
中提供了五个好用的静态函数 ,这里我们使用 deleteFromTables
一键清空表。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 import static org.junit.jupiter.api.Assertions.*;import static org.springframework.test.jdbc.JdbcTestUtils.*;@SpringBootTest (webEnvironment = WebEnvironment.RANDOM_PORT)public class UserTest { @Autowired private JdbcTemplate jdbcTemplate; @Autowired private TestRestTemplate restTemplate; @Autowired private UserMapper userMapper; @AfterEach public void clearDatabase () { deleteFromTables(jdbcTemplate, "users" ); } @Test public void test () { assertNull(restTemplate.getForObject("/api/user/1" , String.class )) ; } @Test public void test2 () { List<Long> userIds = new ArrayList<>(); assertNull(restTemplate.getForObject("/api/user/1" , String.class )) ; for (int i = 0 ; i < 10 ; i++) { User user = new User("lyh543@outlook.com" + Math.random(), "123456" , "lyh543" ); userMapper.insert(user); userIds.add(user.getId()); } for (Long i : userIds) { assertTrue(restTemplate .getForObject("/api/user/" + i, String.class ) .contains("lyh543")); } assertNull(restTemplate.getForObject("/api/user/" + (Collections.min(userIds) - 1 ), String.class )) ; assertNull(restTemplate.getForObject("/api/user/" + (Collections.max(userIds) + 1 ), String.class )) ; assertEquals(400 , restTemplate .getForEntity("/api/user/lyh543" , String.class ) .getStatusCodeValue ()) ; } @Test public void test3 () { assertEquals(0 , countRowsInTable(jdbcTemplate, "users" )); } }
三个测试都能成功通过。
重构测试类 注意到 UserTest
有非常多的重复代码,于是我们简单重构一下:
将判断请求的状态码封装为 assertStatusCodeEquals
;
将 @Autowired
放到父类,这样所有的子类就不用再写;
将清空数据库的 clearDatabase
也放到父类。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 package com.lyh543.springbootdemo.utils;import com.lyh543.springbootdemo.mapper.UserMapper;import org.junit.jupiter.api.AfterEach;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.boot.test.web.client.TestRestTemplate;import org.springframework.jdbc.core.JdbcTemplate;import static org.junit.jupiter.api.Assertions.*;import static org.springframework.test.jdbc.JdbcTestUtils.*;public abstract class TestTemplate { @Autowired protected JdbcTemplate jdbcTemplate; @Autowired protected TestRestTemplate restTemplate; @Autowired protected UserMapper userMapper; public void assertStatusCodeEquals (int expected, String url) { assertEquals(expected, restTemplate .getForEntity(url, String.class ) .getStatusCodeValue ()) ; } @AfterEach public void clearDatabase () { deleteFromTables(jdbcTemplate, "users" ); } }
重构后的 UserTest 类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 package com.lyh543.springbootdemo.test;import com.lyh543.springbootdemo.entity.User;import com.lyh543.springbootdemo.utils.TestTemplate;import org.junit.jupiter.api.Test;import org.springframework.boot.test.context.SpringBootTest;import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;import java.util.ArrayList;import java.util.Collections;import java.util.List;import static org.junit.jupiter.api.Assertions.assertEquals;import static org.junit.jupiter.api.Assertions.assertTrue;import static org.springframework.test.jdbc.JdbcTestUtils.countRowsInTable;@SpringBootTest (webEnvironment = WebEnvironment.RANDOM_PORT)public class UserTest extends TestTemplate { @Test public void test () { assertStatusCodeEquals(404 , "/api/user/1" ); } @Test public void test2 () { assertStatusCodeEquals(404 , "/api/user/1" ); List<Long> userIds = new ArrayList<>(); for (int i = 0 ; i < 10 ; i++) { User user = new User("lyh543@outlook.com" + Math.random(), "123456" , "lyh543" ); userMapper.insert(user); userIds.add(user.getId()); } for (Long i : userIds) assertTrue(restTemplate.getForObject("/api/user/" + i, String.class).contains("lyh543")); for (long i: new long []{-1 , 0 , Collections.min(userIds) - 1 , Collections.max(userIds) + 1 }) assertStatusCodeEquals(404 , "/api/user/" + i); assertStatusCodeEquals(400 , "/api/user/lyh543" ); } @Test public void test3 () { assertEquals(0 , countRowsInTable(jdbcTemplate, "users" )); } }
最后更新时间:2021-08-30 16:46:32