一、单元测试
1.单元测试是什么?
借用一下百度百科的话,
单元测试(unit testing),是指对软件中的最小可测试单元进行检查和验证。对于单元测试中单元的含义,一般来说,要根据实际情况去判定其具体含义,如C语言中单元指一个函数,Java里单元指一个类,图形化的软件中可以指一个窗口或一个菜单等。总的来说,单元就是人为规定的最小的被测功能模块。单元测试是在软件开发过程中要进行的最低级别的测试活动,软件的独立单元将在与程序的其他部分相隔离的情况下进行测试。
这里有几个关键点:
单元是人为规定的
单元测试是独立单元,要和其他部分相分离。
2.为什么需要单元测试?
这里谈谈我自己的感受,就我自己而言,是没有单元测试的习惯的,我感觉单元测试会非常耗费时间,同时我认为这些时间花费的不太值得,因为在我初学阶段,做的都是一些简单的crud项目。
但是随着我开发的项目越来越大,需求越来越复杂,我渐渐发现我做的项目质量越来越不稳定,常常会出现一些奇怪的bug。当出现bug时,我们往往要定位问题的所在。就比如前段时间我自己做的一个通用物联网平台,在录制演示视频时输入矫正公式(支持四则运算),当时在其他设备下都能正常运作,但偏偏那次出现了异常。好在整个项目都是自己做的,对于一些实现细节上也都心里有数,调试了一会后发现是算法问题,那时我才猛然想起自己写的时候明白这个算法实现不支持负数,如果要进行负数运算得变成“(0-x)”的形式,而恰巧那台设备上传的数据是负数,所以出现了问题。
好在是全栈开发,所有东西都是自己做的,如果这个项目是团队开发,我估计定位bug的所耗费的时间将会指数级增长。
正因为在集成测试等大规模测试中,定位bug所耗费的时间实在是太长了,所以我们需要单元测试来保证每个小模块的正确性。 尽管它会耗费更多的时间,但是这些时间比起后期层出不穷的bug以及解决bug所耗费的是时间,这些都是值得的。
在开发项目的过程中,很多时候都是在解决之前的bug遗留。
我在网上看到过相关的总结,写的非常好分享一下——单元测试到底是什么?应该怎么做?
单元测试对我们的产品质量是非常重要的。
单元测试是所有测试中最底层的一类测试,是第一个环节,也是最重要的一个环节,是唯一一次有保证能够代码覆盖率达到100%的测试,是整个软件测试过程的基础和前提,单元测试防止了开发的后期因bug过多而失控,单元测试的性价比是最好的。
据统计,大约有80%的错误是在软件设计阶段引入的,并且修正一个软件错误所需的费用将随着软件生命期的进展而上升。错误发现的越晚,修复它的费用就越高,而且呈指数增长的趋势。
作为编码人员,也是单元测试的主要执行者,是唯一能够做到生产出无缺陷程序这一点的人,其他任何人都无法做到这一点代码规范、优化,可测试性的代码
放心重构
自动化执行three-thousand times
二、Junit
1.什么是junit
JUnit是一个Java语言的单元测试框架。它由Kent Beck和Erich Gamma建立,逐渐成为源于Kent Beck的sUnit的xUnit家族中为最成功的一个。 JUnit有它自己的JUnit扩展生态圈。
目前junit已经发展到了junit5,相较于junit4有了很大的改变。JUnit5由来自三个不同子项目的几个不同模块组成。
JUnit 5=JUnit平台+JUnit Jupiter+JUnit Vintage
详见官网
注:junit基本上Java单元测试的主流,现今大多数Java项目都有junit的身影
2.Junit概念——断言
刚接触过单元测试的同学在学习junit时肯定会疑惑assert方法到底是什么意思,什么叫断言。我一开始接触时就是这样,疑惑断言是干嘛的。
其实断言其实是一些辅助函数,他们用来帮助我们确定被测试的方法是否按照预期的效果正常工作,通常,把这些辅助函数称为断言。
3.Junit的简单使用
以下演示为maven项目
①导入依赖
<dependencies> <!-- ... --> <dependency> <groupId>org.junit.jupiter</groupId> <artifactId>junit-jupiter</artifactId> <version>5.8.1</version> <scope>test</scope> </dependency> <!-- ... --> </dependencies> <build> <plugins> <plugin> <artifactId>maven-surefire-plugin</artifactId> <version>2.22.2</version> </plugin> <plugin> <artifactId>maven-failsafe-plugin</artifactId> <version>2.22.2</version> </plugin> </plugins> </build>
②编写测试用例
这里引用官网上的例子
import static org.junit.jupiter.api.Assertions.assertEquals; import example.util.Calculator; import org.junit.jupiter.api.Test; class MyFirstJUnitJupiterTests { private final Calculator calculator = new Calculator(); @Test void addition() { assertEquals(2, calculator.add(1, 1)); } }
上面这个例子就是断言了calculator.add(1, 1)的返回值会等于2。
在idea中运行测试将会很方便,只需点击运行图标即可
如果不是idea中,也只需加个mian函数运行即可。
如果运行断言正确,那么程序会如下:
如果断言错误,junit会给你抛出一个AssertionFailedError异常,并告诉你出错的情况
4.SpringBoot环境下的junit使用
当然我们在实际开发中,比如在SpringBoot环境下开发,这时很多业务代码类都是被注入到Spring容器,而类之间又有其他注入类的依赖,像之前那样创建一个测试对象显然不现实。那有什么办法能解决这个问题呢?
下面我来介绍一下junit在SpringBoot+SSM项目中的使用。
①导入依赖
在SpringBoot中它将依赖进行整合,如果我们需要测试的相关依赖,只需引入对应的测试模块即可
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> <exclusions> <exclusion> <groupId>org.junit.vintage</groupId> <artifactId>junit-vintage-engine</artifactId> </exclusion> </exclusions> </dependency>
②编写测试用例
测试对象类
package com.example.demo.service; import org.springframework.stereotype.Component; @Component public class Junit5Test { public int add(int i,int j){ System.out.println("-----------add被执行了---------------"); return i+j; } public int doAdd(int i,int j){ System.out.println("------------doAdd被执行了--------------"); //被mock的函数会先执行,且只会执行一次 System.out.println(add(i,j)); return add(i,j); } }
测试用例
import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; 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.SpyBean; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.Mockito.when; //初始化一个spring的上下文,使其可以使用一些注入(junit5)。junit4会用runwith @SpringBootTest class Junit5TestTest { @Autowired Junit5Test junit5Test; //会初始化一次 @BeforeAll static void init(){ System.out.println("init"); } //所有测试方法前都会执行一遍 @BeforeEach void each(){ System.out.println("each"); } @Test void getDeviceStatistic() { Assertions.assertEquals(2,spyJunit5Test.doAdd(1,1)); } }
如果需要SpringBoot上下文环境只需在其上加个@SpringBootTest注解即可,当然在老项目中我们可能会看到@RunWith(SpringRunner.class)这种写法。前者是junit5的写法,后者是junit4的写法。
当我们需要spring容器中的测试对象时,我们只需正常注入即可。
@Autowired Junit5Test junit5Test;
三、模拟数据——mockito框架的使用
1.mock
在实际开发进行单测时,我们测试对象很可能需要请求网络数据或者改变数据库,可是我们又不想让它去变化,这时我们可以使用mockito框架来对数据进行mock。
所谓的mock,就是指,如果我们写的代码依赖于某些对象,而这些对象又很难手动创建(即不知道如何初始化等,像HttpRequest等对象),那么就用一个虚拟的对象来测试。因为它传入的是一个class文件,所以static代码块还是会被运行,但构造函数,实例代码块都不会被执行。
2.打桩Stub
所谓打桩Stub,就是用来提供测试时所需要的测试数据,因为是mock的对象,所以可能有些方法并不能知道返回值,因此我们需要去假定返回值。可以对各种交互设置相应的回应,即对方法设置调用返回值,使用when(…).thenReturn(…)和doReturn(…).when(…)。
比如:
//You can mock concrete classes, not only interfaces LinkedList mockedList = mock(LinkedList.class); //stubbing when(mockedList.get(0)).thenReturn("first"); when(mockedList.get(1)).thenThrow(new RuntimeException());
doReturn().when()是无副作用的。打桩的同时不会执行方法。
when().thenReturn()是有副作用的,其副作用是指在打桩的同时会先执行一遍方法,这时可能会造成一定的副作用。
3.@MockBean和@SpyBean
当然在SpringBoot的环境下也可以直接@SpyBean和@MockBean注解来替代@Autowired的注入对象,这样就有了一个虚拟的对象。
@MockBean
如果仅使用@MockBean,会将修饰的对象mock掉,这样Junit5Test的add()方法就不再执行具体的细节,但是MockBean会将目标对象的所有方法全部mock,所以test不能真实地被执行,也就无法测试了。
@SpyBean
而有些情况我们又需要执行真实的方法,我们只想对某些方法进行mock,这时就可以使用@SpyBean。
使用@SpyBean修饰的spyJunit5Test是一个真实对象,仅当when(spyJunit5Test.add(1,1)).thenReturn(2);时,add方法被打桩,其他的方法仍被真实调用。
以下是示例
package com.example.demo.service; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; 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.SpyBean; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.Mockito.when; //初始化一个spring的上下文,使其可以使用一些注入(junit5)。junit4会用runwith @SpringBootTest class Junit5TestTest { // @Autowired // Junit5Test junit5Test; //介于@Autowired和@MockBean之间的注解,当配置了when时使用mock,没有进行打桩则走正常方法 @SpyBean Junit5Test spyJunit5Test; //完全使用mock方法,方法都不会去真正执行。当调用mockBean修饰的方法时,不会去真正执行该方法,只会返回打桩后的值,如果没有打桩时会返回默认值,比如int就返回0。 // @MockBean // Junit5Test mockJunit5Test; //会初始化一次 @BeforeAll static void init(){ System.out.println("init"); } //所有测试方法前都会执行一遍 @BeforeEach void each(){ System.out.println("each"); } @Test void getDeviceStatistic() { Assertions.assertEquals(1,1); } @Test void testDeviceStatisticByMock() { //配置mock when(spyJunit5Test.add(1,1)).thenReturn(2); Assertions.assertEquals(2,spyJunit5Test.doAdd(1,1)); } }
最后,祝大家程序员节快乐!