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,并不想将整个应用启动
@RestController
public class HealthController {
@RequestMapping("/health")
public String health(){
return "UP";
}
}
测试代码:
@RunWith(SpringRunner.class)
@WebMvcTest(HealthController.class)
public class HealthControllerTest {
@Autowired
private MockMvc mvc;
@Test
public void health() throws Exception {
this.mvc.perform(MockMvcRequestBuilders.get("/health").accept(MediaType.TEXT_PLAIN))
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(MockMvcResultMatchers.content().string("UP"));
}
}
注意
- SpringRunner除了比 SpringJUnit4ClassRunner短一点,其他二者相同
- 如果只想测试 Controller,那么注解中加上@WebMvcTest,并引入要测试的 Controller 即可
- 自动注入 MockMvc,为什么不用 RestTemplate,因为此时应用并没有真正启动,相关端口也没有打开
- MockMvcRequestBuilders 用来构建 http 请求参数,它能做的有很多,这里不展开
- MockMvcResultMatchers 用来做请求结果的数据解析,这里也不展开
场景二:复杂的 Controller
上述示例太过简单,一个普通的 Controller 还应该包括如下元素:
- Service
- 请求参数
- 返回 Json数据
- 甚至某些字段是从 配置中心获取的
@RestController
public class UserController {
@Value("${config.username:bishion}")
private String defaultUser;
@Autowired
private UserService userService;
@RequestMapping("/query")
public List<UserDTO> query(String username){
if(StringUtils.isEmpty(userDTO.getUsername())){
username = defaultUser;
}
List<User> users = userService.queryUser(username);
return transfer(users); // DOList 转 DTOList
}
}
测试代码:
@RunWith(SpringRunner.class)
@WebMvcTest(UserController.class)
public class UserControllerTest {
@Autowired
private MockMvc mvc;
@MockBean
private UserService userService;
@Test
public void query() throws Exception {
List<User> userList = new ArrayList<>(1);
User user = new User();
user.setUsername("bishion");
userList.add(user);
BDDMockito.given(userService.queryUser(null)).willReturn(Collections.emptyList());
BDDMockito.given(userService.queryUser(BDDMockito.startsWith("bi"))).willReturn(userList);
BDDMockito.given(userService.queryUser(BDDMockito.startsWith("${config.username}"))).willReturn(userList);
this.mvc.perform(MockMvcRequestBuilders.get("/query")
.param("username", "bishion"))
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(MockMvcResultMatchers.jsonPath("$.length()", "").value(1));
this.mvc.perform(MockMvcRequestBuilders.get("/query")
.param("username", "guo"))
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(MockMvcResultMatchers.jsonPath("$.length()", "").value(0));
this.mvc.perform(MockMvcRequestBuilders.get("/query"))
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(MockMvcResultMatchers.jsonPath("$.length()", "").value(1));
}
}
注意
- UserController 依赖了 UserService,因此要使用 @MockBean 对该 bean 做 mock
- UserController.query() 调用了 UserService.query() 方法,因此要使用 BDDMockito.given().willReturn() 对该方法做mock
- MockMvcResultMatchers 中使用了 jsonPath,可以对返回值做解析
- 如果 Controller 依赖了外部配置而且没有配置值,那么它直接使用 @Value 注解的值,这里为 ${config.username}
- 如果你想为依赖的外部配置添加测试配置,可以使用 @TestPropertySource 比如:@TestPropertySource(properties = “config.username=test”)
场景三:Service 的单测
需要对一个 Service 做单测,该 Service 调用了其它 Service
@FeignClient(name = "remote-service",url = "https://www.baidu.com")
public interface BaiduService {
@RequestMapping("/")
String request();
}
@Service
public class CallRemoteService {
@Autowired
private BaiduService baiduService;
public String callBaidu() {
String baidu = baiduService.request();
return baidu.substring(2, 4);
}
}
测试代码:
@RunWith(SpringRunner.class)
@Import(CallRemoteService.class)
public class CallRemoteServiceTest {
@MockBean
private BaiduService baiduService;
@Autowired
private CallRemoteService callRemoteService;
@Test
public void callBaidu() {
BDDMockito.given(this.baiduService.request()).willReturn("SUCCESS");
String result = callRemoteService.callBaidu();
Assert.hasLength(result,"返回数据不应该为空");
Assert.isTrue(result.length() == 2,"返回数据长度应为2");
}
}
注意
- 如果需要测试一个 Service, 需要使用 @Import 将该 Service 引入到上下文
- 因为该 Service 调用了别的 Service,所以需要 @MockBean
场景四:mybatis 的单测
针对 dao 层的单测:
- 项目使用了 注解和 XML 两种方式的 mapper
- 单测需要添加 mybatis-spring-boot-starter 依赖
@Mapper
public interface UserDao {
@Insert("insert into User values(null,#{username},#{age})")
@Options(useGeneratedKeys=true,keyColumn = "id")
Integer addUser(User user);
@Select("select * from User where username = #{username}")
List<User> queryUserByName(String username);
// 该方法为 xml 配置
Integer updateUserById(User user);
}
测试代码:
@RunWith(SpringRunner.class)
@MybatisTest
//@Rollback(false)
//@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
public class UserDaoTest {
@Autowired
private UserDao userDao;
@Test
public void testUserDao(){
// 测试插入数据
User user = new User();
user.setUsername("bizi");
user.setAge(18);
userDao.addUser(user);
// 测试更新数据
User newUser = new User();
newUser.setId(user.getId());
newUser.setUsername("bishion");
userDao.updateUserById(newUser);
// 测试查询数据
List<User> userList = userDao.queryUserByName("bishion");
Assert.notNull(user.getId(),"未获取到ID");
Assert.notEmpty(userList,"未查到插入数据");
}
}
注意:
- @AutoConfigureTestDatabase 用于指定是否使用 mybatis 单测内置数据库,默认是使用
- 该测试用例依赖 mybatis-spring-boot-starter-test
- 因为 兼容性 问题,请不要将 @MapperScan 注解放到Application启动类上,否则会报错
- 如果你使用的是内置数据库,需要在 src/test/resources 下面添加 schema.sql,里面放入建表语句
场景五:Feign 的测试
虽然说起来很傻,但是有时候还真的需要单独测试 Feign
@FeignClient(name = "remote-service",url = "https://www.baidu.com")
public interface BaiduService {
@RequestMapping("/")
String request();
}
测试代码:
@RunWith(SpringRunner.class)
@RestClientTest(BaiduService.class)
@ImportAutoConfiguration({RibbonAutoConfiguration.class, FeignRibbonClientAutoConfiguration.class, FeignAutoConfiguration.class})
public class BaiduServiceTest {
@Autowired
private BaiduService baiduService;
@Test
public void request(){
Assert.hasText(baiduService.request(),"未查到数据");
}
}
注意:
- 因为测试 Feign 需要相关的上下文,所以要手动引入
- 需要使用 @RestClientTest 将被测接口加进来
- 个人觉得直接使用 RestTemplate 也挺好
场景六:集成测试
需要将整个项目启动,然后再单测
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.ANY)
public class UserController {
@Autowired
private TestRestTemplate template;
@Test
public void testAddUser(){
UserDTO userDTO = new UserDTO();
userDTO.setUsername("bishion");
String result = template.postForEntity("/addUser",userDTO, String.class).getBody();
Assert.isTrue("SUCCESS".endsWith(result),"返回不成功"+result);
}
@Test
public void testQueryUser(){
MultiValueMap<String, String> map= new LinkedMultiValueMap<>();
map.add("username", "bishion");
List<UserDTO> result = template.postForEntity("/query",map, List.class).getBody();
Assert.isTrue(result.size()>0,"没有查出数据");
}
}
注意
- 如果你之前设置了 schema.sql,这里一定要显式设置 AutoConfigureTestDatabase 为 Replace.ANY
- 有了 @SpringBootTest,可以将 TestRestTemplate 自动注入
总结
- 因为时间仓促,只是简单看了下文档做了个总结,所以很多问题没有深究
- 这里只是列了一些典型场景,后续本文档会更新,放在 https://bishion.github.io/2019/03/16/sprig-boot-test/
- 本文源码放在 https://github.com/bishion/springboot-test.git
- spring boot 关于测试这一块的文档写的太抽象了