在 Spring Boot 应用的开发中,为了测试 Filter, 通常要使用 Mock 的方法。 但使用 Mock 作为一个模拟对象机制,割裂了系统组件之间应有的联系,容易造成 Mock 测试通过了,但实际上线确出错的情况,特别是在维护和重构时。 本文介绍一种不使用 Mock 对象测试 Filter 的方法。
本文的环境和案例基于 使用JUnit5和H2数据库实现SpringBoot应用的功能测试,如果不熟悉的读者,可以先参考该文。
构建应用中的 Filter
在很多提供 Restful 服务的系统中,通常通过用户凭证(token)来确认,跟踪用户的身份、合法性及用户权限。对于受保护的URL, 通常需要验证客户端发送的 token, 通常客户端在发起请求时,在 HTTP Request 的 Body 或是 Http Request 的 Header 中携带 token。每个受保护的URL需要首先验证该 token, 然后再确定用户的合法性和权限。因为有很多URL需要加入这个逻辑,为了避免重复代码,所以我们自然想到用 Filter 来完成。 示例代码如下:
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
| @Slf4j @Component public class SecurityFilter implements Filter {
@Autowired private AccountRepos accountRepos;
@Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) servletRequest;
String token = req.getHeader(HEADER_AUTH_TOKEN_KEY);
if (token == null) { throw new BizException(BIZ_LOGIN_FIRSTLY); }
Optional<AccountBean> optionalAccountBean = accountRepos.findByToken(token); if (optionalAccountBean.isPresent()) { AccountBean accountBean = optionalAccountBean.get(); req.setAttribute(ATTR_ACCOUNT_BEAN_KEY, accountBean); filterChain.doFilter(servletRequest, servletResponse); } else { throw new BizException(BIZ_LOGIN_FIRSTLY); } } }
|
在通常的 Web 应用中,当验证不通过时,通常强制跳转到登录页面,但 Restful 服务应用中,我们直接返回错误信息。
构建测试服务和测试数据
因为不使用 Mock 对象,所以我们选哟构造一个专门用于测试的服务,为了不让构造的测试服务发布到生产环境中,我们将测试服务构造到 src/test/java 目录下。 在 src/test/java 中新建包: filter, 然后在包中新建一个 RestController。 代码如下:
1 2 3 4 5 6 7 8 9
| @RestController @RequestMapping("/v1/api/auth") public class FliterTestController {
@PostMapping("/test") public String testFilter() { return "{\"res\":\"ok\"}"; } }
|
在测试方法中,我们简单的返回要给 json 串。 当然,也可以返回定义好的实体对象。
在 data.sql 文件中加入测试数据,如下:
1
| insert into Account_Bean (id, name, pwd, token) values (2, 'jack', '123', 'token-001');
|
编写测试方法
测试方法与测试 RestController 的一致。比如要测试 token 错误的情况,可以这样写:
1 2 3 4 5 6 7 8 9 10 11 12 13
| @Test public void testAuthFilterWithNotExistToken() {
HttpHeaders headers = new HttpHeaders(); headers.set(Constants.HEADER_AUTH_TOKEN_KEY, "token-xxx");
HttpEntity request = new HttpEntity(headers);
ApiResult res = this.restTemplate.postForObject("http://localhost:" + port + "/v1/api/auth/test", request, ApiResult.class); assertNotNull(res); assertFalse(res.isSucc());
}
|
测试没有 Header 中没有带 token 情况可以写成:
1 2 3 4 5 6 7 8
| @Test public void testAuthFilterWithNoToken() {
ApiResult res = this.restTemplate.postForObject("http://localhost:" + port + "/v1/api/auth/test", null, ApiResult.class); assertNotNull(res); assertFalse(res.isSucc());
}
|
测试正常 token 的情况可以这样测:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| @Test public void testAuthFilter() {
HttpHeaders headers = new HttpHeaders(); headers.set(Constants.HEADER_AUTH_TOKEN_KEY, "token-001");
HttpEntity request = new HttpEntity(headers);
ApiResult res = this.restTemplate.postForObject("http://localhost:" + port + "/v1/api/auth/test", request, ApiResult.class);
assertNotNull(res); assertTrue(res.isSucc());
}
|
需要注意的是,因为测试服务是写在 src/test/java 中的,所以需要在 src/test/java 中增加一个使用了@SpringBootApplication注解的类来加载 RestController
该类很简单,如下:
1 2 3
| @SpringBootApplication public class ElearningApiApplicationTestRun { }
|
在测试类中,需要在@SpringBootTest 注解中增加对 ElearningApiApplicationTestRun 的引用。如下:
1 2 3 4 5 6 7 8 9 10 11
| @ExtendWith(SpringExtension.class) @SpringBootTest(classes = {ElearingApiApplication.class, ElearningApiApplicationTestRun.class}, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) public class SecurityFilterTest {
@LocalServerPort private int port;
@Autowired private TestRestTemplate restTemplate;
....
|