共计 10814 个字符,预计需要花费 28 分钟才能阅读完成。
作者:京东物流 秦彪
1. 什么是单元测试
(1)单元测试环节:
测试过程依照阶段划分分为:单元测试、集成测试、零碎测试、验收测试等。相干含意如下:
1) 单元测试:针对计算机程序模块进行输入正确性测验工作。
2) 集成测试:在单元测试根底上,整合各个模块组成子系统,进行集成测试。
3) 零碎测试:将整个交付所波及的合作内容都纳入其中思考,蕴含计算机硬件、软件、接口、操作等等一系列作为一个整体,测验是否满足软件或需要阐明。
4) 验收测试:在交付或者公布之前对所做的工作进行测试测验。
单元测试是阶段性测试的首要环节,也是白盒测试的一种,该内容的编写与实际能够前置在研发实现,研发在编写业务代码的时候就须要生成对应代码的单元测试。单元测试的发起人是程序设计者,受益人也是编写程序的人,所以对于程序员,十分有必要造成自我约束力,实现根本的单元测试用例编写。
(2)单元测试特色:
由上可知,单元测试其实是针对软件中最小的测试单元来进行验证的。这里的单元就是指相干的性能子集,比方一个办法、一个类等。值得注意的是作为最低级别的测试流动,单元测试验证的对象仅限于以后测试内容,与程序其它局部内容相隔离,总结起来单元测试有以下特色:
1) 次要性能是证实编写的代码内容与冀望输入统一。
2) 最小最低级的测试内容,由程序员本身发动,保障程序根本组件失常。
3) 单元测试尽量不要区分类与办法,主张以过程性的办法为测试单位,简略实用高效为指标。
4) 不要偏离主题,专一于测试一小块的代码,保障根底性能。
5) 剥离与内部接口、存储之间的依赖,使单元测试可控。
6) 任何工夫任何程序执行单元测试都须要是胜利的。
2. 为什么要单元测试
(1)单元测试意义:
程序代码都是由根本单元一直组合成简单的零碎,底层根本单元都无奈保障输入输出正确性,层级递增时,问题就会一直放大,直到整个零碎解体无奈应用。所以单元测试的意义就在于保障基本功能是失常可用且稳固的。而对于接口、数据源等起因造成的不稳固因素,是外在起因,不在单元测试思考范畴之内。
(2)应用 main 办法进行测试:
@PostMapping(value="/save")
public Map<String,Object> save(@RequestBody Student stu) {studentService.save(stu);
Map<String,Object> params = new HashMap<>();
params.put("code",200);
params.put("message","保留胜利");
return params;
}
如果要对下面的 Controller 进行测试,能够编写如下的代码示例,应用 main 办法进行测试的时候,先启动整个工程利用,而后编写 main 办法如下进行拜访,在单步调试代码。
public static void main(String[] args) {HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
String json = "{"name":" 张三 ","className":" 三年级一班 ","age":"20","sex":" 男 "}";
HttpEntity<String> httpEntity = new HttpEntity<>(json, headers);
String url = "http://localhost:9092/student/save";
MainMethodTest test = new MainMethodTest();
ResponseEntity<Map> responseEntity = test.getRestTemplate().postForEntity(url, httpEntity, Map.class);
System.out.println(responseEntity.getBody());
}
(3)应用 main 办法进行测试的毛病:
1) 通过编写大量的 main 办法针对每个内容做打印输出到控制台干燥繁琐,不具备优雅性。
2) 测试方法不能一起运行,后果须要程序员本人判断正确性。
3) 对立且重复性工作应该交给工具去实现。
3. 单元测试框架 -JUnit
3.1 JUnit 简介
JUnit 官网:https://junit.org/。JUnit 是一个用于编写可反复测试的简略框架。它是用于单元测试框架的 xUnit 体系结构的一个实例。
JUnit 的特点:
(1)针对于 Java 语言特定设计的单元测试框架,应用十分宽泛。
(2)特定畛域的规范测试框架。
(3)可能在多种 IDE 开发平台应用,蕴含 Idea、Eclipse 中进行集成。
(4)可能不便由 Maven 引入应用。
(5)能够不便的编写单元测试代码,查看测试后果等。
JUnit 的重要概念:
名称 | 性能作用 |
---|---|
Assert | 断言办法汇合 |
TestCase | 示意一个测试案例 |
TestSuite | 蕴含一组 TestCase,形成一组测试 |
TestResult | 收集测试后果 |
JUnit 的一些注意事项及标准:
(1)测试方法必须应用 @Test 润饰
(2)测试方法必须应用 public void 进行润饰,不能带参数
(3)测试代码的包应该和被测试代码包构造保持一致
(4)测试单元中的每个办法必须能够独立测试,办法间不能有任何依赖
(5)测试类个别应用 Test 作为类名的后缀
(6)测试方法使个别用 test 作为办法名的前缀
JUnit 失败后果阐明:
(1)Failure:测试后果和预期后果不统一导致,示意测试不通过
(2)error:由异样代码引起,它能够产生于测试代码自身的谬误,也能够是被测代码的 Bug
3.2 JUnit 内容
(1)断言的 API
断言办法 | 断言形容 |
---|---|
assertNull(String message, Object object) | 查看对象是否为空,不为空报错 |
assertNotNull(String message, Object object) | 查看对象是否不为空,为空报错 |
assertEquals(String message, Object expected, Object actual) | 查看对象值是否相等,不相等报错 |
assertTrue(String message, boolean condition) | 查看条件是否为真,不为真报错 |
assertFalse(String message, boolean condition) | 查看条件是否为假,为真报错 |
assertSame(String message, Object expected, Object actual) | 查看对象援用是否相等,不相等报错 |
assertNotSame(String message, Object unexpected, Object actual) | 查看对象援用是否不等,相等报错 |
assertArrayEquals(String message, Object[] expecteds, Object[] actuals) | 查看数组值是否相等,遍历比拟,不相等报错 |
assertArrayEquals(String message, Object[] expecteds, Object[] actuals) | 查看数组值是否相等,遍历比拟,不相等报错 |
assertThat(String reason, T actual, Matcher<? super T> matcher) | 查看对象是否满足给定规定,不满足报错 |
(2)JUnit 罕用注解:
1)@Test: 定义一个测试方法 @Test(excepted=xx.class): xx.class 示意异样类,示意测试的办法抛出此异样时,认为是失常的测试通过的 @Test(timeout = 毫秒数) : 测试方法执行工夫是否合乎预期。
2)@BeforeClass:在所有的办法执行前被执行,static 办法全局只会执行一次,而且第一个运行。
3)@AfterClass:在所有的办法执行之后进行执行,static 办法全局只会执行一次,最初一个运行。
4)@Before:在每一个测试方法被运行前执行一次。
5)@After:在每一个测试方法运行后被执行一次。
6)@Ignore:所润饰的测试方法会被测试运行器疏忽。
7)@RunWith:能够更改测试执行器应用 junit 测试执行器。
3.3 JUnit 应用
3.3.1 Controller 层单元测试
(1)Springboot 中应用 maven 引入 Junit 非常简单, 应用如下依赖即可引入:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
(2)下面应用 main 办法案例能够应用如下的 Junit 代码实现:
@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,且不须要写申请代码,这些都由工具主动实现。
(3)案例中相干组件介绍
本案例中结构 mockMVC 对象时,也能够应用如下形式:
@Autowired
private StudentController studentController;
@Before
public void setupMockMvc(){
// 初始化 MockMvc 对象
mockMvc = MockMvcBuilders.standaloneSetup(studentController).build();}
其中 MockMVC 是 Spring 测试框架提供的用于 REST 申请的工具,是对 Http 申请的模仿,无需启动整个模块就能够对 Controller 层进行调用,速度快且不依赖网络环境。
应用 MockMVC 的根本步骤如下:
1) mockMvc.perform 执行申请
2) MockMvcRequestBuilders.post 或 get 结构申请
3) MockHttpServletRequestBuilder.param 或 content 增加申请参数
4) MockMvcRequestBuilders.contentType 增加申请类型
5) MockMvcRequestBuilders.accept 增加响应类型
6) ResultActions.andExpect 增加后果断言
7) ResultActions.andDo 增加返回后果后置解决
8) ResultActions.andReturn 执行实现后返回相应后果
3.3.2 Service 层单元测试
能够编写如下代码对 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("张三"));
}
}
执行后果:
3.3.3 Dao 层单元测试
能够编写如下代码对 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) 能够执行单元测试之后回滚所新增的数据,放弃数据库不产生脏数据。
3.3.4 异样测试
(1)在 service 层定义一个异常情况:
public void computeScore() {int a = 10, b = 0;}
(2)在 service 的测试类中定义单元测试办法:
@Test(expected = ArithmeticException.class)
public void computeScoreTest() {studentService.computeScore();
}
(3)执行单元测试也会通过,起因是 @Test 注解中的定义了异样
3.3.5 测试套件测多个类
(1)新建一个空的单元测试类
(2)利用注解 @RunWith(Suite.class) 和 @SuiteClasses 表明要一起单元测试的类
@RunWith(Suite.class)
@Suite.SuiteClasses({StudentServiceTest.class, StudentDaoTest.class})
public class AllTest {}
运行后果:
3.3.6 idea 中查看单元测试覆盖率
(1)单测覆盖率
测试覆盖率是掂量测试过程工作自身的有效性,晋升测试效率和缩小程序 bug,晋升产品可靠性与稳定性的指标。
统计单元测试覆盖率的意义:
1)能够洞察整个代码中的根底组件性能的所有盲点,发现相干问题。
2)进步代码品质,通常覆盖率低示意代码品质也不会太高,因为单测不通过原本就映射出思考到各种状况不够充沛。
3)从覆盖率的达标上能够进步代码的设计能力。
(2)在 idea 中查看单元测试覆盖率很简略,只需依照图中示例的图标运行,或者在单元测试办法或类上右键 Run ‘xxx’ with Coverage 即可。执行后果是一个表格,列出了类、办法、行数、分支笼罩状况。
(3)在代码中会标识出笼罩状况,绿色的是已笼罩的,红色的是未笼罩的。
(4)如果想要导出单元测试的覆盖率后果,能够应用如下图所示的形式,勾选 Open generated HTML in browser
导出后果:
3.3.7 JUnit 插件主动生成单测代码
(1)装置插件,重启 idea 失效
(2)配置插件
(3)应用插件
在须要生成单测代码的类上右键 generate…,如下图所示。
生成后果:
4. 单元测试工具 -Mockito
4.1 Mockito 简介
在单元测试过程中主张不要依赖特定的接口与数据起源,此时就波及到对相干数据的模仿,比方 Http 和 JDBC 的返回后果等,能够应用虚构对象即 Mock 对象进行模仿,使得单元测试不在耦合。
Mock 过程的应用前提:
(1)理论对象时很难被结构进去的
(2)理论对象的特定行为很难被触发
(3)理论对象可能以后还不存在,比方依赖的接口还没有开发实现等等。
Mockito 官网:https://site.mockito.org。Mockito 和 JUnit 一样是专门针对 Java 语言的 mock 数据框架,它与同类的 EasyMock 和 jMock 性能十分类似,然而该工具更加简略易用。
Mockito 的特点:
(1)能够模仿类不仅仅是接口
(2)通过注解形式简略易懂
(3)反对程序验证
(4)具备参数匹配器
4.2 Mockito 应用
maven 引入 spring-boot-starter-test 会主动将 mockito 引入到工程中。
4.2.1 应用案例
(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)执行后果:
(5)后果解析
上述内容并没有实现 BookService 接口的 orderBook(String name) 办法。然而应用 mockito 进行模仿数据之后,却通过了单元测试,起因就在于 Mockito 替换了原本要在 StudentService 的 orderBook 办法中获取的对象,此处就模仿了该对象很难获取或以后无奈获取到,用模仿数据进行代替。
4.2.2 相干语法
罕用 API:
上述案例中用到了 mockito 的 when、any、theWhen 等语法。接下来介绍下都有哪些罕用的 API:
1) mock:模仿一个须要的对象
2) when:个别配合 thenXXX 一起应用,示意当执行什么操作之后怎么。
3) any: 返回一个特定对象的缺省值,上例中标识能够填写任何 String 类型的数据。
4) theReturn: 在执行特定操作后返回指定后果。
5) spy:发明一个监控对象。
6) verify:验证特定的行为。
7) doReturn:返回后果。
8) doThrow:抛出特定异样。
9) doAnswer:做一个自定义响应。
10) times:操作执行次数。
11) atLeastOnce:操作至多要执行一次。
12) atLeast:操作至多执行指定的次数。
13) atMost:操作至少执行指定的次数。
14) atMostOnce:操作至少执行一次。
15) doNothing:不做任何的解决。
16) doReturn:返回一个后果。
17) doThrow:抛出一个指定异样。
18) doAnswer:指定一个特定操作。
19) doCallRealMethod:用于监控对象返回一个实在后果。
4.2.3 应用要点
(1)打桩
Mockito 中有 Stub,所谓存根或者叫打桩的概念,下面案例中的 Mockito.when(bookService.orderBook(any(String.class))).thenReturn(expectBook); 就是打桩的含意,先定义好如果依照既定的形式调用了什么,后果就输入什么。而后在应用 Book book = studentService.orderBook(“”); 即依照指定存根输入指定后果。
@Test
public void verifyTest() {List mockedList = mock(List.class);
mockedList.add("one");
verify(mockedList).add("one"); // 验证通过,因为后面定义了这个桩
verify(mockedList).add("two"); // 验证失败,因为后面没有定义了这个桩
}
(2)参数匹配
上例 StudentService 的 orderBook 办法中的 any(String.class) 即为参数匹配器,能够匹配任何此处定义的 String 类型的数据。
(3)次数验证
@Test
public void timesTest() {List mockedList = mock(List.class);
when(mockedList.get(anyInt())).thenReturn(1000);
System.out.println(mockedList.get(1));
System.out.println(mockedList.get(1));
System.out.println(mockedList.get(1));
System.out.println(mockedList.get(2));
// 验证通过:get(1) 被调用 3 次
verify(mockedList, times(3)).get(1);
// 验证通过:get(1) 至多被调用 1 次
verify(mockedList, atLeastOnce()).get(1);
// 验证通过:get(1) 至多被调用 3 次
verify(mockedList, atLeast(3)).get(1);
}
(4)程序验证
@Test
public void orderBookTest1() {String json = "{"id":12,"location":" 书架 A12","name":" 三国演义 "}";
String json1 = "{"id":21,"location":" 书架 A21","name":" 水浒传 "}";
String json2 = "{"id":22,"location":" 书架 A22","name":" 红楼梦 "}";
String json3 = "{"id":23,"location":" 书架 A23","name":" 西游记 "}";
when(bookService.orderBook("")).thenReturn(JSON.parseObject(json, Book.class));
Book book = bookService.orderBook("");
Assert.assertTrue("预约书籍有误", "三国演义".equals(book.getName()));
when(bookService.orderBook("")).thenReturn(JSON.parseObject(json1, Book.class)).
thenReturn(JSON.parseObject(json2, Book.class)).
thenReturn(JSON.parseObject(json3, Book.class));
Book book1 = bookService.orderBook("");
Book book2 = bookService.orderBook("");
Book book3 = bookService.orderBook("");
Book book4 = bookService.orderBook("");
Book book5 = bookService.orderBook("");
// 全副验证通过,按程序最初打桩打了 3 次,大于 3 次依照最初对象输入
Assert.assertTrue("预约书籍有误", "水浒传".equals(book1.getName()));
Assert.assertTrue("预约书籍有误", "红楼梦".equals(book2.getName()));
Assert.assertTrue("预约书籍有误", "西游记".equals(book3.getName()));
Assert.assertTrue("预约书籍有误", "西游记".equals(book4.getName()));
Assert.assertTrue("预约书籍有误", "西游记".equals(book5.getName()));
}
(5)异样验证
@Test(expected = RuntimeException.class)
public void exceptionTest() {List mockedList = mock(List.class);
doThrow(new RuntimeException()).when(mockedList).add(1);
// 验证通过
mockedList.add(1);
}