JAVA 单元测试
JAVA 单元测试
学习资料
- Springboot使用MockMvc测试接口
- 单元测试代码覆盖率
- 自定义测试信息
- Springboot 单元测试01
- Springboot 单元测试02
- Springboot 编写单元测试(Mock 解构模块依赖,模拟方法调用并响应)
- Springboot 整合Mock
- IDEA 整合 sonar 插件
- 代码质量检测-SonarQube
单元测试
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;
}
}
接口调用测试
(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);
}
}
接口测试案例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);
}
}
常见问题处理
访问404问题:启动程序通过接口URL访问正常响应,但是通过单元测试访问出现404问题,进一步检查Controller访问路径确认是否正确。
排查后发现是因为MvcMock测试运行独立于配置的servlet上下文路径,如果程序设定了Servlet的context-path配置,则相应需要调整配置(可以查看项目启动配置进行确认,从日志可以看到默认是没有/api
前缀的的)
如果说没有指定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);
}
}
(3)service层测试
@SpringBootTest
class UserServiceTest {
// UserService对象,模拟测试
@Autowired
private UserService userService;
@Test
void testService() {
userService.testService("啊哈哈");
}
}
结合结果分析,对比上述controller接口测试,此处service测试依赖的是Spring 环境的上下文支持
(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 环境的上下文支持
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只是提供了一种模拟调用机制,实际上并没有调用方法。
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));
}
}
模拟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");
}
}
测试案例参考
/**
* 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 服务端安装
- SonarQube 官网下载:SonarQube-代码质量管理平台(Devops代码安全审计必备)
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 绑定: