Spring MVC服务端单元测试工具MockMvc

2022-04-06 15:48:43 浏览数 (1)

什么是单元测试?

是指对软件中的最小可测试单元进行检查和验证;作为后台开发,我们对外提供的每一个RESTful API就是一个最小的可测试单元,为了确保可用性,我们在接口对外提供服务之前要尽可能的保证接口是按预期的要求在执行,因此,单元测试就是开发过程中必不可少的一项工作;完善的单元测试技能快速定位开发过程中的BUG,同时也可以减少因为BUG导致对接过程带来的大量人员沟通所消耗的时间成本。当需要持续性完善及优化代码的时候,一个好的单元测试用例能够帮助我们快速的确认修改是否对预期产生影响。

单元测试的方式
  • 浏览器测试;当我们开发好一个接口,如:/user/1,那我们就可以在浏览器中输入:http://127.0.0.1/order/1 看是否能得到我们期望的结果;这种方式的特点就是简单,缺点是只能测试GET接口;
  • PostMan;市面上很多类似的工具,功能强大,简单好用;缺点是可配置性较弱;
  • MockMvc;Spring MVC服务端测试工具,功能强大,灵活性更强,可配置性更强,更有利于调整或成功之后的功能确认;缺点是需要在开发的过程中多花一点点时间去写测试用例(个人觉得这个时间消耗是会在后续的便捷操作中还给你的)。
示例
基础项目
  • 创建Spring Boot项目
  • pom.xml
代码语言:javascript复制
<dependency>
 <groupId>org.springframework.boot</groupId>
 <artifactId>spring-boot-starter</artifactId>
</dependency>

<dependency>
 <groupId>org.springframework.boot</groupId>
 <artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
 <groupId>org.springframework.boot</groupId>
 <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>

<dependency>
 <groupId>mysql</groupId>
 <artifactId>mysql-connector-java</artifactId>
</dependency>

<dependency>
 <groupId>org.projectlombok</groupId>
 <artifactId>lombok</artifactId>
</dependency>

<dependency>
 <groupId>org.springframework.boot</groupId>
 <artifactId>spring-boot-starter-test</artifactId>
 <scope>test</scope>
 <exclusions>
 <exclusion>
 <groupId>org.junit.vintage</groupId>
 <artifactId>junit-vintage-engine</artifactId>
 </exclusion>
 </exclusions>
</dependency>
  • 用于测试的RESTful API服务;一个基础的对用户进行增删改查的API服务
代码语言:javascript复制
@Data
@AllArgsConstructor
public class User {
 private Integer id;

 private String username;

 private String nickName;

 private Integer age;

 private String password;
}

import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.web.PageableDefault;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*;

import java.util.ArrayList;
import java.util.List;

/**
 * @author lupf
 * @date 2020/7/13 9:16
 * @desc
 */
@RestController
@RequestMapping ("/user")
@Slf4j
public class UserController {

 /**
     * 等价于 @RequestMapping (value = "user", method = RequestMethod.GET)
     *
     * @param name
     * @param pageable
     * @return
     * @RequestParam 用于映射请求参数
     * @PageableDefault 用于配置默认的分页数据
     */
 @GetMapping
 public List<User> getUserByName(@ApiParam (value = "用户名") @RequestParam ("username") String name,
 @PageableDefault (page = , size = , sort = {"age"}, direction = Sort.Direction.DESC) Pageable pageable) {
        log.info("username:{}", name);
        log.info("pageable.getPageSize():{}", pageable.getPageSize());
        log.info("pageable.getPageNumber():{}", pageable.getPageNumber());
        log.info("pageable.getSort():{}", pageable.getSort());
        User user = new User(, name, "xiaoxx", , "123456");
        List<User> users = new ArrayList<>();

        users.add(user);
 return users;
    }

 /**
     * 根据ID获取用户的详细信息
     *
     * @param id
     * @return
     */
 @GetMapping ("/{id:\d }")
 public User getUserInfoById(@PathVariable Integer id) {
        log.info("username:{}", id);
        User user = new User(, "zhangsan", "xiaoxx", , "123456");
 return user;
    }

 /**
     * 添加用户信息
     * Spring会将请求中content中的json对象转换为一个User对象
     *
     * @param user
     */
 @PostMapping
 public void addUser(@RequestBody User user) {
        log.info("user:{}", user);
    }

 /**
     * 根据用户ID修改用户数据
     *
     * @param id   修改的用户对应的ID
     * @param user 待修改的用户信息
     */
 @PutMapping ("/{id:\d }")
 public void update(@PathVariable Integer id, @RequestBody User user) {
        log.info("update user id:{}", id);
        log.info("update user:{}", user);
    }

 /**
     * 根据用户id删除
     *
     * @param id
     */
 @DeleteMapping ("/{id}")
 public void delete(@PathVariable Integer id) {
        log.info("delete user id:{}", id);
    }
}
测试用例
  • 在 /src/test/java/你的包名/ApplicationTests (默认情况下会自动创建一个)
代码语言:javascript复制
@RunWith (SpringRunner.class)
@SpringBootTest
@Slf4j
public class SpringbootRestfulApiApplicationTests {

}
  • 加入对应的测试用例代码;开发的原则,要尽量保证最小的修改测试;改一点测一点,因此以下的测试用例应该是在功能开发过程中,一边写功能一边写的测试用例
代码语言:javascript复制
import lombok.extern.slf4j.Slf4j;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;

@RunWith (SpringRunner.class)
@SpringBootTest
@Slf4j
public class SpringbootRestfulApiApplicationTests {
 @Autowired
    WebApplicationContext wac;

    MockMvc mockMvc;

 /**
     * 每个测试用例执行之前都会执行这一段方法
     */
 @Before
 public void setup() {
        mockMvc = MockMvcBuilders.webAppContextSetup(wac).build();
    }

 /**
     * 根据用户名查询的测试用例
     *
     * @throws Exception
     */
 @Test
 public void queryUserSuccessByUserName() throws Exception {
        String responseStr = mockMvc.perform(
 // 请求构建对象
            MockMvcRequestBuilders
 // 指定请求的restful api的地址
 // .get 就是表示发送get方法
                .get("/user")
 // 指定请求内容的格式
                .contentType(MediaType.APPLICATION_JSON_UTF8)
 // 参数
                .param("username", "zhangsan")
 // 页面
                .param("page", "1")
 // 分页的大小
                .param("size", "10")
 // 排序
                .param("sort", "age,desc"))
 // 指定响应的预期状态码
            .andExpect(MockMvcResultMatchers.status().isOk())
 // 指定响应预期的内容
            .andExpect(MockMvcResultMatchers.jsonPath("$.length()").value())
 // 获取到响应数据
            .andReturn().getResponse().getContentAsString();

        log.info("return string:{}", responseStr);

 // jsonPath  : https://github.com/json-path/JsonPath
    }

 /**
     * 根据ID查询用户详情的测试用例
     *
     * @throws Exception
     */
 @Test
 public void queryUserSuccessByIdSuccess() throws Exception {
        String responseStr = mockMvc.perform(
 // 请求构建对象
            MockMvcRequestBuilders
 // 指定请求的restful api的地址
 // .get 就是表示发送get方法
                .get("/user/1")
 // 指定请求内容的格式
                .contentType(MediaType.APPLICATION_JSON_UTF8))
 // 指定响应的预期状态码
            .andExpect(MockMvcResultMatchers.status().isOk())
 // 指定响应预期的内容
 // 要求返回的对象的用户名为:zhangsan
            .andExpect(MockMvcResultMatchers.jsonPath("$.username").value("zhangsan"))
 // 获取到响应数据
            .andReturn().getResponse().getContentAsString();

        log.info("return string:{}", responseStr);
    }

 /**
     * 根据ID查询用户信息的错误场景测试用例
     * 当如果传递的用户ID不是数据 {id:\d }正则表达式匹配失败的情况
     *
     * @throws Exception
     */
 @Test
 public void queryUserSuccessByIdFail() throws Exception {
        String responseStr = mockMvc.perform(
 // 请求构建对象
            MockMvcRequestBuilders
 // 指定请求的restful api的地址
 // .get 就是表示发送get方法
                .get("/user/mm")
 // 指定请求内容的格式
                .contentType(MediaType.APPLICATION_JSON_UTF8))
 // 指定响应的预期状态码为4xx
            .andExpect(MockMvcResultMatchers.status().is4xxClientError())
 // 获取到响应数据
            .andReturn().getResponse().getContentAsString();

        log.info("return string:{}", responseStr);
    }

 /**
     * 添加的测试用例
     *
     * @throws Exception
     */
 @Test
 public void addUserSuccess() throws Exception {
        String content = "{"username":"wangwu","age":25,"nickName":"wuwu","password":"123321"}";
        mockMvc
            .perform(MockMvcRequestBuilders.post("/user").contentType(MediaType.APPLICATION_JSON_UTF8).content(content))
            .andExpect(MockMvcResultMatchers.status().isOk());
    }

 /**
     * 修改的测试用例
     *
     * @throws Exception
     */
 @Test
 public void updateUserSuccess() throws Exception {
        String content = "{"username":"wangwu","age":30,"nickName":"wwuwu","password":"789456"}";
        mockMvc.perform(
 //
            MockMvcRequestBuilders
 // 请求对象
                .put("/user/1")
 // 请求内容的个数
                .contentType(MediaType.APPLICATION_JSON_UTF8)
 // 请求数据
                .content(content))
 // 响应状态要求
            .andExpect(MockMvcResultMatchers.status().isOk());
    }

 /**
     * 删除的测试用例
     *
     * @throws Exception
     */
 @Test
 public void deleteUserSuccess() throws Exception {
        mockMvc.perform(MockMvcRequestBuilders.delete("/user/1").contentType(MediaType.APPLICATION_JSON_UTF8))
            .andExpect(MockMvcResultMatchers.status().isOk());
    }
}

常用属性的说明

  • MockMvcRequestBuilders 请求构建者对象
    • get、post、put、delete分别表示发送对应Method的请求,参数为对应的接口地址;如 MockMvcRequestBuilders.get("/user/1")
    • param表示要传递的请求参数;如 param("username","zhangsan")
    • contentType 指定请求数据的数据格式;如: MediaType.APPLICATION_JSON_UTF8 表示请求数据为Json格式的数据
    • content 请求后端传递的数据;数据格式需要和contentType指定的对应
  • andExpect 添加期望的结果
    • MockMvcResultMatchers.status().isOk() 表示期望的响应状态码为200
    • MockMvcResultMatchers.status().is4xxClientError()表示期望的响应状态码为4xx 还可以是3xx、5xx等状态
    • MockMvcResultMatchers.jsonPath().value() 表示期望响应的json数据达到什么预期; 如:.jsonPath("$.length()").value(),表示期望响应的JsonArray的元素个数为个;如.jsonPath("$.username").value("zhangsan"),表示期望响应的JsonObject中的username值为zhangsan
  • andReturn获取响应的对象
    • getResponse() 获取响应的response对象
    • getResponse().getContentAsString() 获取响应的文本信息
JsonPath说明

详情可参考官方文档: https://github.com/json-path/JsonPath

用例测试结果
  • 确认单个测试用例;选择对应的方法,右键-->run "xxx()"
    • 达到预期(绿色)
  • 未达到预期(红色)
  • 所有测试用例一起确认;选择对应的class,右键-->run “xxxxTests()”

至此,我们就可以基于MockMvc 单元测试任意我们想要的接口,使其到我们的预期

0 人点赞