您好!
欢迎来到京东云开发者社区
登录
首页
博文
课程
大赛
工具
用户中心
开源
首页
博文
课程
大赛
工具
开源
更多
用户中心
开发者社区
>
博文
>
Java单元测试浅析(JUnit+Mockito)
分享
打开微信扫码分享
点击前往QQ分享
点击前往微博分享
点击复制链接
Java单元测试浅析(JUnit+Mockito)
京东云开发者
2023-03-20
IP归属:北京
27840浏览
测试
**作者:京东物流 秦彪** # 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/](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](https://my.oschina.net/azibug) 修饰 (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](https://my.oschina.net/azibug): 定义一个测试方法 @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,且不需要写请求代码,这些都由工具自动完成。 ![](https://oscimg.oschina.net/oscnet/up-fc2d51e083fc2074913817176810cb7b71a.png) (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("张三")); } } ``` 执行结果: ![](https://oscimg.oschina.net/oscnet/up-9e961c1d1513c88b6042d4d23eaa2f33f0a.png) ### 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); } } ``` ![](https://oscimg.oschina.net/oscnet/up-bec18abd80a07995b03cf2bb40da93d3f3b.png) 其中@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注解中的定义了异常 ![](https://oscimg.oschina.net/oscnet/up-5b600e82301981797fc5ea4299bd9c65c2f.png) ### 3.3.5 测试套件测多个类 (1) 新建一个空的单元测试类 (2) 利用注解@RunWith(Suite.class)和@SuiteClasses标明要一起单元测试的类 ``` @RunWith(Suite.class) @Suite.SuiteClasses({ StudentServiceTest.class, StudentDaoTest.class}) public class AllTest { } ``` 运行结果: ![](https://oscimg.oschina.net/oscnet/up-7bf029c6805bf4a47d9107bfd1a2380e209.png) ### 3.3.6 idea中查看单元测试覆盖率 (1) 单测覆盖率 测试覆盖率是衡量测试过程工作本身的有效性,提升测试效率和减少程序bug,提升产品可靠性与稳定性的指标。 统计单元测试覆盖率的意义: 1) 可以洞察整个代码中的基础组件功能的所有盲点,发现相关问题。 2) 提高代码质量,通常覆盖率低表示代码质量也不会太高,因为单测不通过本来就映射出考虑到各种情况不够充分。 3) 从覆盖率的达标上可以提高代码的设计能力。 (2) 在idea中查看单元测试覆盖率很简单,只需按照图中示例的图标运行,或者在单元测试方法或类上右键Run 'xxx' with Coverage即可。执行结果是一个表格,列出了类、方法、行数、分支覆盖情况。 ![](https://oscimg.oschina.net/oscnet/up-63e45c56510e1b0f77836cd922b0869cd3c.png) (3) 在代码中会标识出覆盖情况,绿色的是已覆盖的,红色的是未覆盖的。 ![](https://oscimg.oschina.net/oscnet/up-3d2fc716decf2bbb1b43985dc9b7db4232f.png) (4) 如果想要导出单元测试的覆盖率结果,可以使用如下图所示的方式,勾选 Open generated HTML in browser ![](https://oscimg.oschina.net/oscnet/up-9a945ef981f9a21315d30e8cb38685512e5.png) 导出结果: ![](https://oscimg.oschina.net/oscnet/up-01cb19c96eef8b42bd264b575e3c7d36c9a.png) ### 3.3.7 JUnit插件自动生成单测代码 (1) 安装插件,重启idea生效 ![](https://oscimg.oschina.net/oscnet/up-a8327273908109e3e96d17711043daea240.png) (2) 配置插件 ![](https://oscimg.oschina.net/oscnet/up-aef4131c2b247a4a35e55472f5dfd0fabce.png) ![](https://oscimg.oschina.net/oscnet/up-32d1ee9ff42d63f9c79d8bde078e5e84ae8.png) (3) 使用插件 在需要生成单测代码的类上右键generate...,如下图所示。 ![](https://oscimg.oschina.net/oscnet/up-4b042f2a58cf93c5d0439edcff414f1ada0.png) 生成结果: ![](https://oscimg.oschina.net/oscnet/up-087939cf5561cc2473d4f3e750cd02633f7.png) # 4\. 单元测试工具-Mockito ## 4.1 Mockito简介 在单元测试过程中主张不要依赖特定的接口与数据来源,此时就涉及到对相关数据的模拟,比如Http和JDBC的返回结果等,可以使用虚拟对象即Mock对象进行模拟,使得单元测试不在耦合。 Mock过程的使用前提: (1) 实际对象时很难被构造出来的 (2) 实际对象的特定行为很难被触发 (3) 实际对象可能当前还不存在,比如依赖的接口还没有开发完成等等。 Mockito官网:[https://s](https://junit.org/)[ite.mockito.org](http://ite.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) 执行结果: ![](https://oscimg.oschina.net/oscnet/up-e4c112c0dcf50a0301b478e41309b703342.png) (5) 结果解析 上述内容并没有实现BookService接口的orderBook(String name)方法。但是使用mockito进行模拟数据之后,却通过了单元测试,原因就在于Mockito替换了本来要在StudentService的orderBook方法中获取的对象,此处就模拟了该对象很难获取或当前无法获取到,用模拟数据进行替代。 ![](https://oscimg.oschina.net/oscnet/up-441a60d3978d59c235898e670faa5b89a75.png) ### 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); } ```
原创文章,需联系作者,授权转载
上一篇:精准测试之覆盖
下一篇:cookie时效无限延长方案
相关文章
安全测试之探索windows游戏扫雷
Jmeter压测实战:Jmeter二次开发之JSF采样器实现
Laputa自动化测试框架介绍
京东云开发者
文章数
92
阅读量
207747
作者其他文章
01
安全测试之探索windows游戏扫雷
扫雷游戏相信很多人都从小玩过,在那个电脑游戏并不多的时代,扫雷成为玩的热度蛮高的一款游戏之一,然而就在有一次,接触到了一次不寻常的扫雷过程,使得后来我也有了这个冲动,也来做一次。通过动态调试,逆向和C来写一个扫雷辅助工具从而提高逆向与编码技能。
01
与时俱进,京东云高性能计算平台
01
https 的本质、证书验证过程以及数据加密
01
《京东金融APP的鸿蒙之旅系列专题》新特性篇:意图框架接入
京东云开发者
文章数
92
阅读量
207747
作者其他文章
01
安全测试之探索windows游戏扫雷
01
与时俱进,京东云高性能计算平台
01
https 的本质、证书验证过程以及数据加密
01
《京东金融APP的鸿蒙之旅系列专题》新特性篇:意图框架接入
添加企业微信
获取1V1专业服务
扫码关注
京东云开发者公众号