常用Mock
| 工具名称 | 作用描述 | 主要差异点 |
|---|---|---|
| Mockito | 模拟接口、抽象类和普通的Java对象 | - 易于使用,提供注解支持。 - 不支持静态方法和构造函数的mocking。 |
| PowerMock | 扩展其他mock框架,模拟静态方法、构造函数和私有方法 | - 与Mockito和EasyMock兼容。 - 支持高级mocking技术。 |
| EasyMock | 模拟对象和接口的行为、验证模拟对象 | - 需要使用expect-run-verify模式。 - 适合重载方法的测试。 |
| JMock | 支持模拟对象和部分模拟,允许对部分方法进行mocking。 | - 支持partial mocks和状态验证。 |
| JMockit | 允许对整个类进行mocking,支持无侵入性测试。 | - 支持无侵入性测试。 - 可以mock final和static方法。 |
| WireMock | 专注于HTTP(S)请求和响应的模拟,适合API和Web服务测试。 | - 作为独立服务运行,支持模拟Web服务。 |
| JSONassert | 断言JSON结构和内容的库,可以与Mockito等结合使用。 | - 专注于JSON数据的断言。 |
| Hamcrest | 一个匹配器库,可以与Mockito等mock框架结合使用,提供丰富的断言方法。 | - 不直接提供mocking功能,而是增强断言能力。 |
| BDDMockito | 为Mockito添加行为驱动开发(BDD)风格的API。 | - 与Mockito兼容,提供BDD风格的测试方法。 |
| Mockito-Kotlin | 为Kotlin语言提供Mockito的扩展,支持在Kotlin中使用Mockito。 | - 针对Kotlin语法和特性优化。 |
这里我们使用JUnit4+Mockito+PowerMock
- JUnit4:单元测试
- Mockito:模拟对象方法行为
- PowerMock:模拟静态方法
jacoco组件:检查代码单元测试覆盖率
# 输出单元测试代码覆盖率报告
mvn test jacoco:report
# 报告路径:target/site/jacoco.index.html
mock版本
powermock官网github.com/powermock/p…
| Mockito | PowerMock |
|---|---|
| 2.8.9+ | 2.x |
| 2.8.0-2.8.9 | 1.7.x |
| 2.7.5 | 1.7.0RC4 |
| 2.4.0 | 1.7.0RC2 |
| 2.0.0-beta - 2.0.42-beta | 1.6.5-1.7.0RC |
| 1.10.8 - 1.10.x | 1.6.2 - 2.0 |
| 1.9.5-rc1 - 1.9.5 | 1.5.0 - 1.5.6 |
| 1.9.0-rc1 & 1.9.0 | 1.4.10 - 1.4.12 |
| 1.8.5 | 1.3.9 - 1.4.9 |
| 1.8.4 | 1.3.7 & 1.3.8 |
| 1.8.3 | 1.3.6 |
| 1.8.1 & 1.8.2 | 1.3.5 |
| 1.8 | 1.3 |
| 1.7 | 1.2.5 |
<dependencies>
<!-- JUnit 4 -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency>
<!-- Mockito -->
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>3.12.4</version>
<scope>test</scope>
</dependency>
<!-- PowerMock -->
<!-- PowerMock 需要 Mockito 的所有依赖,包括 mockito-core 和 mockito-all -->
<dependency>
<groupId>org.powermock</groupId>
<artifactId>powermock-module-junit4</artifactId>
<version>2.0.9</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.powermock</groupId>
<artifactId>powermock-api-mockito2</artifactId>
<version>2.0.9</version>
<scope>test</scope>
</dependency>
</dependencies>
JUnit 4
常用注解功能
基本注解
| 注解 | 作用描述 |
|---|---|
@BeforeClass | 在所有测试方法之前执行,用于标记静态方法,每个测试类只执行一次 |
@AfterClass | 在所有测试方法之后执行,用于标记静态方法,每个测试类只执行一次 |
@Before | 在每个测试方法执行之前调用,用于每个测试的前置操作 |
@After | 在每个测试方法执行之后调用,用于每个测试的后置操作 |
@Test | 在每个测试方法执行后执行 |
@Ignore | 忽略某个测试方法或测试类 |
@RunWith(Class<? extends Runner>) | 指定自定义的测试运行器 |
@Parameters | 提供参数化的测试数据 |
@Rule | 允许灵活地添加或重写测试规则 |
@FixMethodOrder(MethodSorters.class) | 指定测试方法的执行顺序 |
@Test(expected = Exception.class) | 声明测试方法应该抛出指定的异常 |
@Test(timeout = long) | 设置测试方法的超时时间,运行时间超过设置时间,则运行失败 |
@Repeat(int) | 重复执行测试方法多次 |
分组注解
| 注解 | 作用 | 描述 |
|---|---|---|
@Category(Class<?>) | 定义组 | 测试方法或测试类进行分组,与Suite.SuiteClasses搭配使用实现只运行指定组的测试方法 |
@IncludeCategory(Class<?>) | 包含组 | 包含运行的用例组,和Category搭配使用 |
@ExcludeCategory(Class<?>) | 排除组 | 排除运行的用例组,和Category搭配使用 |
@Suite.SuiteClasses(Class<?>[]) | 指定类的组 | 组合多个测试类,和Category搭配使用 |
断言方法
断言方法调用org.junit.Assert类
| 断言方法名 | 作用 | 样例代码 |
|---|---|---|
assertEquals | 断言两个值或对象相等。如果不相等,则测试失败。 | assertEquals("expected", "actual"); |
assertNotEquals | 验证两个值或对象是否不相等 | assertNotEquals(1, 2); |
assertTrue | 断言一个条件为true。如果条件为false,则测试失败。 | assertTrue(condition); |
assertFalse | 断言一个条件为false。如果条件为true,则测试失败。 | assertFalse("Error message", condition); |
assertNotNull | 断言一个对象不是null。如果对象为null,则测试失败。 | assertNotNull("Object should not be null", obj); |
assertNull | 断言一个对象是null。如果对象不是null,则测试失败。 | assertNull("Object should be null", obj); |
assertSame | 断言两个引用指向同一个对象。如果不是,测试失败。 | assertSame(expected, actual); |
assertNotSame | 验证两个对象引用是否不指向内存中的同一个对象 | assertNotSame(new Object(), new Object()); |
assertNotSame | 断言两个引用没有指向同一个对象。如果是,测试失败。 | assertNotSame("Should not be same object", obj1, obj2); |
assertArrayEquals | 断言两个数组内容相等。如果不相等,则测试失败。 | assertArrayEquals(expectedArray, actualArray); |
fail | 使测试立即失败。通常用于未达到特定条件的情况。 | fail("Test failed due to some reason"); |
assertThat | 使用Hamcrest匹配器进行断言。匹配器不匹配时,测试失败。 | assertThat("Test", is("expected")); |
JUnit运行顺序流程
graph LR
A{{开始测试类}} --> B["@BeforeClass"]
B --> C(["@Before(方法)"])
B --> C1(["@Before(方法)"])
B --> C2([...更多测试方法])
subgraph 方法1
C --> D(["@Test方法1"]) --> E(["@After(方法)"])
end
subgraph 方法2
C1 --> D1(["@Test方法2"]) --> E1(["@After(方法)"])
end
subgraph 其他方法
C2
end
E --> L["@AfterClass"]
E1 --> L["@AfterClass"]
C2 --> L["@AfterClass"]
L --> M{{结束测试类}}
单元测试分组
- 定义分组:相当于定义一个接口,一个接口为一个分组,接口不需要实现内容
- 在单元测试类上使用@Category({接口1.class, 接口1.class}) 注解,注解中引用对应的接口分组
- 定义分组执行的类,通过@Suite.SuiteClasses({单元测试类.class, 单元测试类.class}) 标识被@Category标识的测试类,@RunWith(Categories.class)指定该类为Category类,运行这个类就直接能运行@Suite.SuiteClasses标识的类
流程图
graph TD
subgraph 分组接口
A{{"分组A<br>public interface A{}"}}
B{{"分组B<br>public interface B{}"}}
end
subgraph 测试类
A1["@Category(A.class)<br>A1测试类"] --> A
A2["@Category(A.class)<br>A2测试类"] --> A
B1["@Category(B.class)<br>B1测试类"] --> B
B2["@Category(B.class)<br>B2测试类"] --> B
end
subgraph "分组执行单元测试"
AA["<div style=text-align:left;>
// 表示此类为Category类<br>
@RunWith(Categories.class)<br>
// 要运行的类<br>
@Suite.SuiteClasses({A1.class, B1.class}) <br>
// 值包含A组<br>
@IncludeCategory(A.class)<br>
public class AA {<br>
// 只会运行A1类<br>
}
</div>
"]
BB["<div style='text-align:left;'>
// 表示此类为Category类<br>
@RunWith(Categories.class)<br>
// 要运行的类<br>
@Suite.SuiteClasses({A1.class,A2.class,B1.class, B2.class}) <br>
// 排除A组<br>
@ExcludeCategory(A.class)<br>
public class AA {<br>
// 只会运行B1,B2类<br>
}
</div>
"]
end
AA --> A1
AA --> B1
BB --> A1
BB --> A2
BB --> B1
BB --> B2
@RunWith:指定运行器
常用的运行器
| 运行器 | 作用 | 说明 |
|---|---|---|
| 不需要显式指定,JUnit 4.x默认使用。 | 提供JUnit 4测试框架的核心功能 | JUnit 4.x 的默认测试运行器 |
@RunWith(BlockJUnit4ClassRunner.class) | 集成Spring测试环境 | 允许JUnit 4的测试用例在Spring TestContext框架下运行 |
@RunWith(SpringRunner.class) | 集成Spring Boot测试环境 | Spring Boot 1.x版本使用的测试运行器 |
@RunWith(SpringBootTest.class) | 提供Spring Boot测试的快捷方式。 | Spring Boot 2.x及以上版本推荐的测试运行器 |
@RunWith(Parameterized.class) | 支持参数化测试 | 用于参数化测试,可以对不同的输入参数执行相同的测试用例 |
@RunWith(Suite.class) | 测试套件的组织 | 用于将多个测试类组合成一个测试套件执行 |
@RunWith(Categories.class) | 分组测试 | 允许根据类别来过滤测试方法,只执行特定类别的测试 |
@RunWith(Theories.class) | 支持理论测试 | 用于JUnit的理论(Theories)测试,类似于参数化测试,但更高级 |
@RunWith(Enclosed.class) | 组织内部测试类 | 允许在单个测试类中包含其他测试类 |
powermock和mockito的常用运行器
| 运行器 | 作用 | 说明 |
|---|---|---|
@RunWith(MockitoJUnitRunner.class) | 允许在JUnit测试中使用Mockito注解和功能。 | Mockito的JUnit测试运行器,用于执行Mockito测试。 |
@RunWith(MockitoJUnitRunner.class) | 提供Mockito测试的便捷方式。 | Mockito提供的JUnit测试运行器,用于集成Mockito的测试。 |
@RunWith(PowerMockRunner.class) | 扩展JUnit的功能,支持高级模拟。 | PowerMock的测试运行器,支持模拟静态方法、构造函数等。 |
@RunWith(DelegatingTestRunner.class) | 允许与Spring测试上下文等框架集成。 | PowerMock的另一个测试运行器,用于与Spring等框架集成。 |
@RunWith(PowerMockJUnit47RunnerDelegate.class) | 支持JUnit 4.7以上版本的测试。 | PowerMock的JUnit 4.7+专用运行器,用于模拟静态和私有方法。 |
@RunWith(PowerMockJUnit4Runner.class) | 支持JUnit 4的测试。 | PowerMock的JUnit 4专用运行器。 |
@Parameters:参数化测试
将数据转化为一个集合,测试方法自动循环集合的参数进行调用
需要与@RunWith(Parameterized.class)配合使用
- @Parameters:被注解的方法返回是一个集合,集合中包含的是一个数组
- @Parameter:被注解的变量不能为private,不指定下标默认取数组中下标0的数据
- @Test:会被调用集合.size()次,每次获取被@Parameter的变量的值就是被@Parameters返回的值中的某一个某一条数据
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameter;
import org.junit.runners.Parameterized.Parameters;
import java.util.Arrays;
import java.util.Collection;
import static org.junit.Assert.assertEquals;
// 使用Parameterized运行器执行参数化测试
@RunWith(Parameterized.class)
public class ParameterizedTest {
// 第一个参数:输入值
@Parameter
public int input;
// 第二个参数:期望值
@Parameter(1)
public int expected;
/**
* 构造函数,用于初始化测试数据
* 如果使用了构造,那么就不需要使用@Parameter注解
* @param input
* @param expected
*/
/*public ParameterizedTest(int input, int expected) {
this.input = input;
this.expected = expected;
}*/
// 提供测试数据的方法
@Parameters
public static Collection<Object[]> data() {
// 返回一组输入和期望输出的集合,用于参数化测试
// 集合对应了@Parameter(下标)
return Arrays.asList(new Object[][]{
{1, 2},
{2, 4},
{3, 6},
{4, 62}
});
}
// 执行测试,验证输入值的两倍是否等于期望值
@Test
public void testDouble() {
// 断言输入值的两倍等于期望值
// 会被执行4次,每次取expected的值为2,4,6,62
assertEquals(expected, input * 2);
}
}
@Rule:测试规则
内置规则
| 规则名称 | 作用 | 用法示例 |
|---|---|---|
ExternalResource | 管理外部资源,例如文件锁或网络连接。在测试前后获取和释放资源。 | @Rule public ExternalResource resource = new ExternalResource(); |
TemporaryFolder | 创建临时文件夹和文件,用于测试需要写入文件的场景。 | @Rule public TemporaryFolder folder = new TemporaryFolder(); |
ErrorCollector | 收集测试过程中的所有错误和失败,而不是使第一个失败点的测试立即停止。 | @Rule public ErrorCollector collector = new ErrorCollector(); |
ExpectedException | 检查测试过程中是否抛出了预期的异常。 | @Rule public ExpectedException thrown = ExpectedException.none(); |
Stopwatch | 提供一个计时器,用于测量测试方法的执行时间。 | @Rule public Stopwatch stopwatch = new Stopwatch(); |
Verifier | 验证测试过程中的假设。 | @Rule public Verifier verifier = new Verifier(); |
TestWatchman | 在测试完成后执行清理工作。 | @Rule public TestWatchman watchman = new TestWatchman(); |
TestName | 存储当前测试的名称,可以在测试方法中使用。 | @Rule public TestName name = new TestName(); |
ExternalResource
- 重写
before()和after()方法。 - 在
before()方法中编写资源初始化的代码。 - 在
after()方法中编写资源释放的代码 - 在每个
@Test都会执行before()和after()方法
// 声明一个外部资源规则
@Rule
public ExternalResource resource = new ExternalResource() {
@Override
protected void before() throws Throwable {
// 设置测试前的资源
System.out.println("Setting up resources before test.");
}
@Override
protected void after() {
// 清理测试后的资源
System.out.println("Cleaning up resources after test.");
}
};
@Test
public void testSomething() {
// 测试逻辑
System.out.println("Running the test.");
}
TemporaryFolder
在测试中创建临时文件或目录,测试完成后自动删除
@Rule
public TemporaryFolder folder = new TemporaryFolder();
@Test
public void testWithTempFile() throws IOException {
File file = folder.newFile("test.txt");
File tempFile = folder.newFile();
// 使用临时文件进行测试
System.out.println("Temporary file created: " + tempFile.getAbsolutePath());
System.out.println("Temporary file created: " + file.getAbsolutePath());
}
ErrorCollector
收集测试中的所有错误和失败,可以在测试结束后统一处理
@Rule
public ErrorCollector collector = new ErrorCollector();
@Test
public void testWithCollector() {
collector.addError(new Throwable("Error occurred"));
// 测试逻辑,会先输出22222,最后收集起来再报错
System.out.println("22222");
}
ExpectedException
检查测试是否抛出了预期的异常
@Rule
public ExpectedException thrown = ExpectedException.none();
@Test
public void testException() {
// 测试逻辑,预期抛出IllegalArgumentException异常
thrown.expect(IllegalArgumentException.class);
System.out.println("11111");
// 最后抛出预期异常则方法运行成功
throw new IllegalArgumentException("Test exception");
}
Stopwatch
用于测量测试方法的执行时间
@Rule
public Stopwatch stopwatch = new Stopwatch();
@Test
public void testTiming() {
long startTime = stopwatch.start();
// 执行测试
stopwatch.stop();
// 输出最终花费时长
System.out.println("Test took: " + stopwatch.getTime());
}
Verifier
测试执行完成后执行一些额外的验证,重写verify()验证逻辑
// 声明一个验证器规则
@Rule
public Verifier verifier = new Verifier() {
@Override
protected void verify() throws Throwable {
// 执行额外的验证
System.out.println("Running additional verification after test.");
}
};
@Test
public void testSomething() {
// 测试逻辑
System.out.println("Running the test.");
}
TestWatchman
在测试执行的生命周期中进行自定义操作,每个测试方法都会调用生命周期
- 方法开始前
- 方法调用成功
- 方法调用失败
- 方法结束
import org.junit.Test;
import org.junit.rules.TestWatcher;
import org.junit.runner.Description;
import org.junit.Rule;
public class TestWatchmanExample {
// 使用@Rule注解声明自定义的TestWatchman规则
@Rule
public TestWatcher watchman = new TestWatcher(){
// 方法开始之前调用
protected void starting(Description description) {
System.out.println("Starting test: " + description.getMethodName());
}
// 方法成功完成后调用
protected void succeeded(Description description) {
System.out.println("Test succeeded: " + description.getMethodName());
}
// 方法失败后调用
protected void failed(Throwable e, Description description) {
System.out.println("Test failed: " + description.getMethodName() + " with exception: " + e.getMessage());
}
// 方法结束后调用,无论成功还是失败
protected void finished(Description description) {
System.out.println("Finished test: " + description.getMethodName());
}
};
// 测试方法示例
@Test
public void testMethod1() {
// 测试逻辑
System.out.println("Running testMethod1");
}
@Test
public void testMethod2() {
// 测试逻辑
System.out.println("Running testMethod2");
// 假设这里发生了一个错误
throw new RuntimeException("Something went wrong");
}
}
TestName
获取当前测试方法的名称
@Rule
public TestName name = new TestName();
@Test
public void testExample() {
String testName = name.getMethodName();
// 使用测试名称,输出的就是testName
System.out.println("Test name: " + testName);
}
@FixMethodOrder:执行方法运行顺序
通过MethodSorters类设置方法的执行顺序
策略
| 排序策略 | 作用 | 用法示例 |
|---|---|---|
MethodSorters.JVM | 使用JVM的自然顺序执行测试方法。通常与方法在类中声明的顺序相同。 | @FixMethodOrder(MethodSorters.JVM) |
MethodSorters.NAME_ASCENDING | 按照测试方法名称的字母顺序升序执行测试方法。 | @FixMethodOrder(MethodSorters.NAME_ASCENDING) |
MethodSorters.NAME_DESCENDING | 按照测试方法名称的字母顺序降序执行测试方法。 | @FixMethodOrder(MethodSorters.NAME_DESCENDING) |
用例
// 使用@FixMethodOrder注解来指定测试方法的执行顺序
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
public class FixMethodOrderExampleTest {
// 测试方法将按照名称的字母顺序执行
@Test
public void firstTestMethod() {
System.out.println("Executing 11111 test method.");
// 测试逻辑
}
@Test
public void secondTestMethod() {
System.out.println("Executing 22222 test method.");
// 测试逻辑,可能依赖于firstTestMethod的执行结果
}
@Test
public void thirdTestMethod() {
System.out.println("Executing 33333 test method.");
// 测试逻辑,可能需要在secondTestMethod之后执行
}
}
Mockito
特性
- 可以创建接口和类的模拟对象,在测试中代替真实对象(测试桩)
- 模拟对象的行为
- 检查模拟对象是否按照预期被调用
无法模拟的情况,需要配合PowerMock
- 无法对final的类和方法进行Mock
- 无法对static的类和方法进行Mock
- 无法对局部变量进行Mock
Mockito资源
注解
| 注解 | 作用 | 用法示例 |
|---|---|---|
@Mock | 创建一个模拟对象 | @Mock private Dependency dependency; |
@Spy | 创建一个部分模拟对象,即部分行为是真实的,部分是模拟的 | @Spy private RealObject realObject; |
@InjectMocks | 自动注入所有标记为@Mock的对象到当前测试类中 | @InjectMocks private MyClass testClass; |
@Captor | 为验证方法调用的参数提供捕获器 | @Captor ArgumentCaptor<String> captor; |
@RunRules | 指定一个或多个测试规则,这些规则将应用于测试方法 | @RunRules(MyTestRule.class) |
@JUnitRule | 用于JUnit 4的规则,等同于@Rule | @JUnitRule public ExpectedException thrown = ExpectedException.none(); |
仅SpringBoot项目使用
| 注解 | 作用 | 用法示例 |
|---|---|---|
@MockBean | 在Spring测试环境中,用模拟对象替换Spring的Bean | @MockBean private Service service; |
@SpyBean | 在Spring测试环境中,用部分模拟对象替换Spring的Bean | @SpyBean private Service service; |
@Qualifier | 与@InjectMocks一起使用,指定注入的Bean的名称 | @Qualifier("myBean") @InjectMocks private MyClass testClass; |
支持Mockito
有3种方式让单元测试类支持Mockito
-
指定运行器方式:在测试类上添加
@RunWith(MockitoJUnitRunner.class)注解 -
调用静态方法方式:在测试类里添加方法,并让其在所有测试方法执行前执行
// @Before注解表示在任意使用@Test注解标注的public void方法执行之前执行 @Before public void init() { // 这句代码需要在运行测试函数之前被调用 //MockitoAnnotations.openMocks(this); // 下面这条语句和上面的效果相同 MockitoAnnotations.initMocks(this); } -
添加规则方式
@Rule public MockitoRule mockitoRule = MockitoJUnit.rule();
代码样例
建议静态导入会使代码更简洁
import static org.mockito.Mockito.*;
import static org.junit.Assert.*;
import org.junit.*;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
import static org.mockito.Mockito.*;
public class MyTest {
// 声明一个接口,该接口将被模拟
public interface MyService {
String doSomething(String input);
}
// 声明一个模拟对象
@Mock
private MyService mockService;
@Rule
public MockitoRule mockitoRule = MockitoJUnit.rule();
@Test
public void testMyService() {
// 设置模拟对象的行为,调用此方法返回指定数据
when(mockService.doSomething("testInput")).thenReturn("mockedResult");
// 调用模拟对象的方法
String result = mockService.doSomething("testInput");
// 验证方法是否按照预期被调用了
verify(mockService).doSomething("testInput");
// 断言方法返回值是否符合预期
Assert.assertEquals("mockedResult", result);
}
}
功能
模拟对象方法
模拟对象方法的返回,被模拟的对象,调用方法会返回指定值
@Mock
List mockedList;
@Test
public void mockTest() {
// 测试桩,当指定的语句执行后返回对应的值或抛异常
when(mockedList.get(0)).thenReturn("first");
when(mockedList.get(1)).thenThrow(new RuntimeException());
// 因为get(999) 没有打桩,因此输出null
System.out.println(mockedList.get(999));
// 当调用未打桩的方法,int返回0,对象返回null
System.out.println(mockedList.size());
// 输出测试桩对应的内容first
System.out.println(mockedList.get(0));
// 输出测试桩对应的内容,抛异常
System.out.println(mockedList.get(1));
}
测试桩方法传参实现任意参数模拟,需要使用参数匹配器
参数匹配器
| 匹配器名称 | 作用 | 用法示例 |
|---|---|---|
anyInt() | 匹配任何int类型的参数 | when(mock.someMethod(anyInt())).thenReturn("someValue"); |
anyString() | 匹配任何String类型的参数 | when(mock.someMethod(anyString())).thenReturn("someValue"); |
any(Class<T> type) | 匹配任何指定类型的参数 | when(mock.someMethod(any(String.class))).thenReturn("someValue"); |
isA(Class<T> type) | 与any功能一致 | when(mock.someMethod(any(String.class))).thenReturn("someValue"); |
eq(value) | 匹配等于指定值的参数 | when(mock.someMethod(eq("expectedValue"))).thenReturn("someValue"); |
same(expectedObject) | 匹配与指定对象相同的参数(使用==比较) | when(mock.someMethod(same(expectedObject))).thenReturn("someValue"); |
endsWith(suffix) | 匹配以指定后缀结尾的字符串参数 | verify(mock).someMethod(endsWith("suffix")); |
contains(substring) | 匹配包含指定子字符串的字符串参数 | verify(mock).someMethod(contains("substring")); |
argThat(matcher) | 允许使用自定义的匹配逻辑 | when(mock.someMethod(argThat(x -> x % 2 == 0))).thenReturn(60); |
or(matcher1, matcher2, ...) | 匹配任一提供的匹配器 | verify(mock).someMethod(or(eq("value1"), eq("value2"))); |
anyList() | 匹配任何List类型的参数 | when(mock.someMethod(anyList())).thenReturn("someValue"); |
anySet() | 匹配任何Set类型的参数 | when(mock.someMethod(anySet())).thenReturn("someValue"); |
anyMap() | 匹配任何Map类型的参数 | when(mock.someMethod(anyMap())).thenReturn("someValue"); |
isNull() | 匹配 null 值的参数 | when(mock.someMethod(isNull())).thenThrow(new IllegalArgumentException()); |
notNull() | 匹配非 null 值的参数 | when(mock.someMethod(notNull())).thenReturn(false); |
@Test
public void mockTest1() {
List mockedList = mock(List.class);
// isA()的参数为一个类型,可以是任意类型。当调用的方法的参数为该类型时,该测试桩将会起效
when(mockedList.get(isA(Integer.class))).thenReturn("isA");
System.out.println(mockedList.get(0));//返回isA
// any()和isA()的作用完全一样
when(mockedList.get(any(Integer.class))).thenReturn("any");
System.out.println(mockedList.get(1));//返回any
// 当参数为Int类型(Interger也算)时,返回对应的值
when(mockedList.get(anyInt())).thenReturn("anyInt");
System.out.println(mockedList.get(0));//返回anyInt
// 当参数等于1时,返回对应的值
when(mockedList.get(eq(1))).thenReturn("equal");
System.out.println(mockedList.get(1));//equal
}
Mockito的验证方法
| 验证方法 | 作用 | 用法示例 |
|---|---|---|
verify(mock) | 验证模拟对象的某个方法是否被调用过。 | verify(mock).someMethod(); |
verify(mock, times(1)) | 验证模拟对象的某个方法是否被调用了指定的次数。 | verify(mock, times(1)).someMethod(); |
verify(mock, atLeastOnce()) | 验证模拟对象的某个方法是否至少被调用了一次。 | verify(mock, atLeastOnce()).someMethod(); |
verify(mock, atLeast(n)) | 验证模拟对象的某个方法是否至少被调用了n次。 | verify(mock, atLeast(5)).someMethod(); |
verify(mock, never()) | 验证模拟对象的某个方法从未被调用。 | verify(mock, never()).someMethod(); |
verifyNoMoreInteractions(mock) | 验证在测试方法的剩余部分没有与模拟对象的任何交互。 | verifyNoMoreInteractions(mock); |
verifyZeroInteractions(mock) | 验证在测试方法中没有与模拟对象的任何交互。 | verifyZeroInteractions(mock); |
verify(mock, timeout(n)) | 验证在指定的超时时间内,模拟对象的某个方法是否被调用。 | verify(mock, timeout(1000)).someMethod(); |
验证函数的确切、最少、从未调用次数
@Test
public void mockTest2() {
List mockedList = mock(List.class);
mockedList.add(1);
mockedList.add(1);
// 验证是否执行了恰好2次
verify(mockedList,times(2)).add(1);
// 验证是否执行了至少2次
verify(mockedList,atLeast(2)).add(1);
// 验证是否执行了至多2次
verify(mockedList,atMost(2)).add(1);
// 验证是否从未被执行
verify(mockedList, never()).add(1);
// 默认验证是否执行了恰好1次
verify(mockedList).add(1);
}
模拟方法执行时抛异常
@Test
public void mockTest3() {
List mockedList = mock(List.class);
// 无返回值的方法执行时抛异常
doThrow(new RuntimeException()).when(mockedList).clear();
// 调用这句代码会抛出异常
mockedList.clear();
}
验证执行执行顺序
验证单个mock对象的执行顺序
@Test
public void mockTest4() {
List mockedList = mock(List.class);
mockedList.add(1);
mockedList.add(2);
mockedList.add(3);
// 为mock对象创建一个InOrder对象
InOrder inOrder = inOrder(mockedList);
// 验证是否按顺序执行
inOrder.verify(mockedList).add(1);
inOrder.verify(mockedList).add(2);
inOrder.verify(mockedList).add(3);
}
为连续的调用做测试桩
当调用多次调用时,返回不同的值
@Test
public void mockTest5() {
List mockedList = mock(List.class);
when(mockedList.get(0))
// 第一次调用时返回firstReturn
.thenReturn("firstReturn")
// 第二次调用时返回secondReturn
.thenReturn("secondReturn")
// 第三次(包括)之后的调用抛异常
.thenThrow(new RuntimeException());
//简写效果一样
/*when(mockedList.get(0))
.thenReturn("firstReturn", "secondReturn")
.thenThrow(new RuntimeException());*/
mockedList.add(1);
mockedList.add(2);
// 第一次调用,返回firstReturn
System.out.println(mockedList.get(0));
// 第二次调用,返回secondReturn
System.out.println(mockedList.get(0));
// 非测试桩的调用,返回null
System.out.println(mockedList.get(1));
// 第三次调用,报错
System.out.println(mockedList.get(0));
}
调用原方法执行
让Mock的对象调用原始的代码逻辑
class User{
public int method(){
return 222;
}
}
@Test
public void mockTest(){
User mockedUser = mock(User.class);
when(mockedUser.method()).thenReturn(111);
// 返回mock的结果111
System.out.println(mockedUser.method());
// 设置调用原始逻辑
when(mockedUser.method()).thenCallRealMethod();
// 调用原函数,返回222
System.out.println(mockedUser.method());
}
模拟对象部分方法
spy:监控对象
spy和mock区别
spy:如果不对spy对象的methodA打桩,那么调用spy对象的methodA时,会调用真实方法。 mock:如果不对mock对象的methodA打桩,将doNothing,且返回默认值(null,0,false)。
当使用监控对象时请考虑==doReturn|Answer|Throw()==函数族来进行打桩
@Spy
List spyList = new ArrayList();
@Test
public void mockTest6() {
// 与@Spy效果相同,这里使用List.class会有问题
// List spyList = spy(ArrayList.class);
// 打桩
when(spyList.size()).thenReturn(10086);
// 效果和上个语句一样,类似的还有doThrow/doAnser等
// doReturn(10086).when(spyList).size();
// 没有被打桩的方法会正常调用
spyList.add(5);
// 返回第一个元素5
System.out.println(spyList.get(0));
// 返回打桩的值10086
System.out.println(spyList.size());
}
将模拟对象注入到需要测试的类中
在Mock对象时可以带参数
-
RETURNS_SMART_NULLS:使用RETURNS_SMART_NULLS参数创建的mock对象,不会
抛出NullPointerException异常- 样例: List mock = mock(List.class, RETURNS_SMART_NULLS)
-
RETURNS_DEEP_STUBS:使用RETURNS_DEEP_STUBS参数创建的mock对象,会自动将该对象中的全局变量进行mock所需的对象
-
样例:FakeEntity fakeEntity = mock(FakeEntity.class, RETURNS_DEEP_STUBS);
-
deepstubsTest等同于deepstubsTest2
@Test public void deepstubsTest() { // 模拟fakeEntity,内部的UserFakeEntity对象也一起模拟了 FakeEntity fakeEntity = mock(FakeEntity.class, RETURNS_DEEP_STUBS); when(fakeEntity.getUserFakeEntity().getName()).thenReturn("Beijing"); System.out.println(fakeEntity.getUserFakeEntity().getName()); assertEquals("Beijing", fakeEntity.getUserFakeEntity().getName()); } @Test public void deepstubsTest2() { // 模拟fakeEntity FakeEntity fakeEntity = mock(FakeEntity.class); //模拟UserFakeEntity UserFakeEntity userFakeEntity = mock(UserFakeEntity.class); // 将fakeEntity类对象中UserFakeEntity返回进行模拟 when(fakeEntity.getUserFakeEntity()).thenReturn(userFakeEntity); when(userFakeEntity.getName()).thenReturn("Beijing"); System.out.println(fakeEntity.getUserFakeEntity().getName()); assertEquals("Beijing", fakeEntity.getUserFakeEntity().getName()); }
-
被测试类MyService.java
注意:无法模拟方法内创建的对象,例如在doSomething()方法中创建了List list=new ..(),list是无法被模拟的
import java.util.Map;
// 假设有一个待测试的类,名为 MyService
public class MyService {
private final Map<String,String> map;
public MyService(Map<String,String> map) {
this.map = map;
}
public String doSomething() {
return map.get("ddd");
}
}
模拟被测试类中的全局变量对象
public class MyServiceTest {
// 通过注解注入模拟对象(被@InjectMocks注解的对象)
@Mock
Map<String, String> map;
// 被测试对象
@InjectMocks
private MyService myService;
// 规则方式支持Mockito
@Rule
public MockitoRule mockitoRule = MockitoJUnit.rule();
@Test
public void testDoSomething() {
// 设置模拟对象的行为
Mockito.when(map.get(anyString())).thenReturn("Mocked data");
// 调用待测试的方法,返回Mocked data
String result = myService.doSomething();
// 验证结果是否符合预期
assertEquals("Mocked data", result);
}
}
参数捕获
获取方法入参代码,功能是获取调用方法时传入的参数
两种方式:
- 注解:@Captor(Class.class)
- 代码:ArgumentCaptor.forClass(Class.class)
// 注解方式
//@Captor
//private ArgumentCaptor<List> argument;
@Test
public void testCaptureArgument() {
// 创建一个包含两个字符串元素的列表
List<String> list = Arrays.asList("1", "2");
// 使用Mockito的mock方法创建一个模拟的List对象
List mockedList = mock(List.class);
// 将之前创建的列表添加到模拟的List对象中
mockedList.addAll(list);
// 使用Mockito的ArgumentCaptor.forClass方法创建一个参数捕获器
// 这里捕获的是List类型的方法参数,捕获到了
ArgumentCaptor<List> argument = ArgumentCaptor.forClass(List.class);
// 验证模拟对象的addAll方法是否被调用,并捕获传递给该方法的参数
verify(mockedList).addAll(argument.capture());
// 断言捕获的参数列表的大小是否为2
assertEquals(2, argument.getValue().size());
// 断言捕获的参数列表是否与原始列表相同
assertEquals(list, argument.getValue());
}
}
PowerMock
静态方法模拟:模拟静态方法的行为,包括返回值、抛出异常或调用其他方法。
私有方法调用:使用Whitebox类来访问和调用私有方法。
构造函数模拟:模拟类的构造函数的行为,包括返回值、抛出异常或调用其他方法。
模拟最终类:模拟Java最终类(final classes)的能力
基本用法
必须使用PowerMock的运行器@RunWith(PowerMockRunner.class)
// 使用PowerMockRunner作为测试运行器
@RunWith(PowerMockRunner.class)
// 准备Arrays类以供测试
@PrepareForTest({Arrays.class})
public class StaticTest {
@Test
public void staticMethodTest() {
// 使用PowerMockito模拟Arrays类的静态方法
PowerMockito.mockStatic(Arrays.class);
// 创建一个包含一个字符串元素的列表
List d = new ArrayList<>();
d.add("test");
// 模拟Arrays.toString方法,当传入空的int数组时,返回"mocked result"
PowerMockito.when(Arrays.toString(new int[]{})).thenReturn("mocked result");
// 模拟Arrays.asList方法,当传入任何对象时,返回之前创建的列表d
PowerMockito.when(Arrays.asList(any(Object.class))).thenReturn(d);
// 调用模拟的Arrays.toString方法,返回值:mocked result
String string = Arrays.toString(new int[]{});
System.out.println(string);
// 调用模拟的Arrays.asList方法,返回值:[test]
List list = Arrays.asList("lll");
System.out.println(list);
}
}
注解
| 注解 | 作用 | 样例代码 |
|---|---|---|
@PrepareForTest | 指定需要PowerMock处理的类,以便模拟静态、私有和final方法。 | @PrepareForTest({MyClass.class}) |
@PowerMockIgnore | 指定PowerMock应该忽略的类或包,以避免潜在的冲突。 | @PowerMockIgnore("org.hamcrest.*") |
Mock局部变量对象
模拟方法内new的对象
- withNoArguments:不需要参数的new
- withAnyArguments:任何参数的new
- withArguments:指定参数的new
// 使用PowerMockRunner作为测试运行器
@RunWith(PowerMockRunner.class)
// 准备HashMap类以便于测试时进行模拟
@PrepareForTest(HashMap.class)
public class EmployeeServiceTest {
public class EmployeeService {
public int getTotalEmployee() {
// 准备模拟的局部变量
HashMap<String,String> employeeDao = new HashMap<>();
return employeeDao.size();
}
}
@Test
public void testGetTotalEmployeeWithMock() throws Exception {
// 创建HashMap的模拟对象
HashMap mockMap = PowerMockito.mock(HashMap.class);
// 当新建HashMap对象时,返回模拟的对象
PowerMockito.whenNew(HashMap.class).withNoArguments().thenReturn(mockMap);
// 设置当调用size方法时,返回10
PowerMockito.when(mockMap.size()).thenReturn(10);
EmployeeService service = new EmployeeService();
// 调用getTotalEmployee方法,返回10
int total = service.getTotalEmployee();
// 验证获取的员工总数是否与模拟的值一致
assertEquals(10, total);
}
}
Mock静态方法
将需要模拟的类放在PrepareForTest中,并且放入mockStatic方法中
通过PowerMockito.when来模拟对应静态方法的返回值
@RunWith(PowerMockRunner.class)
@PrepareForTest(YourStaticClass.class)
public class StaticMethodTest {
@Test
public void testStaticMethod() {
// 模拟静态方法
PowerMockito.mockStatic(YourStaticClass.class);
PowerMockito.when(YourStaticClass.staticMethod()).thenReturn("mocked result");
// 测试逻辑
String result = YourStaticClass.staticMethod();
assertEquals("mocked result", result);
}
}
Mock私有方法
- 使用
@PrepareForTest注解指定需要模拟的类。 - 使用PowerMock的
spy方法创建类的实例。 - 使用
PowerMockito.doReturn或PowerMockito.doThrow等方法来模拟私有方法的行为。
@RunWith(PowerMockRunner.class)
@PrepareForTest(YourClass.class)
public class PrivateMethodTest {
@Test
public void testPrivateMethod() {
YourClass yourClass = PowerMockito.spy(new YourClass());
// 模拟私有方法
PowerMockito.doReturn("mocked result").when(yourClass, "privateMethod");
// 测试逻辑
String result = yourClass.publicMethodThatCallsPrivateMethod();
assertEquals("mocked result", result);
}
}
Mock final类的方法
- 使用
@PrepareForTest注解指定需要模拟的最终类。 - 使用PowerMock的
mock方法来模拟最终类。
@RunWith(PowerMockRunner.class)
@PrepareForTest(YourFinalClass.class)
public class FinalClassTest {
@Test
public void testFinalClass() {
// 模拟最终类
YourFinalClass mockFinalClass = PowerMockito.mock(YourFinalClass.class);
PowerMockito.when(mockFinalClass.finalMethod()).thenReturn("mocked result");
// 测试逻辑
String result = mockFinalClass.finalMethod();
assertEquals("mocked result", result);
}
}
Mock构造函数的方法
- 使用
@PrepareForTest注解指定需要模拟的类。 - 使用PowerMock的
whenNew方法来模拟构造函数。
@RunWith(PowerMockRunner.class)
@PrepareForTest(YourClass.class)
public class ConstructorTest {
@Test
public void testConstructor() {
// 模拟构造函数
PowerMockito.whenNew(YourClass.class).withNoArguments().thenReturn(new YourClass() {
@Override
public void someMethod() {
// 模拟构造函数后的行为
}
});
// 测试逻辑
YourClass yourClass = new YourClass();
yourClass.someMethod();
}
}
Mock回调和结果
模拟调用方法需要执行的逻辑
使用PowerMockito.doAnswer()方法来实现回调逻辑
@Test
public void testMethodWithCallback() throws Exception {
HashMap mockMap = PowerMockito.spy(new HashMap());
// 设置回调逻辑
PowerMockito.doAnswer(invocation -> {
Object[] args = invocation.getArguments();
// 根据参数执行逻辑
if (args[0].equals("parameter")) {
// 执行某些操作
}
return "mocked result";
}).when(mockMap).get(anyString());
// 当执行get方法时,会调用回调逻辑,返回回调逻辑的值:"mocked result"
String result = (String) mockMap.get("parameter");
assertEquals("mocked result", result);
}
验证调用顺序
背景:当被测试的方法依赖于多个被模拟方法的调用顺序时,我们需要确保这些方法按照特定的顺序被调用。
解决方案:使用PowerMockito.inOrder()方法结合verify()来验证方法调用的顺序。
@RunWith(PowerMockRunner.class)
@PrepareForTest(YourClass.class)
public class SequenceTest {
@Mock
private Dependency1 dependency1;
@Mock
private Dependency2 dependency2;
@InjectMocks
private YourClass yourClass;
@Test
public void testMethodSequence() throws Exception {
// 测试逻辑
yourClass.performActions();
// 验证调用顺序
InOrder inOrder = inOrder(dependency1, dependency2);
inOrder.verify(dependency1).firstMethod();
inOrder.verify(dependency2).secondMethod();
}
}
常见问题与解决方案
问题1:模拟静态方法时出现java.lang.UnsupportedOperationException
背景:当尝试模拟一个静态方法,但该方法所在的类或方法被标记为final时,可能会遇到此异常。
解决方案:确保没有将类或方法标记为final,因为PowerMock无法模拟final类或方法。
// 错误的示例:静态方法被标记为final
public final class Utils {
public static int getValue() {
return 42;
}
}
// 正确的示例:移除final关键字
public class Utils {
public static int getValue() {
return 42;
}
}
问题2:使用@InjectMocks时,构造函数未被正确调用
背景:在使用@InjectMocks注解自动注入依赖时,如果构造函数中包含了复杂的逻辑,可能会发现这些逻辑没有被执行。
解决方案:确保构造函数中没有复杂的逻辑,或者使用@Mock注解手动创建并注入依赖。
// 错误的示例:构造函数中包含复杂逻辑
public class MyClass {
public MyClass() {
// 复杂的初始化逻辑
}
}
// 正确的示例:使用@Mock注解手动注入依赖
@RunWith(PowerMockRunner.class)
@PrepareForTest({Dependency.class})
public class MyClassTest {
@Mock
private Dependency dependency;
@InjectMocks
private MyClass myClass;
@Before
public void setUp() {
myClass = new MyClass(dependency);
}
}
问题3:测试运行时出现java.lang.ClassNotFoundException异常
背景:当测试类或被测试的类没有在类路径中时,可能会遇到这个异常。
解决方案:确保所有的类都已经正确编译,并且位于测试的类路径中。
// 错误的示例:类未编译或未放置在正确的位置
public class MyClass {
// ...
}
// 正确的示例:确保类已编译并位于类路径中
// 通常IDE和构建工具会自动处理这些问题