Java测试我们应该都遇到过,一般我们会被要求做单元测试,来验证我们代码的功能以及效率。
这里来和大家一起探讨下有关单于测试。
什么是单元测试?
是指对软件中的最小可测试单元进行检查和验证。对于单元测试中单元的含义,一般来说,要根据实际情况去判定其具体含义,如C语言中单元指一个函数,Java里单元指一个类,图形化的软件中可以指一个窗口或一个菜单等。总的来说,单元就是人为规定的最小的被测功能模块。单元测试是在软件开发过程中要进行的最低级别的测试活动,软件的独立单元将在与程序的其他部分相隔离的情况下进行测试。
经常与单元测试联系起来的另外一些开发活动包括代码走读(Code review),静态分析(Static analysis)和动态分析(Dynamic analysis)。静态分析就是对软件的源代码进行研读,查找错误或收集一些度量数据,并不需要对代码进行编译和执行。动态分析就是通过观察软件运行时的动作,来提供执行跟踪,时间分析,以及测试覆盖度方面的信息。
单元测试的必要性?
公司要求我们写单元测试,我们会按照公司要求的单元测试率,来进行相关工作的完成;
那么会出现这样一些情况:
- 感觉写单元测试费力不讨好,有时候写单元测试的时间大于写业务逻辑的时间,需要 mock 一大堆数据。要保证各种覆盖率,特别是分支覆盖率,需要覆盖到所写的每一个分支。
- 大家写的单元测试质量参差不齐,因为感觉大家都是为了写测试而写测试,而不是真正的 tdd
- 迭代频繁,节奏紧凑的环境下,想要做到真正的 tdd ,还是有很大的难度的
确实,这是一个比较常见的问题。
那么我觉得,对于公司而言,需要有一套相关的单元测试规范,哪些是必须要去写单元测试的,分高低的优先级。同时对于单元测试案例,要做到及时的审查,关注其覆盖率,关注其有效性,对于无效的测试要及时剔除和优化。
单元测试的意义
程序代码都是由基本单元不断组合成复杂的系统,底层基本单元都无法保证输入输出正确性,层级递增时,问题就会不断放大,直到整个系统崩溃无法使用。所以单元测试的意义就在于保证基本功能是正常可用且稳定的。而对于接口、数据源等原因造成的不稳定因素,是外在原因,不在单元测试考虑范围之内。
JUnit使用
Controller层单元测试
这里我们以Springboot为例:
1、引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
2、代码案例
@RunWith(SpringRunner.class)
@SpringBootTest(classes = MainApplication.class)
public class StudentControllerTest {
// 注入Spring容器
@Autowired
private WebApplicationContext applicationContext;
// 模拟Http请求
private MockMvc mockMvc;
@Before
public void setupMockMvc(){
// 初始化MockMvc对象
mockMvc = MockMvcBuilders.webAppContextSetup(applicationContext).build();
}
/**
* 新增学生测试用例
* @throws Exception
*/
@Test
public void addStudent() throws Exception{
String json="{"name":"张三","className":"三年级一班","age":"20","sex":"男"}";
mockMvc.perform(MockMvcRequestBuilders.post("/student/save") //构造一个post请求
// 发送端和接收端数据格式
.contentType(MediaType.APPLICATION_JSON_UTF8)
.accept(MediaType.APPLICATION_JSON_UTF8)
.content(json.getBytes())
)
// 断言校验返回的code编码
.andExpect(MockMvcResultMatchers.status().isOk())
// 添加处理器打印返回结果
.andDo(MockMvcResultHandlers.print());
}
}
只需要在类或者指定方法上右键执行即可,可以直接充当 postman 工作访问指定 url,且不需要写请求代码,
本案例中构造 mockMVC 对象时,也可以使用如下方式:
@Autowired
private StudentController studentController;
@Before
public void setupMockMvc(){
// 初始化MockMvc对象
mockMvc = MockMvcBuilders.standaloneSetup(studentController).build();
}
其中 MockMVC 是 Spring 测试框架提供的用于 REST 请求的工具,是对 Http 请求的模拟,无需启动整个模块就可以对 Controller 层进行调用,速度快且不依赖网络环境。
使用 MockMVC 的基本步骤如下:
-
mockMvc.perform 执行请求
-
MockMvcRequestBuilders.post 或 get 构造请求
-
MockHttpServletRequestBuilder.param 或 content 添加请求参数
-
MockMvcRequestBuilders.contentType 添加请求类型
-
MockMvcRequestBuilders.accept 添加响应类型
-
ResultActions.andExpect 添加结果断言
-
ResultActions.andDo 添加返回结果后置处理
-
ResultActions.andReturn 执行完成后返回相应结果
Service层单元测试
@RunWith(SpringRunner.class)
@SpringBootTest
public class StudentServiceTest {
@Autowired
private StudentService studentService;
@Test
public void getOne() throws Exception {
Student stu = studentService.selectByKey(5);
Assert.assertThat(stu.getName(),CoreMatchers.is("张三"));
}
}
执行结果
DAO层单元测试
代码示例
@RunWith(SpringRunner.class)
@SpringBootTest
public class StudentDaoTest {
@Autowired
private StudentMapper studentMapper;
@Test
@Rollback(value = true)
@Transactional
public void insertOne() throws Exception {
Student student = new Student();
student.setName("李四");
student.setMajor("计算机学院");
student.setAge(25);
student.setSex('男');
int count = studentMapper.insert(student);
Assert.assertEquals(1, count);
}
}
其中 @Rollback (value = true) 可以执行单元测试之后回滚所新增的数据,保持数据库不产生脏数据。
异常测试
1、首先我们在service层可以定义一个异常情况
public void computeScore() {
int a = 10, b = 0;
int c = a/b;
}
2、我们在service中编写单元测试
@Test(expected = ArithmeticException.class)
public void computeScoreTest() {
studentService.computeScore();
}
执行单元测试也会通过,原因是 @Test 注解中的定义了异常
查看单元测试的覆盖率
1) 单测覆盖率
测试覆盖率是衡量测试过程工作本身的有效性,提升测试效率和减少程序 bug,提升产品可靠性与稳定性的指标。
统计单元测试覆盖率的意义:
1) 可以洞察整个代码中的基础组件功能的所有盲点,发现相关问题。
2) 提高代码质量,通常覆盖率低表示代码质量也不会太高,因为单测不通过本来就映射出考虑到各种情况不够充分。
3) 从覆盖率的达标上可以提高代码的设计能力。
(2) 在 idea 中查看单元测试覆盖率很简单,只需按照图中示例的图标运行,或者在单元测试方法或类上右键 Run 'xxx' with Coverage 即可。执行结果是一个表格,列出了类、方法、行数、分支覆盖情况。
(3) 在代码中会标识出覆盖情况,绿色的是已覆盖的,红色的是未覆盖的。
(4) 如果想要导出单元测试的覆盖率结果,可以使用如下图所示的方式,勾选 Open generated HTML in browser
导出结果:
JUnit插件自动生成测试案例
1) 安装插件,重启 idea 生效
(2) 配置插件
(3) 使用插件
在需要生成单测代码的类上右键 generate...,如下图所示。
生成结果:
单元测试工具Mockito
简介
Mockito 是一个针对 Java 的 mocking 框架。它与 EasyMock 和 jMock 很相似,但是通过在执行后校验什么已经被调用,它消除了对期望行为(expectations)的需要。其它的 mocking 库需要你在执行前记录期望行为(expectations),而这导致了丑陋的初始化代码。
Mock 过程的使用前提:
(1) 实际对象时很难被构造出来的
(2) 实际对象的特定行为很难被触发
(3) 实际对象可能当前还不存在,比如依赖的接口还没有开发完成等等。
Mockito 官网:https://site.mockito.org 。Mockito 和 JUnit 一样是专门针对 Java 语言的 mock 数据框架,它与同类的 EasyMock 和 jMock 功能非常相似,但是该工具更加简单易用。
Mockito 的特点:
(1) 可以模拟类不仅仅是接口
(2) 通过注解方式简单易懂
(3) 支持顺序验证
(4) 具备参数匹配器
使用案例
1、在之前的代码中在定义一个 BookService 接口,含义是借书接口,暂且不做实现
public interface BookService {
Book orderBook(String name);
}
2、在之前的 StudentService 类中新增一个 orderBook 方法,含义是学生预定书籍方法,其中实现内容调用上述的 BookService 的 orderBook 方法。
public Book orderBook(String name) {
return bookService.orderBook(name);
}
3、编写单元测试方法,测试 StudentService 的 orderBook 方法
@Test
public void orderBookTest() {
Book expectBook = new Book(1L, "钢铁是怎样炼成的", "书架A01");
Mockito.when(bookService.orderBook(any(String.class))).thenReturn(expectBook);
Book book = studentService.orderBook("");
System.out.println(book);
Assert.assertTrue("预定书籍不符", expectBook.equals(book));
}
4、执行结果
上述内容并没有实现 BookService 接口的 orderBook (String name) 方法。但是使用 mockito 进行模拟数据之后,却通过了单元测试,原因就在于 Mockito 替换了本来要在 StudentService 的 orderBook 方法中获取的对象,此处就模拟了该对象很难获取或当前无法获取到,用模拟数据进行替代。