spring boot 项目常见单测方式

背景

最近组里在推单测,突然想起自己已经三十多年不写单测了,不由心生惭愧。
默默打开 spring boot 官网,看了看相关单测的文档,整理了下自己的需要的一些单测场景。
由于内容比较基础,只管怎么做,不问为什么,出于节省时间的目的,建议写过单测的同学可以不用往下看了。

代码环境

  • spring cloud Edgware.SR5 版本

  • 使用 h2 内置数据库

  • 持久层使用 mybatis 1.3.3

  • spring cloud 组件只使用了 spring cloud config

  • 依赖 spring-boot-starter-test 组件

  • 依赖 mybatis-spring-boot-starter-test 组件

场景一:简单的 Controller

入门场景,测试一个Controller,它没有依赖,没有参数。
我只想测试该 Controller,并不想将整个应用启动

 
  1. @RestController

  2. public class HealthController {

  3. @RequestMapping("/health")

  4. public String health(){

  5. return "UP";

  6. }

  7. }

测试代码:

 
  1. @RunWith(SpringRunner.class)

  2. @WebMvcTest(HealthController.class)

  3. public class HealthControllerTest {

  4. @Autowired

  5. private MockMvc mvc;

  6. @Test

  7. public void health() throws Exception {

  8. this.mvc.perform(MockMvcRequestBuilders.get("/health").accept(MediaType.TEXT_PLAIN))

  9. .andExpect(MockMvcResultMatchers.status().isOk())

  10. .andExpect(MockMvcResultMatchers.content().string("UP"));

  11. }

  12. }

注意

  1. SpringRunner除了比 SpringJUnit4ClassRunner短一点,其他二者相同

  2. 如果只想测试 Controller,那么注解中加上@WebMvcTest,并引入要测试的 Controller 即可

  3. 自动注入 MockMvc,为什么不用 RestTemplate,因为此时应用并没有真正启动,相关端口也没有打开

  4. MockMvcRequestBuilders 用来构建 http 请求参数,它能做的有很多,这里不展开

  5. MockMvcResultMatchers 用来做请求结果的数据解析,这里也不展开

场景二:复杂的 Controller

上述示例太过简单,一个普通的 Controller 还应该包括如下元素:

  • Service

  • 请求参数

  • 返回 Json数据

  • 甚至某些字段是从 配置中心获取的

 
  1. @RestController

  2. public class UserController {

  3. @Value("${config.username:bishion}")

  4. private String defaultUser;

  5. @Autowired

  6. private UserService userService;

  7. @RequestMapping("/query")

  8. public List<UserDTO> query(String username){

  9. if(StringUtils.isEmpty(userDTO.getUsername())){

  10. username = defaultUser;

  11. }

  12. List<User> users = userService.queryUser(username);

  13. return transfer(users); // DOList 转 DTOList

  14. }

  15. }

测试代码:

 
  1. @RunWith(SpringRunner.class)

  2. @WebMvcTest(UserController.class)

  3. public class UserControllerTest {

  4. @Autowired

  5. private MockMvc mvc;

  6. @MockBean

  7. private UserService userService;

  8. @Test

  9. public void query() throws Exception {

  10. List<User> userList = new ArrayList<>(1);

  11. User user = new User();

  12. user.setUsername("bishion");

  13. userList.add(user);

  14. BDDMockito.given(userService.queryUser(null)).willReturn(Collections.emptyList());

  15. BDDMockito.given(userService.queryUser(BDDMockito.startsWith("bi"))).willReturn(userList);

  16. BDDMockito.given(userService.queryUser(BDDMockito.startsWith("${config.username}"))).willReturn(userList);

  17. this.mvc.perform(MockMvcRequestBuilders.get("/query")

  18. .param("username", "bishion"))

  19. .andExpect(MockMvcResultMatchers.status().isOk())

  20. .andExpect(MockMvcResultMatchers.jsonPath("$.length()", "").value(1));

  21. this.mvc.perform(MockMvcRequestBuilders.get("/query")

  22. .param("username", "guo"))

  23. .andExpect(MockMvcResultMatchers.status().isOk())

  24. .andExpect(MockMvcResultMatchers.jsonPath("$.length()", "").value(0));

  25. this.mvc.perform(MockMvcRequestBuilders.get("/query"))

  26. .andExpect(MockMvcResultMatchers.status().isOk())

  27. .andExpect(MockMvcResultMatchers.jsonPath("$.length()", "").value(1));

  28. }

  29. }

注意

  1. UserController 依赖了 UserService,因此要使用 @MockBean 对该 bean 做 mock

  2. UserController.query() 调用了 UserService.query() 方法,因此要使用 BDDMockito.given().willReturn() 对该方法做mock

  3. MockMvcResultMatchers 中使用了 jsonPath,可以对返回值做解析

  4. 如果 Controller 依赖了外部配置而且没有配置值,那么它直接使用 @Value 注解的值,这里为 ${config.username}

  5. 如果你想为依赖的外部配置添加测试配置,可以使用 @TestPropertySource 比如:@TestPropertySource(properties = "config.username=test")

场景三:Service 的单测

需要对一个 Service 做单测,该 Service 调用了其它 Service

 
  1. @FeignClient(name = "remote-service",url = "https://www.baidu.com")

  2. public interface BaiduService {

  3. @RequestMapping("/")

  4. String request();

  5. }

  6. @Service

  7. public class CallRemoteService {

  8. @Autowired

  9. private BaiduService baiduService;

  10. public String callBaidu() {

  11. String baidu = baiduService.request();

  12. return baidu.substring(2, 4);

  13. }

  14. }

测试代码:

 
  1. @RunWith(SpringRunner.class)

  2. @Import(CallRemoteService.class)

  3. public class CallRemoteServiceTest {

  4. @MockBean

  5. private BaiduService baiduService;

  6. @Autowired

  7. private CallRemoteService callRemoteService;

  8. @Test

  9. public void callBaidu() {

  10. BDDMockito.given(this.baiduService.request()).willReturn("SUCCESS");

  11. String result = callRemoteService.callBaidu();

  12. Assert.hasLength(result,"返回数据不应该为空");

  13. Assert.isTrue(result.length() == 2,"返回数据长度应为2");

  14. }

  15. }

注意

  1. 如果需要测试一个 Service, 需要使用 @Import 将该 Service 引入到上下文

  2. 因为该 Service 调用了别的 Service,所以需要 @MockBean

场景四:mybatis 的单测

针对 dao 层的单测:

  1. 项目使用了 注解和 XML 两种方式的 mapper

  2. 单测需要添加 mybatis-spring-boot-starter 依赖

 
  1. @Mapper

  2. public interface UserDao {

  3. @Insert("insert into User values(null,#{username},#{age})")

  4. @Options(useGeneratedKeys=true,keyColumn = "id")

  5. Integer addUser(User user);

  6. @Select("select * from User where username = #{username}")

  7. List<User> queryUserByName(String username);

  8. // 该方法为 xml 配置

  9. Integer updateUserById(User user);

  10. }

测试代码:

 
  1. @RunWith(SpringRunner.class)

  2. @MybatisTest

  3. //@Rollback(false)

  4. //@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)

  5. public class UserDaoTest {

  6. @Autowired

  7. private UserDao userDao;

  8. @Test

  9. public void testUserDao(){

  10. // 测试插入数据

  11. User user = new User();

  12. user.setUsername("bizi");

  13. user.setAge(18);

  14. userDao.addUser(user);

  15. // 测试更新数据

  16. User newUser = new User();

  17. newUser.setId(user.getId());

  18. newUser.setUsername("bishion");

  19. userDao.updateUserById(newUser);

  20. // 测试查询数据

  21. List<User> userList = userDao.queryUserByName("bishion");

  22. Assert.notNull(user.getId(),"未获取到ID");

  23. Assert.notEmpty(userList,"未查到插入数据");

  24. }

  25. }

注意:

  1. @AutoConfigureTestDatabase 用于指定是否使用 mybatis 单测内置数据库,默认是使用

  2. 该测试用例依赖 mybatis-spring-boot-starter-test

  3. 因为 兼容性 问题,请不要将 @MapperScan 注解放到Application启动类上,否则会报错

  4. 如果你使用的是内置数据库,需要在 src/test/resources 下面添加 schema.sql,里面放入建表语句

场景五:Feign 的测试

虽然说起来很傻,但是有时候还真的需要单独测试 Feign

 
  1. @FeignClient(name = "remote-service",url = "https://www.baidu.com")

  2. public interface BaiduService {

  3. @RequestMapping("/")

  4. String request();

  5. }

测试代码:

 
  1. @RunWith(SpringRunner.class)

  2. @RestClientTest(BaiduService.class)

  3. @ImportAutoConfiguration({RibbonAutoConfiguration.class, FeignRibbonClientAutoConfiguration.class, FeignAutoConfiguration.class})

  4. public class BaiduServiceTest {

  5. @Autowired

  6. private BaiduService baiduService;

  7. @Test

  8. public void request(){

  9. Assert.hasText(baiduService.request(),"未查到数据");

  10. }

  11. }

注意:

  1. 因为测试 Feign 需要相关的上下文,所以要手动引入

  2. 需要使用 @RestClientTest 将被测接口加进来

  3. 个人觉得直接使用 RestTemplate 也挺好

场景六:集成测试

需要将整个项目启动,然后再单测

 
  1. @RunWith(SpringRunner.class)

  2. @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)

  3. @AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.ANY)

  4. public class UserController {

  5. @Autowired

  6. private TestRestTemplate template;

  7. @Test

  8. public void testAddUser(){

  9. UserDTO userDTO = new UserDTO();

  10. userDTO.setUsername("bishion");

  11. String result = template.postForEntity("/addUser",userDTO, String.class).getBody();

  12. Assert.isTrue("SUCCESS".endsWith(result),"返回不成功"+result);

  13. }

  14. @Test

  15. public void testQueryUser(){

  16. MultiValueMap<String, String> map= new LinkedMultiValueMap<>();

  17. map.add("username", "bishion");

  18. List<UserDTO> result = template.postForEntity("/query",map, List.class).getBody();

  19. Assert.isTrue(result.size()>0,"没有查出数据");

  20. }

  21. }

注意

  1. 如果你之前设置了 schema.sql,这里一定要显式设置 AutoConfigureTestDatabase 为 Replace.ANY

  2. 有了 @SpringBootTest,可以将 TestRestTemplate 自动注入

总结

  1. 因为时间仓促,只是简单看了下文档做了个总结,所以很多问题没有深究

  2. 这里只是列了一些典型场景,后续本文档会更新,放在 https://bishion.github.io/2019/03/16/sprig-boot-test/

  3. 本文源码放在 https://github.com/bishion/springboot-test.git

  4. spring boot 关于测试这一块的文档写的太抽象了

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值