跳至主要內容

JAVA 单元测试

holic-x...大约 15 分钟碎片化碎片化

JAVA 单元测试

学习资料

单元测试

1.覆盖率

引入单元测试依赖

<!--引入单元测试依赖-->
<dependency>
  <groupId>org.junit.jupiter</groupId>
  <artifactId>junit-jupiter</artifactId>
  <version>5.8.2</version>
  <scope>test</scope>
</dependency>

普通类测试

​ 使用 idea 辅助查看单元测试覆盖率,例如构建一个Springboot项目,构建一个工具类用作测试,然后引入其相关的测试用例,查看覆盖率

构建OperatorUtil工具类

public class OperatorUtil {
    public static int add(int a, int b) {
        return a + b;
    }
    public static int sub(int a, int b) {
        return a - b;
    }
    public static int mul(int a, int b) {
        return a * b;
    }
    public static int div(int a, int b) {
        if (b == 0) {
            return 0;
        }
        return a / b;
    }
}

构建OperatorUtilTest测试类

class OperatorUtilTest {

    @Test
    void add() {
        int res = OperatorUtil.add(1, 2);
        assert res == 3;
    }

    @Test
    void sub() {
        int res = OperatorUtil.sub(2, 1);
        assert res == 1;
    }

    @Test
    void mul() {
        int res = OperatorUtil.mul(2, 1);
        assert res == 2;
    }

    @Test
    void div() {
        int res1 = OperatorUtil.div(2, 1);
        assert res1 == 2;

        int res2 = OperatorUtil.div(2, 0);
        assert res2 == 0;
    }
}

image-20240723083711678

接口调用测试

(1)测试案例

Controller 接口构建

@RestController
@RequestMapping("/demo")
public class DemoController {

    // 普通接口
    @GetMapping("/getName")
    public String getName() {
        return "hello";
    }

    // 带单个参数
    @GetMapping("/showName/{name}")
    public String showName(@PathVariable String name) {
        return name;
    }

    // 带多个参数
    @GetMapping("/showInfo")
    public String showInfo(@RequestParam String name,@RequestParam int age) {
        return "name : " + name + " age : " + age;
    }

    // 请求参数为实体类型
    @PostMapping("/showJson")
    public String showJson(@RequestBody JSONObject jsonObject) {
        return jsonObject.toJSONString();
    }

    // 带header校验
    @GetMapping("/getToken")
    public String showNameWithHeader(@RequestHeader(name = "userToken") String userToken) {
        return "userToken: " + userToken;
    }

}

接口测试案例1(自动配置MockMvc)

/**
 * JUnit 5
 */
@SpringBootTest
@AutoConfigureMockMvc
class DemoControllerTest1 {

    private static final Logger log = LoggerFactory.getLogger(DemoControllerTest1.class);

    // 自动配置MockMvc
    @Autowired
    private MockMvc mockMvc;

    private HttpHeaders headers;

    // 配置访问根路径
    private String baseUrl = "/demo"; // MvcMock测试运行独立于配置的servlet上下文路径

    @BeforeEach
    public void init() {
        MultiValueMap<String, String> headerMap = new LinkedMultiValueMap<>();
        headerMap.add(HeaderConstants.USER_TOKEN,"holic-x");
        headers = new HttpHeaders();
        headers.addAll(headerMap);
        // 权限限定(获取到权限配置,进行鉴权),模拟拥有权限访问某个资源
    }

    @SneakyThrows
    @Test
    void getName() {
        String url = baseUrl + "/getName";
        MvcResult mvcResult = mockMvc.perform(MockMvcRequestBuilders.get(url) // 接口访问路径
                .headers(headers) // header配置
                .contentType(MediaType.APPLICATION_JSON)// content-type 配置
                ).andExpect(MockMvcResultMatchers.status().isOk()) // 预期结果
                .andReturn();
        // 查看响应结果
        String res = mvcResult.getResponse().getContentAsString(StandardCharsets.UTF_8);
        log.info(res);
    }

}

image-20240723150223001

接口测试案例2(MvcMock测试运行独立于配置的servlet上下文路径)

/**
 * JUnit 5
 */
//@ActiveProfiles("test") // 激活配置
//@SpringBootTest(classes = DemoApplication.class)
@SpringBootTest
//@AutoConfigureMockMvc
//@WebMvcTest(controllers = DemoController.class)  // 限定测试范围,不会加载整个应用程序上下文(加快测试速度,专注于对web层测试)
class DemoControllerTest2 {

    private static final Logger log = LoggerFactory.getLogger(DemoControllerTest2.class);
    @Autowired
    private WebApplicationContext wac;

    private MockMvc mockMvc;
    private HttpHeaders headers;

    // 配置访问根路径
    private String baseUrl = "/api/demo";  // MvcMock测试运行独立于配置的servlet上下文路径

    @BeforeEach
    public void init() {
        mockMvc = MockMvcBuilders.webAppContextSetup(wac).build();
        // 配置请求头信息
        MultiValueMap<String, String> headerMap = new LinkedMultiValueMap<>();
        headerMap.add(HeaderConstants.COUNTRY,"CN");
        headerMap.add(HeaderConstants.APP_LANG,"Chinese");
        headerMap.add(HeaderConstants.USER_TOKEN,"holic-x");
        headerMap.add(HeaderConstants.TIMESTAMP,String.valueOf(new Date().getTime()));
        headers = new HttpHeaders();
        headers.addAll(headerMap);
        // 权限限定(获取到权限配置,进行鉴权),模拟拥有权限访问某个资源
    }

    @DisplayName("获取名称信息") // @DisplayName 给测试方法自定义显示名称
    @SneakyThrows // 用于在方法上自动抛出异常,便于开发使用
    @Test
    void getName() {
        String url = baseUrl + "/getName";
        MvcResult mvcResult = mockMvc.perform(MockMvcRequestBuilders.get(url) // 接口访问路径
                .headers(headers) // header配置
                .contextPath("/api") // MvcMock测试运行独立于配置的servlet上下文路径
                ).andExpect(MockMvcResultMatchers.status().isOk()) // 预期结果
                .andReturn();
        // 查看响应结果
        String res = mvcResult.getResponse().getContentAsString(StandardCharsets.UTF_8);
        log.info(res);
    }

    @SneakyThrows
    @Test
    void showNameWithHeader() {
        // 方式1:@PathVariable 参数构建,配置访问路径(参数装配在URL中)
        String requestUrl = baseUrl + "/showName/" + "哈哈哈" ;

        // 方式2
        String url = baseUrl + "/showNameWithHeader/{name}" ;
        // 构建请求参数
        String name = "哈哈哈";
        // Mock构建请求
//         MvcResult mvcResult = mockMvc.perform(MockMvcRequestBuilders.get(requestUrl) // 接口访问路径
        MvcResult mvcResult = mockMvc.perform(MockMvcRequestBuilders.get(url,name) // 接口访问路径
                        .headers(headers) // header配置
                        .contextPath("/api") // MvcMock测试运行独立于配置的servlet上下文路径
                        .contentType(MediaType.APPLICATION_FORM_URLENCODED_VALUE) // content-type 配置
                ).andExpect(MockMvcResultMatchers.status().isOk()) // 预期结果
                .andReturn();
        // 查看响应结果
        String res = mvcResult.getResponse().getContentAsString(StandardCharsets.UTF_8);
        log.info(res);
    }

    @SneakyThrows
    @Test
    void showInfo() {
        // 配置访问路径
        String url = baseUrl + "/showInfo";
        // 构建请求参数
        String name = "hahaha";
        // Mock构建请求
        MvcResult mvcResult = mockMvc.perform(MockMvcRequestBuilders.get(url) // 接口访问路径
                        .contextPath("/api") // MvcMock测试运行独立于配置的servlet上下文路径
//                        .contentType(MediaType.APPLICATION_FORM_URLENCODED_VALUE) // content-type 配置
                        .param("name",name) // 构建 @RequestParam 参数
                        .param("age","18")
                ).andExpect(MockMvcResultMatchers.status().isOk()) // 预期结果
                .andReturn();
        // 查看响应结果
        String res = mvcResult.getResponse().getContentAsString(StandardCharsets.UTF_8);
        log.info(res);
    }

    @SneakyThrows
    @Test
    void showJson() {
        // 配置访问路径
        String url = baseUrl + "/showJson";

        // 构建请求参数
        JSONObject requestJson = new JSONObject();
        requestJson.put("id", "1");
        requestJson.put("name", "hahaha");

        // Mock构建请求
        MvcResult mvcResult = mockMvc.perform(MockMvcRequestBuilders.post(url) // 接口访问路径
                        .contextPath("/api") // MvcMock测试运行独立于配置的servlet上下文路径
                        .contentType(MediaType.APPLICATION_JSON) // content-type 配置
                        .content(requestJson.toJSONString()) // 请求参数内容
                ).andExpect(MockMvcResultMatchers.status().isOk()) // 预期结果
                .andReturn();
        // 查看响应结果
        String res = mvcResult.getResponse().getContentAsString(StandardCharsets.UTF_8);
        log.info(res);
    }

    @SneakyThrows
    @Test
    void getToken() {
        String url = baseUrl + "/getToken";
        MvcResult mvcResult = mockMvc.perform(MockMvcRequestBuilders.get(url) // 接口访问路径
                        .headers(headers) // header配置
                        .contextPath("/api") // MvcMock测试运行独立于配置的servlet上下文路径
                ).andExpect(MockMvcResultMatchers.status().isOk()) // 预期结果
                .andReturn();
        // 查看响应结果
        String res = mvcResult.getResponse().getContentAsString(StandardCharsets.UTF_8);
        log.info(res);
    }

}

image-20240723134946624

常见问题处理

访问404问题:启动程序通过接口URL访问正常响应,但是通过单元测试访问出现404问题,进一步检查Controller访问路径确认是否正确。

​ 排查后发现是因为MvcMock测试运行独立于配置的servlet上下文路径,如果程序设定了Servlet的context-path配置,则相应需要调整配置(可以查看项目启动配置进行确认,从日志可以看到默认是没有/api前缀的的)

image-20240723104947188

​ 如果说没有指定MockMvcRequestBuilders的contextPath配置,其默认就是无context-path前缀启动,则相应访问路径也无需带/api(context-path),即此处这个配置和application.yml中的context-path配置是无关的,其由MockMvcRequestBuilders决定启动的上下文配置

​ 使用MockMvc可以测试controller层

// 测试单个controller
mockMvc = MockMvcBuilders.standaloneSetup(mockMvcController).build();

// 测试多个controller
mockMvc = MockMvcBuilders.webAppContextSetup(context).build();
(2)测试参考

普通接口测试

	@Test
  void pageMessageCenterByUserId(@Autowired MockMvc mvc) throws Exception {
    MvcResult mvcResult = mvc.perform(get("xxx")
      // 请求数据类型
      .contentType(MediaType.APPLICATION_FORM_URLENCODED_VALUE)
      // 返回数据类型
      .accept(MediaType.APPLICATION_JSON_UTF8_VALUE)
      // session会话对象
      .session(session)
      // URL传参
      .param("key", "value")
      // body传参
      .content(json))
      // 验证参数
      .andExpect(status().isOk())
      .andExpect(jsonPath("$.code").value(0))
      // 打印请求和响应体
      .andDo(MockMvcResultHandlers.print());
    // 打印响应body
    System.out.println(mvcResult.getResponse().getContentAsString());
  }

文件下载测试

​ 使用mockMvc测试下载文件时,需要注意controller方法的返回值需要为void,否则会报HttpMessageNotWritableException的异常错误

@Test
@WithUserDetails("admin")
@DisplayName("测试下载excel文件")
void downExcel() throws Exception {
    mockMvc.perform(get("/system/operate/export/excel")
                    .accept(MediaType.APPLICATION_OCTET_STREAM)
                    .param("startTime", "2022-11-22 10:51:25")
                    .param("endTime", "2022-11-23 10:51:25"))
    .andExpect(status().isOk())
    .andDo((result) -> {
        String contentDisposition = result.getResponse().getHeader("Content-Disposition");
        String fileName = URLDecoder.decode(contentDisposition.split("=")[1]);
        ByteArrayInputStream inputStream = new ByteArrayInputStream(result.getResponse().getContentAsByteArray());
        String basePath = System.getProperty("user.dir");
        // 保存为文件
        File file = new File(basePath + "/" + fileName);
        FileUtil.del(file);
        FileOutputStream outputStream = new FileOutputStream(file);
        StreamUtils.copy(inputStream, outputStream);
        outputStream.close();
        inputStream.close();
    });
}

MVC层测试

环境准备

# 数据表构建
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for t_user
-- ----------------------------
DROP TABLE IF EXISTS `t_user`;
CREATE TABLE `t_user` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `name` varchar(30) COLLATE utf8mb4_unicode_ci NOT NULL,
  `age` int(11) NOT NULL,
  PRIMARY KEY (`id`),
  KEY `index_age` (`age`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=31 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

-- ----------------------------
-- Records of t_user
-- ----------------------------
BEGIN;
INSERT INTO `t_user` (`id`, `name`, `age`) VALUES (1, '路飞', 1);
INSERT INTO `t_user` (`id`, `name`, `age`) VALUES (2, '索隆', 2);
INSERT INTO `t_user` (`id`, `name`, `age`) VALUES (3, '山治', 8);
INSERT INTO `t_user` (`id`, `name`, `age`) VALUES (4, '乌索普', 3);
INSERT INTO `t_user` (`id`, `name`, `age`) VALUES (5, '香克斯', 4);
INSERT INTO `t_user` (`id`, `name`, `age`) VALUES (6, '小张', 9);
INSERT INTO `t_user` (`id`, `name`, `age`) VALUES (7, '小白', 7);
INSERT INTO `t_user` (`id`, `name`, `age`) VALUES (8, '小红', 5);
INSERT INTO `t_user` (`id`, `name`, `age`) VALUES (9, '小李', 11);
INSERT INTO `t_user` (`id`, `name`, `age`) VALUES (10, '小黄', 10);
INSERT INTO `t_user` (`id`, `name`, `age`) VALUES (11, '小谢', 6);
INSERT INTO `t_user` (`id`, `name`, `age`) VALUES (12, '小吴', 12);
INSERT INTO `t_user` (`id`, `name`, `age`) VALUES (13, '小毛', 14);
INSERT INTO `t_user` (`id`, `name`, `age`) VALUES (14, '小赵', 13);
INSERT INTO `t_user` (`id`, `name`, `age`) VALUES (15, '小钱', 15);
INSERT INTO `t_user` (`id`, `name`, `age`) VALUES (16, '小王', 16);
INSERT INTO `t_user` (`id`, `name`, `age`) VALUES (17, '小乐', 17);
INSERT INTO `t_user` (`id`, `name`, `age`) VALUES (18, '小乐', 18);
INSERT INTO `t_user` (`id`, `name`, `age`) VALUES (19, '小虎', 21);
INSERT INTO `t_user` (`id`, `name`, `age`) VALUES (20, '小胡', 19);
INSERT INTO `t_user` (`id`, `name`, `age`) VALUES (21, '小于', 23);
INSERT INTO `t_user` (`id`, `name`, `age`) VALUES (22, '小余', 22);
INSERT INTO `t_user` (`id`, `name`, `age`) VALUES (23, '小鱼', 20);
INSERT INTO `t_user` (`id`, `name`, `age`) VALUES (24, '小马', 25);
INSERT INTO `t_user` (`id`, `name`, `age`) VALUES (25, '小仔', 24);
INSERT INTO `t_user` (`id`, `name`, `age`) VALUES (26, '小包', 26);
INSERT INTO `t_user` (`id`, `name`, `age`) VALUES (27, '小宝', 27);
INSERT INTO `t_user` (`id`, `name`, `age`) VALUES (28, '小好', 28);
INSERT INTO `t_user` (`id`, `name`, `age`) VALUES (29, '小尼', 29);
INSERT INTO `t_user` (`id`, `name`, `age`) VALUES (30, '小许', 30);
COMMIT;

SET FOREIGN_KEY_CHECKS = 1;
(1)环境构建

​ 构建用户CRUD操作

model层

/**
 * User 实体类
 */
@Data
@TableName("t_user")
public class User {
    @TableId("id")
    private Integer id;

    @TableField("name")
    private String name;

    @TableField("age")
    private Integer age;

    public User(){}
    public User(String name,Integer age){
        this.name = name;
        this.age = age;
    }
}

mapper层

@Mapper
public interface UserMapper extends BaseMapper<User> {}

service层

# 接口
public interface UserService extends IService<User> {
    public void testService(String key);
}

# 实现
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {

    @Override
    public void testService(String key) {
        System.out.println("key:" + key);
    }
}

controller层

@RestController
@RequestMapping("/user")
public class UserController {

    @Autowired
    private UserService userService;

    @PostMapping("/add")
    public String add(@RequestBody User user) {
        boolean res = userService.save(user);
        if(res){
            return "success";
        }else {
            return "fail";
        }
    }

    @GetMapping("/delete")
    public String delete(@RequestParam Integer id) {
        boolean res = userService.removeById(id);
        if(res){
            return "success";
        }else {
            return "fail";
        }
    }

    @PostMapping("/update")
    public String update(@RequestBody User user) {
        boolean res = userService.updateById(user);
        if(res){
            return "success";
        }else {
            return "fail";
        }
    }

    @GetMapping("/get")
    public User get(@RequestParam Integer id) {
        User findUser = userService.getById(id);
        return findUser;
    }

    @GetMapping("/getByCond")
    public List<User> getByCond(@RequestParam String searchKey) {
        QueryWrapper<User> queryWrapper = new QueryWrapper<>();
        queryWrapper.like("name",searchKey);
        List<User> userList = userService.list(queryWrapper);
        return userList;
    }
}
(2)controller层测试
@SpringBootTest
class UserControllerTest {

    private static final Logger log = LoggerFactory.getLogger(UserControllerTest.class);
    @Autowired
    private WebApplicationContext wac;

    private MockMvc mockMvc;
    private HttpHeaders headers;

    // 配置访问根路径
    private String baseUrl = "/user"; // MvcMock测试运行独立于配置的servlet上下文路径

    @BeforeEach
    public void init() {
        mockMvc = MockMvcBuilders.webAppContextSetup(wac).build();
        MultiValueMap<String, String> headerMap = new LinkedMultiValueMap<>();
        headerMap.add(HeaderConstants.USER_TOKEN,"holic-x");
        headerMap.add(HeaderConstants.TIMESTAMP,String.valueOf(new Date().getTime()));
        headers = new HttpHeaders();
        headers.addAll(headerMap);
        // 权限限定(获取到权限配置,进行鉴权),模拟拥有权限访问某个资源
    }

    @Transactional // 默认情况下在每个测试方法结束时回滚事务(但如果使用的是RANDOM_PORT或DEFINED_PORT的提供了一个真正的servlet环境的情况下,回滚失效)
    @SneakyThrows
    @Test
    void add() {
        // 接口访问路径
        String url = baseUrl + "/add";
        // 模拟数据
        User user = new User("noob",18);
        // 模拟接口调用
        MvcResult mvcResult = mockMvc.perform(MockMvcRequestBuilders.post(url) // 接口访问路径
                        .headers(headers) // header配置
                        .contentType(MediaType.APPLICATION_JSON)// content-type 配置
                        .content(JSONObject.toJSONBytes(user))
                ).andExpect(MockMvcResultMatchers.status().isOk()) // 预期结果
                .andReturn();
        // 查看响应结果
        String res = mvcResult.getResponse().getContentAsString(StandardCharsets.UTF_8);
        log.info(res);
    }

    @Transactional
    @SneakyThrows
    @Test
    void delete() {
        // 接口访问路径
        String url = baseUrl + "/delete";
        // 模拟接口调用
        MvcResult mvcResult = mockMvc.perform(MockMvcRequestBuilders.get(url) // 接口访问路径
                        .headers(headers) // header配置
                        .contentType(MediaType.APPLICATION_JSON)// content-type 配置
                        .param("id","1")
                ).andExpect(MockMvcResultMatchers.status().isOk()) // 预期结果
                .andReturn();
        // 查看响应结果
        String res = mvcResult.getResponse().getContentAsString(StandardCharsets.UTF_8);
        log.info(res);
    }

    @Transactional
    @SneakyThrows
    @Test
    void update() {
        // 接口访问路径
        String url = baseUrl + "/update";
        // 模拟数据
        User user = new User();
        user.setId(1);
        user.setName("noob");
        user.setAge(18);

        // 模拟接口调用
        MvcResult mvcResult = mockMvc.perform(MockMvcRequestBuilders.post(url) // 接口访问路径
                        .headers(headers) // header配置
                        .contentType(MediaType.APPLICATION_JSON)// content-type 配置
                        .content(JSONObject.toJSONBytes(user))
                ).andExpect(MockMvcResultMatchers.status().isOk()) // 预期结果
                .andReturn();
        // 查看响应结果
        String res = mvcResult.getResponse().getContentAsString(StandardCharsets.UTF_8);
        log.info(res);
    }

    @SneakyThrows
    @Test
    void get() {
        // 接口访问路径
        String url = baseUrl + "/get";
        // 模拟接口调用
        MvcResult mvcResult = mockMvc.perform(MockMvcRequestBuilders.get(url) // 接口访问路径
                        .headers(headers) // header配置
                        .contentType(MediaType.APPLICATION_JSON)// content-type 配置
                        .param("id","1")
                ).andExpect(MockMvcResultMatchers.status().isOk()) // 预期结果
                .andReturn();
        // 查看响应结果
        String res = mvcResult.getResponse().getContentAsString(StandardCharsets.UTF_8);
        log.info(res);
    }

    @SneakyThrows
    @Test
    void getByCond() {
        // 接口访问路径
        String url = baseUrl + "/getByCond";
        // 模拟接口调用
        MvcResult mvcResult = mockMvc.perform(MockMvcRequestBuilders.get(url) // 接口访问路径
                        .headers(headers) // header配置
                        .contentType(MediaType.APPLICATION_JSON)// content-type 配置
                        .param("searchKey","小")
                ).andExpect(MockMvcResultMatchers.status().isOk()) // 预期结果
                .andReturn();
        // 查看响应结果
        String res = mvcResult.getResponse().getContentAsString(StandardCharsets.UTF_8);
        log.info(res);
    }
}

image-20240723143709324

(3)service层测试
@SpringBootTest
class UserServiceTest {

    // UserService对象,模拟测试
    @Autowired
    private UserService userService;

    @Test
    void testService() {
        userService.testService("啊哈哈");
    }
}

​ 结合结果分析,对比上述controller接口测试,此处service测试依赖的是Spring 环境的上下文支持

image-20240723151044280

(4)mapper层测试
@SpringBootTest
class UserMapperTest {

    @Autowired
    private UserMapper userMapper;

    @Transactional
//    @Rollback(value = false) 如果希望事务提交,则可指定回滚为false
    @Test
    void insert() {
        User user = new User("noob",20);
        userMapper.insert(user);
    }
}

​ 结合结果分析,对比上述controller接口测试,此处mapper测试依赖的是Spring 环境的上下文支持

image-20240723151609600

2.Mockito

​ 借助Mockito可以模拟一些接口实现调用,模拟需要返回的数据。例如在微服务场景中经常涉及到多个服务调用,但是可能需要调用的服务还没开发好或者调用异常,按照传统的开发模式,可能就会通过自定义模拟数据的形式来避免开发阻塞,借助Mockito可以更加灵活便捷地达到单元测试目的。需注意@Mock的一些适用范围

普通测试

​ 以OperatorService操作测试为例,模拟单元测试

public interface OperatorService {

    public int add(int a, int b);

    public int sub(int a, int b);

    public int mul(int a, int b);

    public int div(int a, int b);

}

@Service
public class OperatorServiceImpl implements OperatorService {
    @Override
    public int add(int a, int b) {
        return a+b;
    }

    @Override
    public int sub(int a, int b) {
        return a-b;
    }

    @Override
    public int mul(int a, int b) {
        return a*b;
    }

    @Override
    public int div(int a, int b) {
        return a/b;
    }
}
@SpringBootTest
class MockitoTest {

    // 模拟测试调用 返回自定义响应值(不会真正执行方法,而是按照自定义规则返回信息,便于开发调试,实际不会调用方法,需额外关注覆盖率)
    @Mock
    private OperatorService operatorService;

    @Test
    void testMockito(){
        // 配置Mock行为
        Mockito.when(operatorService.add(1,2)).thenReturn(100);
        System.out.println(operatorService.add(1,2)); // 配置生效1次
        System.out.println(operatorService.add(5,10));
        System.out.println(operatorService.mul(5,10));
    }

}

// output
100
0
0

​ 结合测试结果可以看到,数据通过Mock并没有真正访问方法(可以设置断点查看是否进入方法),而是通过模拟调用的形式来自定义返回结果,配置只生效一次,再次调用返回为0(显然通过Mock没有实际调用方法);查看其分支覆盖率可以进一步确认,Mock只是提供了一种模拟调用机制,实际上并没有调用方法。

image-20240723213407808

Service测试对比

​ 定义分别定义ServiceA、ServiceB及其相关实现,且ServiceA中定义的方法会调用到ServiceB的内容

// ServiceA
public interface ServiceA {
    public String methodA();
}
// ServiceB
public interface ServiceB {
    public int methodB();
}

// ServiceAImpl
@Service
public class ServiceAImpl implements ServiceA {

    @Autowired
    private ServiceB serviceB;

    @Override
    public String methodA() {
        System.out.println("methodA");
        int res = serviceB.methodB();
        System.out.println("call ServiceB methodB:" + res);
        if(res==888){
            return "success";
        }else {
            return "fail";
        }
    }
}

// ServiceBImpl
@Service
public class ServiceBImpl implements ServiceB {
    @Override
    public int methodB() {
        System.out.println("methodB");
        return 1;
    }
}

​ 构建测试类,查看相应的覆盖率。对比上述案例,此处通过@Autowire注解注入OperatorService,调用的时候是真正执行了相应的方法。

@SpringBootTest
class MockServiceTest {

    @Autowired
    private ServiceA serviceA;

    @Autowired
    private OperatorService operatorService;

    @Test
    void testServiceA() {
        serviceA.methodA();
    }

    @Test
    void testAutowire(){
        System.out.println(operatorService.add(1,2));
        System.out.println(operatorService.mul(5,10));
    }

}

image-20240723213949303

模拟service调用

​ 在日常开发中经常会有调用其他服务接口的场景,在不影响自身业务逻辑开发的场景下,可通过单元测试,借助Mockito配置Service的mock行为,进而模拟测试放行。基于上述案例配置,构建测试参考。例如此处ServiceA调用ServiceB内容,但是可能由于一些原因ServiceB暂时无法提供功能,因此需要对ServiceB进行mock配置,让它返回预期的数据,然后让ServiceA的业务逻辑正常执行下去即可

@SpringBootTest
class MockServiceDemoTest {

    // 要测试目标
    @InjectMocks
    private ServiceAImpl serviceA; // 此处测试的是实现类,区别于@Autowired

    // mock目标(可以是一个实体或service)
    @Mock
    private ServiceB serviceB;

    @Test
    void testServiceA() {
        // 配置mock行为(此处因为ServiceA调用了ServiceB,因此ServiceB是需要mock的目标,而ServiceA为测试目标)
        Mockito.when(serviceB.methodB()).thenReturn(888);
        // 调用实际的服务方法
        String res1 = serviceA.methodA();
        // 验证结果是否符合预期
        assert res1.equals("success");

        // 配置mock行为
        Mockito.when(serviceB.methodB()).thenReturn(0);
        // 调用实际的服务方法
        String res2 = serviceA.methodA();
        // 验证结果是否符合预期
        assert res2.equals("fail");
    }

}

image-20240723214344690

测试案例参考

/**
 * MockService 测试
 */
@SpringBootTest
class MockServiceDemoTest2 {

    // 要测试目标
    @Autowired
    private ServiceA serviceA;

    // mock目标(可以是一个实体或service)
    @MockBean
    private ServiceB serviceB;

    @Test
    void testServiceA() {
        // 配置mock行为(此处因为ServiceA调用了ServiceB,因此ServiceB是需要mock的目标,而ServiceA为测试目标)
        Mockito.when(serviceB.methodB()).thenReturn(888);
        // 调用实际的服务方法
        String res1 = serviceA.methodA();
        // 验证结果是否符合预期
        assert res1.equals("success");
    }

}

​ 在这个例子中,ServiceA是想要测试的服务,ServiceB是它依赖的服务。使用@MockBean,模拟了ServiceB,并设置了它的某个方法返回预期的结果,然后调用YourService的方法,并对结果进行断言。这种方式会启动完整的Spring上下文,可能会比较慢。如果只是想快速测试某个服务,而不想启动整个Spring上下文,可以考虑使用Mockito框架手动模拟依赖

Sonar

Sonar Qube 服务端安装

Sonar 插件安装配置

  • SonarLint 安装:SonarLint运行需要依赖jdk8。在idea中引入sonar组件:File -> Settings -> Plugins -> Marketplace -> 输入SonarLint -> Install
  • SonarLint 配置:File -> Settings -> Other Settings-> SonarLint General Settings -> 添加SonarQube Servers,配置SonarQube服务地址
  • SonarQube Servers 绑定
评论
  • 按正序
  • 按倒序
  • 按热度
Powered by Waline v3.1.3