在实施每日构建的开发团队中,通常都会要求进行自动化的单元测试和功能测试。在数据库类型的应用中,功能测试的难点就是对数据的准备。本文介绍一种通过H2 内存数据库来进行功能测试的方法。
环境说明
在本示例中,分别采用了如下一些软件版本:
Spring Boot: 2.2.5
JUnit: 5.2.0
案例以一个简单的登录服务(Rest Service)为案例。 用户提供用户名和密码,如果验证成功,服务放回登录凭证(token), 如果是非法yoghurt,则返回错误信息。
Spring Boot 服务的基本构建可以参考 Spring Boot 构建Rest服务实验手册(一),本文主要展示项目构建好以后如何借助H2数据库实施自动化的功能测试。
准备依赖库
因为要使用 H2 作为测试库,所以需要在项目中添加 H2 的依赖项, 在 pom.xml 文件中加入:
1 2 3 4 5
| <dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> <scope>runtime</scope> </dependency>
|
在 Spring Boot 2.2.5 版本中生成的 pom.xml 文件中,默认是使用 JUnit 4 的版本,要替换为 JUnit 5,需要做如下的改动。
- 排除(exclusions) spring-boot-starter-test 中已有的 JUnit4
1 2 3 4 5 6 7 8 9 10 11
| <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> <exclusions> <exclusion> <groupId>junit</groupId> <artifactId>junit</artifactId> </exclusion> </exclusions> </dependency>
|
- 添加 JUnit 5的以来项
1 2 3 4 5 6 7 8 9 10 11 12
| <dependency> <groupId>org.junit.jupiter</groupId> <artifactId>junit-jupiter-api</artifactId> <version>5.2.0</version> <scope>test</scope> </dependency> <dependency> <groupId>org.junit.jupiter</groupId> <artifactId>junit-jupiter-engine</artifactId> <version>5.2.0</version> <scope>test</scope> </dependency>
|
- 为了能在 Maven 命令行中执行(每日构建中需要)测试,需要添加正确的 plugin:
1 2 3 4 5
| <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-surefire-plugin</artifactId> <version>2.22.0</version> </plugin>
|
准备 H2 测试数据库和测试数据
- 在 src/test/resources 中添加名为 application.yml 的文件,内容如下:
1 2 3 4 5 6 7 8 9 10 11 12
| spring: datasource: driverClassName: org.h2.Driver url: jdbc:h2:mem:testdb username: sa password: jpa: database: H2 generate-ddl: true hibernate: dialect: org.hibernate.dialect.H2Dialect ddl-auto: create-drop
|
该配置指定在测试中使用内存数据库,这样可以比较简单的处理测试数据。
- 在 data.sql 文件中准备数据。
在 src/test/resources 中添加名为: data.sql 的文件。使用 SQL 语句构建测试数据,比如在本例中就可以简单的加入如下的 SQL 语句:
1
| insert into Account_Bean (id, name, pwd) values (1, 'tom', '123');
|
对应的实体对象为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| @Data @AllArgsConstructor @NoArgsConstructor @Entity public class AccountBean {
@Id @GeneratedValue(strategy= GenerationType.AUTO) private Integer id;
private String name;
private String pwd;
private String token;
}
|
编写测试案例
对于登录功能,我们在Web服务层实现如下代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| @RestController @RequestMapping("/v1/api/sec") public class SecurityApi {
@Autowired private SecurityBiz securityBiz;
@PostMapping("/login") public LoginResult login(@RequestBody LoginForm loginForm) {
LoginResult res = new LoginResult();
String token = securityBiz.loginWithNameAndPassword(loginForm.getName(), loginForm.getPwd()); res.setToken(token);
return res; } }
|
注意: 这里返回的 LoginResult 对象会被拦截器统一转换为 ApiResult 的格式
正确登录的案例
在 src/test/java 相应的包中添加测试类 SecurityApiTest: 代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| @ExtendWith(SpringExtension.class) @SpringBootTest(classes = ElearingApiApplication.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) public class SecurityApiTest {
@LocalServerPort private int port;
@Autowired private TestRestTemplate restTemplate;
@Test public void testLogin() throws Exception {
LoginForm loginForm = new LoginForm(); loginForm.setName("tom"); loginForm.setPwd("123");
ApiResult res = this.restTemplate.postForObject("http://localhost:" + port + "/v1/api/sec/login", loginForm, ApiResult.class);
assertNotNull(res); assertTrue(res.isSucc()); } }
|
可以看到,我们用 JUnit5 中新的 ExtendWith 代替了 JUnit 4 中的 RunWith, 并使用了 SpringBoot 中的 SpringExtension 类。
在测试案例中,我们没有使用 Mock 对象,而是通过 TestRestTemplate 对象直接访问启动的 Spring Boot 服务, 服务的执行也是直接通过原有的代码访问数据库,最大限度的模拟的用户的操作。
验证是否将 token 记录在数据库中
在真是的系统中,不但要将生成的用户凭证(token)发给用户,也需要将 token 写入到数据库中,对于写入的结果,我们也可以在案例中进行测试。修改测试代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| @Autowired private AccountRepos accountRepos;
@Test public void testLogin() throws Exception {
LoginForm loginForm = new LoginForm(); loginForm.setName("tom"); loginForm.setPwd("123");
ApiResult res = this.restTemplate.postForObject("http://localhost:" + port + "/v1/api/sec/login", loginForm, ApiResult.class);
assertNotNull(res); assertTrue(res.isSucc());
Optional<AccountBean> opAccountBean = accountRepos.findById(1); assertTrue(opAccountBean.isPresent()); if (opAccountBean.isPresent()) { AccountBean accountBean = opAccountBean.get(); assertNotNull(accountBean.getToken()); } }
|
可以看到,我们增加了 AccountBean 对于的数据访问对象 AccountRepos, 用来直接操作数据库。然后再代码中通过 findById 的方式找到数据库中的记录,并验证 token 是否存在。当然也可以进一步验证 Token 值。
测试登录不成功的情况
用以下代码,可以验证当输入的密码错误时的情况。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| @Test public void testLoginWithIncorrectPwd() throws Exception {
LoginForm loginForm = new LoginForm(); loginForm.setName("tom"); loginForm.setPwd("xxx");
ApiResult res = this.restTemplate.postForObject("http://localhost:" + port + "/v1/api/sec/login", loginForm, ApiResult.class);
assertNotNull(res); assertFalse(res.isSucc()); assertEquals("SEC-001", res.getCode()); assertNull(res.getData());
}
|
与正确的情况相比,主要是验证结果为 False,错误码为特定的,定义好的错误码。