作者:京东物流 秦彪
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对象时,也能够应用如下形式:
@Autowiredprivate StudentController studentController;@Beforepublic 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)@SpringBootTestpublic 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)@SpringBootTestpublic 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办法
@Testpublic 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); }