一、什么是单元测试
单元测试(unit testing),是指对软件中的最小可测试单元进行检查和验证。“单元”可以是一个函数、方法、类、功能模块或者子系统。 单元测试的核心在于行、分支覆盖率,不关注业务正确性
二、单元测试实现方式
单元测试的实现方式包括:人工静态检查、动态执行跟踪。
- 人工静态检查:就是通常所说的“代码走读”,主要是保证代码逻辑的正确性;
- 动态执行跟踪:就是把程序代码运行起来,检查实际的运行结果和预期结果是否一致。
2.1 人工静态检查
人工静态检查包含的主要内容:
- 检查算法的逻辑正确性
- 模块接口的正确性检查
- 输入参数有没有作正确性检查
- 调用其他方法接口的正确性
- 异常错误处理
- 保证表达式、SQL 语句的正确性
- 检查常量或全局变量使用的正确性
- 程序风格的一致性、规范性
- 检查代码注释是否完整
2.2 动态执行跟踪
动态执行跟踪需要编写测试脚本调用业务代码进行测试,为了更好的管理维护测试脚本,一般会采用单元测试框架来管理,Java 常见的单元测试框架:JUnit、TestNG,其辅助依赖项有 Mockito、SpringBootTest;
2.3 单元测试衡量标准
单元测试的一个重要的衡量标准就是代码覆盖率,尽量做到代码的全覆盖。常见单元测试覆盖标准:
- 语句覆盖
- 分支覆盖
- 条件覆盖
- 分支-条件覆盖
- 条件组合覆盖
- 路径覆盖
- 异常检测
2.4 单元测试遵循的规则
- 每一个测试方法上使用@Test 进行修饰;
- 每一个测试方法必须使用 public void 进行修饰;
- 每一个测试方法不能携带参数;
- 测试代码和源代码在两个不同的项目路径下;
- 测试类的包应该和被测试类保持一致;
- 测试单元中的每个方法必须可以独立测试;
- 每个测试单元都需要有断言,不能有虚假 UT;
- 测试单元内部不要 try-catche;
- 尽量不要使用反射对 private 方法进行测试;
2.5 单元测试代码位置
对于一个 Maven 项目,其结构如下
Gradle
Springboot 中单元测试类写在 src/test/java 目录下,可以进行手动创建测试类,或者通过 idea 自动创建测试类 ctrl+shift+T。
三、引入依赖
3.1 使用 Junit
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-all</artifactId>
<version>1.10.19</version>
<scope>test</scope>
</dependency>
3.2 使用 SpringBootTest
SpringbootTest 使用单元测试需要先引入以下依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
test 依赖会引入如下的 jar 包
序号库作用1Junit5包含兼容 Junit4,Java 应用程序单元测试的事实标准2SpringTest 和 SpringBootTest对 SpringBoot 应用程序的公共和集成测试支持3AssertJ断言库4Hamcrest匹配对象库5MockitoJava 模拟框架6JSONassertJSON 断言库7JsonPathJSON XPath
3.3 PowerMock
<properties>
<!-- mock包 -->
<powermock.version>2.0.2</powermock.version>
</properties>
<dependencies>
<!-- 引入单元测试mock包 -->
<dependency>
<groupId>org.powermock</groupId>
<artifactId>powermock-module-junit4</artifactId>
<version>${powermock.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.powermock</groupId>
<artifactId>powermock-api-mockito2</artifactId>
<version>${powermock.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
四、Junit 的一些用法
4.1 Junit 的一些注解
- @Before 被注解的方法将在当前测试类中的每个@Test 方法前执行;
- @After 被注解的方法将在当前测试类中的每个@Test 方法后执行;
- @Test(expected=XX.class) 这个参数表示我们期望会出现什么异常,比如说在除法中,我们 1/0 会出现 ArithmeticException 异常,那这里@Test(expected=ArithmeticException.class)。在测试这个除法时候依然能够通过。
- @Test(timeout=毫秒 ) 这个参数表示如果测试方法在指定的 timeout 内没有完成,就会强制停止。
- @Ignore 这个注解其实基本上不用,它的意思是所修饰的测试方法会被测试运行器忽略。
- @RunWith @RunWith 可以更改测试运行器, @RunWith(JUnit4.class) :用 JUnit4 来运行 @RunWith(SpringRunner.class) :让测试运行于 Spring 测试环境(Junit4.12 及以上) @RunWith(SpringJUnit4ClassRunner.class):让测试运行于 Spring 测试环境 @RunWith(Suite.class) :整合测试也称 打包测试;可以把之前所有的写好的 test class 类进行集成; @RunWith(Parameterized.class) :进行参数化设置 @ContextConfiguration Spring 整合 JUnit4 测试时,使用注解引入多个配置文件,比如,使用下面的 2 个注解,让测试在 Spring 容器环境下执行,运行环境未 dev
@RunWIth(SpringJunit4ClassRunner.class)
@ContextConfiguration(locations = {"classpath:application.yaml"}
@ActiveProfiles("dev")
4.2 断言用法
序号方法含义1void assertEquals(boolean expected, boolean actual)检查两个变量或者等式是否平衡2void assertFalse(boolean condition)检查条件是假的3void assertNotNull(Object object)检查对象不是空的4void assertNull(Object object)检查对象是空的5void assertTrue(boolean condition)检查条件为真6void fail()在没有报告的情况下使测试不通过
4.3 Junit4 和 Junit5 的一些区别
注意:Junit4 和 Junit5 的注解不要混用
4.4 Junit 单元测试执行流程
4.5 assertThat 的使用
JUnit 4.4 结合 Hamcrest 提供了一个全新的断言语法——assertThat。可以只使用 assertThat 一个断言语句,结合 Hamcrest 提供的匹配符,就可以表达全部的测试思想。 assertThat 的优点:
- 以前 JUnit 提供了很多的 assertion 语句,如:assertEquals,assertNotSame,assertFalse,assertTrue,assertNotNull,assertNull 等,现在有了 JUnit 4.4,一条 assertThat 即可以替代所有的 assertion 语句,这样可以在所有的单元测试中只使用一个断言方法,使得编写测试用例变得简单,代码风格变得统一,测试代码也更容易维护。
- assertThat 使用了 Hamcrest 的 Matcher 匹配符,用户可以使用匹配符规定的匹配准则精确的指定一些想设定满足的条件,具有很强的易读性,而且使用起来更加灵活。
- assertThat 不再像 assertEquals 那样,使用比较难懂的“谓宾主”语法模式(如:assertEquals(3, x);),相反,assertThat 使用了类似于“主谓宾”的易读语法模式(如:assertThat(x,is(3));),使得代码更加直观、易读。
基本语法:
assertThat([value], [matcher statement]);
value: 接下来想要测试的变量值;
matcher statement: 使用 Hamcrest 匹配符来表达的对前面变量所期望的值的声明,如果 value 值与 matcher statement 所表达的期望值相符,则测试成功,否则测试失败。
4.6 测试套件用法
测试嵌套的方法,他的作用是我们把测试类封装起来,也就是把测试类嵌套起来,只需要运行测试套件,就能运行所有的测试类了。
// 这里有很多个测试类
public class Test1 {
@Test
public void test() {
System.out.println("测试类1");
}
}
public class Test2 {
@Test
public void test() {
System.out.println("测试类2");
}
}
// 这里依次可以类推
我们使用测试套件,把这些测试类嵌套在一起。
@RunWith(Suite.class)
@Suite.SuiteClasses({Test1.class,Test2.class等相关测试类})
public class SuiteTest {/*
* 写一个空类:不包含任何方法
* 更改测试运行器Suite.class
* 将测试类作为数组传入到Suite.SuiteClasses({})中
*/
}
4.7 参数化设置用法
如果要测试多组数据怎么办?总不能一个一个输入,然后运行测试吧。这时候我们可以把我们需要测试的数据先配置好。
@RunWith(Parameterized.class)
public class ParameterizedTest {
// 预期
int expected = 0;
// 输入1
int input1 = 0;
// 输入2
int input2 = 0;
/**
* 配置一组测试的数据
*/
@Parameters
public static Collection<Object[]> t() {
return Arrays.asList(new Object[][]{{3, 1, 2}, {4, 2, 2}});
}
public ParameterizedTest(int expected, int input1, int input2) {
this.expected = expected;
this.input1 = input1;
this.input2 = input2;
}
@Test
public void testAdd() {
assertEquals(expected, Calculate.add(input1, input2));
}
}
这时候再去测试,只需要去选择相应的值即可,避免了我们一个一个手动输入。
五、PowerMock 使用
5.1 重要注解说明
- @RunWith(PowerMockRunner.class) // 告诉 JUnit 使用 PowerMockRunner 进行测试
- @PrepareForTest({XXX.class}) // 所有需要测试的类列在此处,适用于模拟 final 类或有 final, private, static, native 方法的类
- @PowerMockIgnore(“javax.management.*”) //为了解决使用 powermock 后,提示 classloader 错误
PowerMock 使用示例
@RunWith(PowerMockRunner.class)
@PrepareForTest(RedisUtil.class)
5.2 使用方式
- 处理 public void 类型方法
Powermockito.doNothing.when(T class2mock, String method, <T>… params>
- 模拟构造函数,即当出现 new InstanceClass() 时可以将此构造函数拦截并替换结果为我们需要的 mock 对象
Powermockito.whenNew(InstanceClass.class).thenReturn(Object value)
- 模拟 final 类,模拟静态方法
//final类
Powermockito.mockStatic(FinalClassToMock.class);
Powermockito.when(StaticClassToMock.method(Object.. params)).thenReturn(Object value)
//staic方法
Powermockito.mockStatic(StaticClassToMock.class);
Powermockito.when(StaticClassToMock.method(Object.. params)).thenReturn(Object value)
- 设置对象的 private 属性
//object为需要设置属性的静态类或对象
Whitebox.setInternalState(Object object, String fieldname, Object… value);
- 接口模拟返回
InterfaceToMock mock = Powermockito.mock(InterfaceToMock.class);
Powermockito.when(mock.method(Params…)).thenReturn(value)
Powermockito.when(mock.method(Params..)).thenThrow(Exception)
六、SpringBootTest 进行单元测试
参考
- SpringBootTest 详解
- SpringBootTest(测试模块)详解_微学苑
6.1 Service 单元测试
对 Controller 进行单元测试时,需要使用到 MockMvc 了。这样就可以不必启动项目就可以测试这些接口了。 MockMvc 实现了对 Http 请求的模拟,能够直接使用网络的形式,转换到 Controller 的调用,这样可以使得测试速度快、不依赖网络环境,而且提供了一套验证的工具,这样可以使得请求的验证统一而且很方便。 示例代码如下:
Controller
package com.unit.test.controller;
import com.unit.test.bean.Student;
import com.unit.test.service.StudentService;
import java.util.List;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/unit/test")
public class JunitController extends BaseController {
@Autowired
private StudentService studentService;
@GetMapping("/not/param")
public String junitControllerNotParam() {
return "没有请求参数的单元测试get方法";
}
@GetMapping("/check/account")
public String checkAccount() {
String account = getAccount();
if (StringUtils.isNotBlank(account)) {
return "成功";
} else {
return "失败";
}
}
@GetMapping("/student")
public Student getStudent(@RequestParam(value = "name") String name) {
return studentService.getByName(name);
}
@GetMapping("/students")
public List<Student> getStudents() {
return studentService.listStudent();
}
@PostMapping("/student")
public String add(@RequestBody List<Student> students) {
students.forEach(student -> studentService.save(student));
return "success";
}
}
测试类
package com.unit.test.controller;
import com.unit.test.bean.Student;
import com.unit.test.service.StudentService;
import com.unit.test.utils.JsonUtils;
import lombok.extern.slf4j.Slf4j;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.boot.test.mock.mockito.MockBeans;
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.MvcResult;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.result.MockMvcResultHandlers;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;
@Slf4j
@SpringBootTest
@RunWith(SpringRunner.class)
public class Junit4ControllerTest {
private JunitController junitController;
@Autowired
private WebApplicationContext webApplicationContext;
@MockBean
private StudentService studentService;
private MockMvc mockMvc;
@Before
public void setUp() {
log.info("setUp...");
// 初始化mockMvc对象
// 指定webApplicationContext上下文,将会从这个上下文获取对应的控制器并得到相应的mockMvc
mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build();
log.info("setUp end...");
}
@After
public void tearDown() {
log.info("@After");
}
@Test
public void junitControllerNotParam_404() throws Exception {
MvcResult mvcResult = mockMvc.perform(MockMvcRequestBuilders.get("/unit/test/get/has/param")
.accept(MediaType.TEXT_HTML_VALUE)
.contentType(MediaType.APPLICATION_JSON_UTF8))
.andDo(MockMvcResultHandlers.print())
.andReturn();
int status = mvcResult.getResponse().getStatus();
Assert.assertEquals(404, status);
}
@Test
public void junitControllerNotParam() throws Exception {
// MockMvcRequestBuilders.get("/url"): 构造一个get请求
MvcResult mvcResult = mockMvc.perform(MockMvcRequestBuilders.get("/unit/test/not/param")
.accept(MediaType.TEXT_HTML_VALUE)
.contentType(MediaType.APPLICATION_JSON_UTF8))
.andDo(MockMvcResultHandlers.print())
.andReturn();
int status = mvcResult.getResponse().getStatus();
String content = mvcResult.getResponse().getContentAsString();
Assert.assertEquals(200, status);
Assert.assertEquals("没有请求参数的单元测试get方法", content);
}
@Test
public void getStudent() throws Exception {
Student student = new Student();
student.setName("zhangsan");
student.setAge(13);
Mockito.when(studentService.getByName("zhangsan")).thenReturn(student);
// MockMvcRequestBuilders.get("/url"): 构造一个get请求
MvcResult mvcResult = mockMvc.perform(MockMvcRequestBuilders.get("/unit/test/student")
// 传参
.param("name", "zhangsan")
// 请求类型,json
.contentType(MediaType.APPLICATION_JSON_UTF8))
// 添加ResultHandler结果处理器,比如调试时 打印结果(print方法)到控制台
.andDo(MockMvcResultHandlers.print())
.andReturn();
int status = mvcResult.getResponse().getStatus();
Assert.assertEquals(200, status);
String content = mvcResult.getResponse().getContentAsString();
Student convert = JsonUtils.str2obj(Student.class, content);
Assert.assertEquals(13, convert.getAge().intValue());
Assert.assertEquals("zhangsan", convert.getName());
}
}
6.2 SpringBoot 引入 MockMvc
- 什么是 Mock? 在面向对象的程序设计中,模拟对象(mock object)是以可控的方式模拟真实对象行为的假对象。在编程过程中,通常通过模拟一些输入数据,来验证程序是否达到预期结果。
- 为什么使用 Mock 对象 使用模拟对象,可以模拟复杂的、真实的对象行为。如果在单元测试中无法使用真实对象,可采用模拟对象进行替代。
- MockMvc 的概念 MockMvc 是由 spring-test 包提供,实现了对 http 请求的模拟,能够直接使用网络的形式,转换到 Controller 的调用,使得测试速度快、不依赖网络环境。同时提供了一套验证的工具,结果的验证十分方便。 接口 MockMvcBuilder,提供一个唯一的 build 方法,用来构造 MockMvc。主要有两个实现 StandaloneMockMvcBuilder 和 DefaultMockMvcBuilder。
- MockMvc 的基本步骤 (1) mockMvc.perform 执行一个请求; (2) MockMvcRequestBuilders.get(“xxx”)构造一个请求; (3) ResultActions.param 添加请求传参; (4) ResultActions.accept()设置返回类型 ; (5) ResultActions.addExpect 添加执行完成后的断言 ; (6) ResultActions.andDo 添加一个结果处理器,表示要对结果做的事情,例如使用 print()输出相应信息 ; (7) ResultActions.andReturn 表示执行完成后返回相应的结果;
6.3 注解含义
- @SpringBootTest: 获取启动类,加载配置,寻找主配置启动类(@SpringBootApplication 注解的)
- @RunWith(SpringRunner.class):让 JUnit 运行 Spring 的测试环境,获取 Spring 环境的上下文支持
- @AutoConfigureMockMvc:用于自动配置 MockMvc,配置后 MockMvc 类可以直接注入,相当于 new MockMvc
七、Jacoco 单元测试工具的使用
参考 Jacoco 单元测试工具的使用
八、单元测试的一些弊端
- 写单元测试会花费大量的时间;
- 代码改动时需要维护单元测试用例;
- 自己测试自己,考虑的场景会不充分;
- 如果代码中存在一些多线程或者 Thread.sleep 等代码,会影响构建速度;
版权归原作者 逐梦码场 所有, 如有侵权,请联系我们删除。