单元测试Junit4+Mockito+PowerMock

852 阅读24分钟

常用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…

powermock和Mockito版本对应关系

MockitoPowerMock
2.8.9+2.x
2.8.0-2.8.91.7.x
2.7.51.7.0RC4
2.4.01.7.0RC2
2.0.0-beta - 2.0.42-beta1.6.5-1.7.0RC
1.10.8 - 1.10.x1.6.2 - 2.0
1.9.5-rc1 - 1.9.51.5.0 - 1.5.6
1.9.0-rc1 & 1.9.01.4.10 - 1.4.12
1.8.51.3.9 - 1.4.9
1.8.41.3.7 & 1.3.8
1.8.31.3.6
1.8.1 & 1.8.21.3.5
1.81.3
1.71.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{{结束测试类}}

单元测试分组

  1. 定义分组:相当于定义一个接口,一个接口为一个分组,接口不需要实现内容
  2. 在单元测试类上使用@Category({接口1.class, 接口1.class}) 注解,注解中引用对应的接口分组
  3. 定义分组执行的类,通过@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

  1. 重写before()after()方法。
  2. before()方法中编写资源初始化的代码。
  3. after()方法中编写资源释放的代码
  4. 在每个@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

  1. 指定运行器方式:在测试类上添加@RunWith(MockitoJUnitRunner.class)注解

  2. 调用静态方法方式:在测试类里添加方法,并让其在所有测试方法执行前执行

    // @Before注解表示在任意使用@Test注解标注的public void方法执行之前执行
    @Before
    public void init() {
        // 这句代码需要在运行测试函数之前被调用
        //MockitoAnnotations.openMocks(this);
        // 下面这条语句和上面的效果相同
        MockitoAnnotations.initMocks(this);
    }
    
  3. 添加规则方式

    @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私有方法

  1. 使用@PrepareForTest注解指定需要模拟的类。
  2. 使用PowerMock的spy方法创建类的实例。
  3. 使用PowerMockito.doReturnPowerMockito.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类的方法

  1. 使用@PrepareForTest注解指定需要模拟的最终类。
  2. 使用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构造函数的方法

  1. 使用@PrepareForTest注解指定需要模拟的类。
  2. 使用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和构建工具会自动处理这些问题