Mock测试与Mockito简介
Mock测试是一种软件测试技术,它允许开发者在测试过程中对外部依赖(如数据库、文件系统、网络请求、其他服务或模块等)进行模拟(Mock)或仿制(Stub),以便在隔离的环境中测试代码。通过使用Mock对象,开发者可以专注于测试当前代码的逻辑,而无需担心外部依赖的复杂性、不稳定性或不可用性。简单来说,Mock测试及在测试时模拟外部行为以避免繁杂甚至不可实现的外部依赖创建并将测试的重点放在需要关注的功能点上。
Mockito 是一个针对 Java 的单元测试模拟框架,它通过简洁易用的 API 和强大的模拟能力,为开发者提供了在单元测试中模拟依赖对象的能力。Mockito 的基本实现原理主要依赖于动态代理和字节码操作技术。
Mockito实战
依赖引入
简单的Mock测试仅需引入mockito-core包即可,创建一个maven测试项目并在pom文件中添加如下依赖。在引入依赖时需注意Mockito的版本,Mockito 2. X支持 java6以上版本,Mockito 3.X支持java8以上版本, Mockito 5.x 支持java11以上版本。
<dependencies>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>3.12.4</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.30</version>
</dependency>
</dependencies>
场景与测试代码介绍
在实战中,项目想要描述一个基本的计算机工作过程,所以我们定义了一个计算机类及其关键的三个接口:输入接口(Input),输出接口(Output),处理接口(Process)。同时我们还定义了一个抽象类用于标识软件。
Computer.java
import api.Input;
import api.Output;
import api.Process;
import lombok.Data;
import soft.Software;
import exception.NoSoftWareException;
import java.util.List;
@Data
public class Computer {
private Input inputDevice;
private Output outPutDevice;
private Process processer;
private List<Software> softwares;
public void baseSoftwareCompute() {
if(softwares == null || softwares.size() == 0) {
throw new NoSoftWareException("no software, can not compute");
}
Software software = softwares.get(0);
compute(software);
}
public void compute(Software software){
Object processResult = processer.process(inputDevice.input(), software);
outPutDevice.output(processResult);
}
}
Input.java
package api;
public interface Input {
Object input();
}
Output.java
package api;
public interface Output {
void output(Object output);
}
Process.java
package api;
import soft.Software;
public interface Process {
Object process(Object input, Software s);
}
Software.java
package soft;
public abstract class Software {
public abstract Object run(Object input);
}
此外,项目还定义了一个基本的Process实现,该实现在运行process方法时会使用传入的程序来处理输入数据并返回处理后的数据。
package impl;
import api.Process;
import soft.Software;
public class DefaultProcessor implements Process {
@Override
public Object process(Object input, Software s) {
return s.run(input);
}
}
接下来,作为主机制造商,我想要验证我的主机是否能正常工作,但是我的供应商们还未给我提供输入、输出设备以供测试,仅有芯片制造商给我提供了一个简单的芯片以供测试。这个时候,我就需要使用Mock模拟测试来帮我的忙啦。
Mock对象的创建
在Mockito中,Mock对象的创建有通过注解创建和调用Mockito.mock()方法来创建两种方式。在Mockit中常用的用于Mock对象的注解包括
- @Mock 注解修饰的对象将被创建为一个默认Mock对象,其未打桩方法将返回返回类型的默认值。(如方法返回类型为int则Mock对象对应方法返回值为0,若方法返回类型为Object则Mock对象对应方法返回值为null )。
- @Spy 注解修饰的对象将被创建为真实类定义的对象,其未打桩方法将按照真实定义执行。
- @InjectMocks 注解修饰想要测试的类,其依赖对象若被@Mock或@Spy修饰,则其依赖的模拟对象将被自动注入。
以下是针对上文场景,分别使用注解和Mockit.mock()方法来创建Mock对象的示例。
public class ComputerMockTest {
@InjectMocks
Computer computer;
@Mock
Input keyBord;
@Mock
Output printer;
@Mock
List<Software> softwares;
@Spy
DefaultProcessor processor;
@Before //@Before修饰的方法将在每个测试方法执行前被调用
public void setUp() {
/**
在测试前需使用此代码开启Mock模拟对象
Junit4也可以给测试类添加注解@RunWith(MockitoJUnitRunner.class) 来开启Mock模拟对象
**/
MockitoAnnotations.openMocks(this);
System.out.println("start to exec test");
}
@After //@After修饰的方法将在每个测试方法执行后被调用
public void completeTest() {
Mockito.reset(softwares,keyBord,printer); //指定完每个测例后清理模拟对象上的桩
System.out.println("complete the execution of test");
}
@Test public void mockObjectTest() {
Software mockedSoftware = Mockito.mock(Software.class);
Assert.assertNotNull(mockedSoftware);
Assert.assertNotNull(computer);
Assert.assertNotNull(computer.getProcesser());
System.out.println("所有被验证实例非空");
}
}
运行结果
start to exec test
所有被验证实例非空
complete the execution of test
打桩(Stubbing)
打桩(Stubbing)及定义模拟对象在特定场景下行为的过程。打桩包含两个核心过程定义及场景和返回。所以打桩常用的两种定义格式分别为
- when(...).thenReturn(...) (或ThenThrow(...)、ThenAnswer(...))
- doSomething(...).when(...).method(...)
两种定义格式可以按需使用,以下是针对上面的场景进行的简单测试,下例中的代码是“Mock对象创建”节示例类下的测试方法。在本次测试用例中我们模拟了一个可以将输入转化为字符串并将所有字母转换为大写的Software,模拟了一个输入源会输入“hello mockito”字符串,模拟了一个输出源会打印输入的信息。
@Test
public void baseComputeTest() {
Mockito.when(softwares.size()).thenReturn(1); //打桩softwares.size()方法,定义其返回为1尽管其列表内部并未存储任何Software
Mockito.doReturn(new Software() {
public Object run(Object input) {
return Optional.ofNullable(input).orElse("").toString().toUpperCase();
}
}).when(softwares).get(0); //打桩softwares.get(0),定义其返回一个将字符串转换为大写并输出的Software
Mockito.when(keyBord.input()).thenReturn("hello mockito."); //打桩keyBord.input()方法,定义其返回一个字符串
Mockito.doAnswer(invocationOnMock -> {
System.out.println("来自屏幕的显示:" + invocationOnMock.getArgument(0).toString());
return null;
}).when(printer).output(Mockito.anyString()); //打桩printer.output(String)方法,定义其将输入到output中的字符串打印出来
computer.baseSoftwareCompute();
}
运行结果
start to exec test
来自屏幕的显示:HELLO MOCKITO.
complete the execution of test
在上例中,使用了两种打桩的格式来对测试模拟对象进行打桩。可以看出Mockito打桩定义的场景是针对特定模拟对象的特定方法和特定参数,在定义场景时可以使用Mockito.anyString()来匹配任一字符串输入,Mockito也提供了一些其他方法来进行通用匹配。在定义方法行为时上例使用了thenReturn()来定义返回值,也使用了doAnswer(Answer) 来定义方法被调用时执行特定代码。此外在进行测试时还可以使用thenThrow(Throwable) 来抛出异常。
验证测试结果结果
在前面的示例中,我们已经使用了一些断言来对测试结果进行判断。此外,Mockito也提供了verify(...)方法,该方法主要致力于验证被打桩方法的调用情况。
@Test
public void mockVerifyTest() {
Software mockSoftware = Mockito.mock(Software.class);
Mockito.doAnswer(invocationOnMock -> {
String[] input = invocationOnMock.getArgument(0).toString().split(" ");
return Long.parseLong(input[0]) + Long.parseLong(input[1]);
}
).when(mockSoftware).run(Mockito.anyString());
Mockito.doAnswer(invocationOnMock -> {
System.out.println("来自屏幕的显示:" + invocationOnMock.getArgument(0).toString());
return null;
}).when(printer).output(Mockito.any());
Mockito.when(keyBord.input()).thenReturn("123 456");
computer.compute(mockSoftware);
Mockito.verify(mockSoftware, Mockito.times(1)).run(Mockito.any()); //验证mockSoftware.run(Object) 方法调用次数为1
System.out.println("执行次数验证成功");
}
运行结果
start to exec test
来自屏幕的显示:579
执行次数验证成功
complete the execution of test
在上例中我们模拟了一个Software,其会将以字符串形式输入的两个数字相加,同时我们也模拟了输入和输出,然后我们使用computer执行了mockSoftware。最后我们使用了Mockito.verify()方法来验证mockSoftware.run(Object)方法的执行次数为1。在上例中使用了Mockito.times(int) 方法来指定验证方法调用次数,除此之外,Mockito还提供了一些其他VerificationMode可供使用。
总结
本文简单介绍了Mock模拟和Mockito的基本概念并用一个简单的Computer示例演示了Mockito在测试中的基本用法。使用Mockito进行测试可以隔离外部依赖,帮助我们快速验证代码功能,Mockito在实现时使用了ByteBuddy动态字节码技术和**Objenesis技术来创建模拟对象,其设计思路也值得学习。感兴趣的小伙伴不妨学习学习其源码。