Spring Boot单元测试

Spring Boot进行单元测试

Posted by yyconstantine on October 23, 2019

Spring Boot进行单元测试


准备工作

  • 引入依赖

    <dependency>
    	<groupId>org.springframework.boot</groupId>
    	<artifactId>spring-boot-starter-test</artifactId>
    	<scope>test</scope>
    </dependency>
    
  • 搭建一个Spring Boot项目环境

  • 对类或方法创建单元测试类,通过Ctrl+Shift+T(win下)或⇧⌘T(Mac下)


Service && Dao 单元测试

创建后的单元测试方法大概长这样👇,我们可以在这里验证dao的结果正确性/service执行逻辑的合理性,这里不再过多描述

@SpringBootTest
@RunWith(SpringRunner.class)
public class AdminDaoTest {
    
    @Autowired
    private AdminDao dao;
    
    @Test
    public void users() {
        List<User> list = dao.userList();
        Assert.assertNotNull(list);
        System.out.println(list);
    }
    
}

Controller单元测试

在对service或dao测试的时候,由于其本身就注入到了Spring容器中,所以我们可以通过@Autowired的方式调用,但controller本身是以HTTP请求的方式调用,所以我们无法直接通过注入的方式实现其调用。在这里,我们使用MockMvc模拟对接口的请求。

  • 一个正常的controller,其中包含GET/POST/PUT/DELETE请求

    @RestController
    @Slf4j
    public class AdminController {
          
        private AdminService adminService;
      
        private RedisUtil redisUtil;
      
    	private PropertyConfig propertyConfig;
      
    	@Autowired
    	public void setAdminService(AdminService adminService) {
    		this.adminService = adminService;
    	}
      
    	@Autowired
    	public void setRedisUtil(RedisUtil redisUtil) {
    		this.redisUtil = redisUtil;
    	}
      
    	@Autowired
    	public void setPropertyConfig(PropertyConfig propertyConfig) {
    		this.propertyConfig = propertyConfig;
    	}
          
        @PutMapping("/pwd")
    	public Result editPwd(@NotNull(message = "用户id不能为空") String globalUserId,
    						  @NotNull(message = "原密码不能为空") String oldPwd,
    						  @NotNull(message = "新密码不能为空") String newPwd) {
    		return adminService.editPwd(globalUserId, MD5Util.getMD5(oldPwd), MD5Util.getMD5(newPwd)) ?
    				Result.success(SuccessEnum.GLOBAL_EDIT_SUCCESS) :
    				Result.error(ErrorEnum.GLOBAL_EDIT_ERROR);
    	}
          
        @DeleteMapping("/quit/{globalUserId}")
    	public Result quit(@NotNull(message = "用户id不能为空") @PathVariable String globalUserId) {
    		// 删除redis对应session
    		redisUtil.del(globalUserId);
    		return Result.success(SuccessEnum.GLOBAL_QUIT_SUCCESS);
    	}
          
        @GetMapping("/resource/{userId}")
    	public Result<List<TreeVO>> resourceList(@NotNull(message = "用户id不能为空") @PathVariable String userId) {
    	    return Result.success(SuccessEnum.GLOBAL_SEARCH_SUCCESS, adminService.resourceList(userId));
    	}
          
        @PostMapping("/user")
    	public Result addUser(@Validated UserDTO userDTO) {
    		return adminService.addUser(userDTO) ?
                    Result.success(SuccessEnum.GLOBAL_ADD_SUCCESS) :
                    Result.error(ErrorEnum.GLOBAL_ADD_ERROR);
    	}
          
    }
    
  • controller对应的单元测试类:

    @SpringBootTest
    @RunWith(SpringRunner.class)
    @Slf4j
    public class AdminControllerTest {
        @Autowired
        private WebApplicationContext context;
      
        private MockMvc mockMvc;
        private MockCookie cookie;
      
        @Before
        public void setupMockMvc() {
            // 初始化MockMvc,用于模拟发送请求
            mockMvc = MockMvcBuilders.webAppContextSetup(context).build();
            // 前置cookie使请求通过拦截器
            cookie = new MockCookie("globalUserId", "12345");
        }
          
        @Test
        public void resourceListByAdmin() throws Exception {
            mockMvc.perform(MockMvcRequestBuilders.get("/resource/12345")
                                                  .cookie(cookie))
                    .andExpect(MockMvcResultMatchers.status().isOk())
                    .andDo(MockMvcResultHandlers.print());
        }
              
        @Test
        public void editPwd() throws Exception {
            String json = "{\"globalUserId\":\"12345\",\"oldPwd\":\"12345\",\"newPwd\":\"678910\"}";
            mockMvc.perform(MockMvcRequestBuilders.put("/pwd")
                                                  .cookie(cookie))
                    .andExpect(MockMvcResultMatchers.status().isOk())
                    .andDo(MockMvcResultHandlers.print());
        }
          
        @Test
        public void quit() throws Exception {
            mockMvc.perform(MockMvcRequestBuilders.delete("/quit/12345")
                    .cookie(cookie))
                    .andExpect(MockMvcResultMatchers.status().isOk())
                    .andDo(MockMvcResultHandlers.print());
        }
          
    }
    
  • 由于POST方法存在多种提交方式,所以我们单拎出来:

    • application/x-www-form-urlencoded,俗称form表单提交

    • @Test
      public void login() throws Exception {
          // 组成提交表单的结构
          UrlEncodedFormEntity form = new UrlEncodedFormEntity(Arrays.asList(
                  new BasicNameValuePair("userName", "admin"),
                  new BasicNameValuePair("password", "123456")
          ), "utf-8");
          // 发送请求
          mockMvc.perform(MockMvcRequestBuilders.post("/login")
                  .cookie(cookie)
                  .content(EntityUtils.toString(form))// 添加表单元素
                  .contentType(MediaType.APPLICATION_FORM_URLENCODED))
                  .andExpect(MockMvcResultMatchers.status().isOk())// 预期返回结果
                  .andDo(MockMvcResultHandlers.print());// 输出返回结果
      }
      
    • application/json,也就是我们用@RequestBody接收的参数

    • @Test
      public void login() throws Exception {
          // 采用组装实体的方式转化json
          UserInfo userInfo = new UserInfo();
          userInfo.setUserName("admin");
          userInfo.setPassword("123456");
          String json = JSONObject.toJSONString(userInfo);
          // 或者你可以直接拼接json串
          String json = "{\"userName\":\"admin\",\"password\":\"123456\"}";
          // 发送请求
          mockMvc.perform(MockMvcRequestBuilders.post("/login")
                  .cookie(cookie)
                  .content(json)
                  .contentType(MediaType.APPLICATION_JSON))
                  .andExpect(MockMvcResultMatchers.status().isOk())// 预期返回结果
                  .andDo(MockMvcResultHandlers.print());// 输出返回结果
      }
      
    • multipart/form-data,表单方式提交流数据,参照Stack Overflow

    • MockMultipartFile file = new MockMultipartFile("data", "dummy.csv",
              "text/plain", "Some dataset...".getBytes());
      MockMultipartHttpServletRequestBuilder builder =
              MockMvcRequestBuilders.fileUpload("/test1/datasets/set1");
      builder.with(new RequestPostProcessor() {
          @Override
          public MockHttpServletRequest postProcessRequest(MockHttpServletRequest request) {
              request.setMethod("PUT");
              return request;
          }
      });
      mvc.perform(builder
              .file(file))
              .andExpect(status().ok());
      
    • 补充:GET提交body
    • mockMvc.perform(MockMvcRequestBuilders.get("/trade/thirdOrder")
                 .content("{\"body\": \"F01cuOb8x7eOPoJEQRDwB0wlWmhj2D7ta573EtYdco97uaZH4tO4n8Gbu1Om6aVgLXPM" +
                         "/hXGXze4ytyzgyfqfA==\",\n" +
                         "    \"key\": \"eFJtqt6Chus7ur50LzHazmkOgw1FW+dXqSgPvu1+XjcliUO/psACPTmbdrWdy8vUm" +
                         "/zXmkEYADsMPPIN6QoehRWCf/9KhiSqvg6XLOwIkHVouKD9EPv0MsuO" +
                         "+swWvp5pCSA4NBI3nfmQPTPwwWNfP7p6MGjrmj5HAOuZSmIhIeA=\",\n" +
                         "    \"organCode\": \"000000\",\n" +
                         "    \"outOrderNo\": \"7891011\",\n" +
                         "    \"sign\": \"EC301FEFF88E548095FB83CEABE654FF\"}")
                 .contentType(MediaType.APPLICATION_JSON_UTF8_VALUE))
                 .andExpect(MockMvcResultMatchers.status().isOk())
                 .andDo(MockMvcResultHandlers.print());
      

一个单元测试的技巧

在做单元测试的时候,由于数据都是开发人员随意指定的,会造成“垃圾数据”,我们就可以在测试类或方法上添加@Transactional注解,测试完即数据回滚

@Test
@Transactional
public void editPwd() throws Exception {
    String json = "{\"globalUserId\":\"12345\",\"oldPwd\":\"12345\",\"newPwd\":\"678910\"}";
    mockMvc.perform(MockMvcRequestBuilders.put("/pwd")
                                          .cookie(cookie))
            .andExpect(MockMvcResultMatchers.status().isOk())
            .andDo(MockMvcResultHandlers.print());
}

给自己埋坑

  • 断言的使用(Assert/AssertThat)
  • jupiter的使用