单元测试
测试驱动开发TDD
- 编写的单元测试应该易于执行(PS:执行个破单元测试,竟然还要搭建个FTP服务器,Fuck!)
- 编写的单元测试应该能够快速执行(PS:执行个破单元测试,竟然要2分钟,我直接启动应用测得了,写个毛单元测试啊)
- 单元测试的执行不能依赖外部环境,不能因为环境的改变导致单元测试执行失败
- 不能依赖数据库中已有的数据做单元测试
- 执行单元测试前后不能对数据库的数据产生任何影响,都要回滚
- 不能依赖远程的接口调用,所有的远程接口调用都要mock
一个单元测试的执行,不应该依赖于外部环境。当单元测试依赖依赖外部环境,例如某一个服务,可以使用mock模拟服务的接口返回数据,从而屏蔽掉外部依赖。
/**
* 注:
* 一)单元测试命名规范
* 类名:被测试类 + Tests
* 方法:被测试方法 + Test
* 二)@Before,@After,@BeforeClass,@AfterClass 标示的方法一个测试类中只能各有一个
*/
public class ExampleTests {
/**
* 在所有测试方法之前运行,只运行一次。
* 一般在此类中申请昂贵的外部资源。父类中有@BeforeClass方法,在其子类运行之前也会运行。
*/
@BeforeClass
public void beforeClass() {
}
/**
* 每个测试方法执行之前都要执行一次。
*/
@Before
public void before() {
}
/**
* 每个测试方法执行之后要执行一次。
*/
@After
public void after() {
}
/**
* 与BeforeClass对应,在所有测试结束后,释放BeforeClass中申请的资源。
*/
@AfterClass
public static void afterClass() {
}
/**
* 测试用例
* 一个测试用例主体内容一般采用三段式:given-when-then
* - Given:构造测试条件;
* - When:执行待测试的方法;
* - Then:判断测试结果是否符合期望。
*/
@Test
public void xxxTest() {
//step1)Given:构造测试条件;
int a = 1;
int b = 2;
//step2)When:执行待测试的方法;
int c = MyMath.add(a, b);
//step3)Then:判断测试结果是否符合期望。
assertEquals(3, c);
}
}
Mock
在做单元测试的时候,我们会发现我们要测试的方法会引用很多外部依赖的对象,而我们没法控制这些外部依赖的对象。 为了解决这个问题,我们需要用到Mock来模拟这些外部依赖的对象,从而控制它们。简述就是mock通过创建模拟对象,代替需要的真实对象。
例如:
Controller从 Service中获取数据,而 Service中数据又来源于各种Dao层(mysql ,redis,es,mongo等),这时可以利用 Mock 去构造虚拟的各种Dao对象用于Service的测试,因为我们只是想测试Service的行为是否符合预期,并不需要去测试其依赖的各种Dao层对象。
Mock 框架:EasyMock,JMock,Mockito,PowerMock
Mockito
Mockito是Spring Boot框架内置的mock框架,引入spring-boot-starter-test就会自动引入mockito。
Mockito 官方手册:https://javadoc.io/doc/org.mockito/mockito-core/latest/org/mockito/Mockito.html
Mockito 三方教程:https://www.tutorialspoint.com/mockito/
我们在执行单元测试时,需要将外部依赖进行Mock,那到底哪些外部依赖需要被Mock呢?
理论上,其实MySQL、MongoDB、ElasticSearch也属于外部依赖,但是由于他们都不是特别好Mock,而且程序的核心逻辑就是增删改查数据库,所以不建议Mock,而是使用真实的数据库。
组件 | 是否Mock |
---|---|
MySQL | N |
MongoDB | N |
ElasticSearch | N |
Feign接口 | Y |
Redis | Y |
Kafka | Y |
RabbitMQ | Y |
FTP | Y |
外部接口 | Y |
pom.xml
spring-boot-starter-test中已经加入了Mockito依赖,所以无需手动引入。
代码实现
mock可以模拟各种各样的对象,从而代替真正的对象做出希望的响应。
//模拟对象的创建:方式一
@Mock
需要模拟的类 obj;
//模拟对象的创建:方式二
需要模拟的类 obj= PowerMockito.mock(需要模拟的类.class);
需要模拟的类 obj= mock(需要模拟的类.class);
//模拟对象调用方法:设置方法的返回值
when(obj.方法()).thenReturn(返回值);
//模拟对象的创建:方式三
@Spy
需要模拟的类 obj;
//模拟对象的创建:方式四
需要模拟的类 obj= PowerMockito.spy(需要模拟的类.class);
需要模拟的类 obj= spy(需要模拟的类.class);
//模拟对象调用方法:真实对象,可以直接调用方法,获取返回值。
返回值 = obj.方法();
//模拟对象调用方法:会先调用真实的方法获得返回值,再设置方法的返回值
when(obj.方法()).thenReturn(返回值);
//断言
assertEquals(实际值, 预期值);
//确保模拟对象的某个方法被调用过
verify(obj).方法();
创建模拟对象
MockIto初始化模拟对象的方法
public class XxxTest {
@Before
public void initMocks() {
MockitoAnnotations.initMocks(this);
}
}
@RunWith(MockitoJUnitRunner.class)
public class XxxTest {
}
mock(XXX.class);
对于模拟的初始化,使用运行器或MockitoAnnotations.initMocks
是严格等价的解决方案。
在MockitoJUnitRunner的javadoc中:
JUnit 4.5 runner初始化使用模拟注释的模拟,因此不需要显式使用MockitoAnnotations.initMocks(对象)。Mock在每个测试方法之前被初始化。
当您已经在测试用例上配置了特定的运行器(例如,SpringJUnit4ClassRunner
)时,可以使用第一个解决方案(使用MockitoAnnotations.initMocks
)。
第二种解决方案(使用MockitoJUnitRunner
)更经典,也是我最喜欢的。代码更简单。使用runner提供了的巨大优势(由@David Wallace在this answer中描述)。
这两种解决方案都允许在测试方法之间共享模拟(和间谍)。再加上@InjectMocks
,它们可以非常快速地编写单元测试。减少了样板模拟代码,测试更容易阅读。例如:
//模拟对象的创建:方式一
@Mock
需要模拟的类 obj;
//模拟对象的创建:方式二
需要模拟的类 obj= PowerMockito.mock(需要模拟的类.class);
需要模拟的类 obj= mock(需要模拟的类.class);
//模拟对象调用方法:设置方法的返回值
when(obj.方法()).thenReturn(返回值);
//模拟对象的创建:方式三
@Spy
需要模拟的类 obj;
//模拟对象的创建:方式四
需要模拟的类 obj= PowerMockito.spy(需要模拟的类.class);
需要模拟的类 obj= spy(需要模拟的类.class);
@InjectMocks
创建一个实例,简单的说是这个Mock可以调用真实代码的方法,其余用@Mock(或@Spy)注解创建的mock将被注入到用该实例中。
@Mock
对函数的调用均执行mock(即虚假函数),不执行真正部分。
@Spy
对函数的调用均执行真正部分。
Mockito中的Mock和Spy都可用于拦截那些尚未实现或不期望被真实调用的对象和方法,并为其设置自定义行为。二者的区别在于Mock不真实调用,Spy会真实调用。
-
@Mock
对该对象所有非私有方法的调用都没有调用真实方法
对该对象私有方法的调用无法进行模拟,会调用真实方法
@Spy
对该对象所有方法的调用都直接调用真实方法
-
日常测试中我们往往只需要Mock一个对象中的某些方法,而非全部方法,因此@Spy更便于我们做Mock测试。
-
对于测试的类,如果有继承的话,需要mock父类中引入的对象,不然模拟对象调用方法时会报错。
打桩/存根
对某个方法指定返回策略的操作(具体表现为两种:1指定返回值,2使用doCallRealMethod()或者thenCallRealMethod()指定当方法被调用时执行实际代码逻辑),功能就是当测试执行到此方法时直接返回我们指定的返回值(此时不会执行此方法的实际代码逻辑)或者执行此方法的实际代码逻辑并返回。
指定返回值
//模拟对象调用方法:真实对象,可以直接调用方法,获取返回值。
返回值 = obj.方法();
when-thenXxx
//模拟对象调用方法:会先调用真实的方法获得返回值,再设置方法的返回值
when(obj.方法()).thenReturn(返回值);
Assert断言
Junit所有的断言都包含在Assert类中。
单元测试用于判断某个特定条件下某个方法的行为;执行单元测试为了证明某段代码的执行结果和期望的一致
只有失败的断言才会被记录,Assert 类中的一些有用的方法列式如下:
void assertEquals(boolean expected, boolean actual)
:检查两个对象是否相等;类似于字符串比较使用的equals()方法。expected为用户期望某一时刻对象的值,actual为某一时刻对象实际的值。void assertSame(boolean condition)
:查看两个对象的引用是否相等;类似于使用“==”比较两个对象void assertArrayEquals(expectedArray, resultArray)
:检查两个数组是否相等void assertTrue(boolean expected, boolean actual)
:检查条件为真void assertNull(Object object)
:检查对象为空- assertThat(String reason, T actual, Matcher matcher) :要求matcher.matches(actual) == true,使用Matcher做自定义的校验。
验证方法调用
//确保模拟对象的某个方法被调用过
verify(mock).方法();
方法是否被调用/方法的调用的次数
//允许至少 x 调用的验证。
atLeast(int minNumberOfInvocations)
//允许至少一次调用的验证。
atLeastOnce()
//允许最多 x 次调用的验证。
atMost(int maxNumberOfInvocations)
//允许最多一次调用的验证。
atMostOnce()
//times(0)的别名,见times(int) 。
never()
//允许检查给定的方法是否只调用一次。
only()
//允许验证调用的确切次数。
times(int wantedNumberOfInvocations)
//验证某些行为发生过一次。
verify(mock).方法();
//验证某些行为至少发生过一次/确切的次数/从未发生过。
verify(T mock, VerificationMode mode)
//验证给定的模拟上没有发生交互。
verifyNoInteractions(Object... mocks)
//检查任何给定的模拟是否有任何未经验证的交互。
verifyNoMoreInteractions(Object... mocks)
方法执行的时间
after(long millis)在给定的毫秒数后将触发验证,允许测试异步代码。
timeout(long millis)验证将一遍又一遍地触发,直到给定的毫秒数,允许测试异步代码。
调用顺序验证
calls(int wantedNumberOfInvocations)允许按顺序进行非贪婪调用的验证。
inOrder(Object… mocks)创建InOrder对象,允许按顺序验证mock的对象。
常见问题
eq()
any()
MockMvc
A类中依赖注入了B类,在写A的测试类时要注意:对A使用@InjectMocks注解,对于B使用@Mock注解
Controller层单元测试不需要启动应用,也就不需要给类名加任何Test相关的注解
注意import相关包的方式,有些人喜欢以static的方式引入,在网上参考他人代码时务必注意一下
mock方法行为时,需注意调用时的url请求参数应该与mock时的参数保持一致,否则mock不成功。(若使用AnyString()等方法mock可无视此条)
不要忘记initMocks
1)因为mock对象的入参,带有时间戳无法mock出结果
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pfRLJZ4D-1673424161455)(/Users/liubo/Library/Application Support/typora-user-images/image-20220908163358287.png)]
PowerMock
官网:http://code.google.com/p/powermock/
PowerMock扩展了EasyMock和Mockito框架,增加了对static和final方法mock支持等功能。
Powermock主要用于打桩。比如:方法A的参数需要传入实例B,方法A需要调用B的某个方法B.C()。方法C因为耗时长或者根本没有实现或者其他不方便在单元测试中实现等原因,需要伪造返回,此时Powermock即可派上用场。
pom.xml
<dependency>
<groupId>org.powermock</groupId>
<artifactId>powermock-api-mockito2</artifactId>
<version>2.0.2</version>
</dependency>
<dependency>
<groupId>org.powermock</groupId>
<artifactId>powermock-module-junit4</artifactId>
<version>2.0.2</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>RELEASE</version>
<scope>compile</scope>
</dependency>
代码实现
PowerMock使用一个自定义类加载器和字节码操作来模拟静态方法,构造函数,final类和方法,私有方法,去除静态初始化器等等。 PowerMock使用简单,在类名前添加注解,在预期前调用PowerMock的mock静态类方法,其他的预期方法和Mockito类似。
@PrepareForTest(System.class)@RunWith(PowerMockRunner.class)public class Test {@org.junit.Testpublic void should_get_filed() { System.out.println(System.getProperty("myName")); PowerMockito.mockStatic(System.class); PowerMockito.when(System.getProperty("myName")).thenReturn("steven"); System.out.println(System.getProperty("myName")); //->null steven }}
0)注解声明
@RunWith(PowerMockRunner.class) // 告诉JUnit使用PowerMockRunner进行测试
@PrepareForTest({Xxx.class, ..., Yyy.class}) // 当你需要使用PowerMock强大功能(模拟final类或有final, private, static, native方法的类)的时候,就需要加注解@PrepareForTest。
public class AbcTest {
@Test
public void method_1() {
}
}
1)mock对象
//1)构造方法引入。2)final Xxx
Xxx xxx = PowerMockito.mock(Xxx.class);
//方法内new Xxx()引入
PowerMockito.whenNew(Xxx.class).withNoArguments().thenReturn(xxx);
PowerMockito.whenNew(Xxx.class).withArguments(param_1, ..., param_n).thenReturn(xxx);
//Xxx().静态方法()引入
PowerMockito.mockStatic(Xxx.class);
//私有方法
PowerMockito.spy(new Xxx());
2)执行mock对象的方法
// 有返回值
PowerMockito.doReturn(返回值).when(xxx).方法();
PowerMockito.when(xxx.方法()).thenReturn(返回值);
// 有返回值:Xxx().静态方法()引入
PowerMockito.when(Xxx.静态方法()).thenReturn(返回值);
// 无返回值
PowerMockito.doNothing().when(xxx).方法();
//无返回值:Xxx().静态方法()引入
PowerMockito.doNothing().when(Xxx.class);
3)执行正常流程
4)断言
断言可以看成是一个 if 语句
if(假设成立){
程序正常运行;
}else{
报错&&终止程序!(避免由程序运行引起更大的错误)
}
有返回值Assert
断言方法 | 断言方法 | 用途 |
---|---|---|
assertEquals(预期值, 真实值); | assertNotEquals(预期值, 真实值); | 比较实际值与预期值是否一致。如果一致则程序继续运行,否则抛出异常打印报错信息。assertEquals(A,B)~=A.equals(B) |
assertSame(expected,actual) | assertNotSame(expected,actual) | 判断预期的值和实际的值是否为同一个参数(即判断是否为相同的引用),如果结果与预期相同,程序继续运行,否则抛出异常。assertSame(A,B) ~= A==B |
assertTrue | assertFalse(message,condition) | 比较条件的真假与预期相同。如果一致则程序继续运行,否则抛出异常。 |
assertNull(message,object) | assertNotNull(message,object) | 判断一个对象是否为空。如果一致则程序继续运行,否则抛出异常。 |
fail(message) | 使测试立即失败,这种断言通常用于标记某个不应该被到达的分支。例如测试中某个代码块要try catch,则在catch代码中加入fail(message)方法,否则代码直接进入catch块,无法判断测试结果。 |
无返回值Mockito
// 校验mock出来对象的方法是否被调用
Mockito.verify(mock对象).方法();
// 校验mock出来对象的方法是否被调用过一次
Mockito.verify(mock对象,Mockito.times(1)).方法();
// 校验mock出来对象的方法是否从未被调用
Mockito.verify(mock对象,Mockito.never()).方法();
Jenkins单元测试
-
找到项目对应的Pipeline
-
选择左侧配置
-
配置sonar_test = “true”
-
执行一次后会自动加载,当自动跳转到http://xxx/sonarqube/dashboard就成功了。