单元测试过程中的一些总结,记录如何利用 Mockito 和 JUnit 来编写单元测试。

认识一下

🚀 什么是单元测试?

单元测试(unit testing),是指对软件中的最小可测试单元进行检查和验证,这些测试有助于验证代码是否正常运行并识别任何错误或错误。至于“单元”的大小或范围,并没有一个明确的标准,“单元”可以是一个函数、方法、类、功能模块或者子系统。
这种类型的测试完全孤立地集中在整个应用程序的一部分上。通常,它是单个类或函数。理想情况下,该装置没有任何副作用。以便尽可能容易地对其进行隔离和测试。
单元测试是自动化测试。换句话说,单元测试是由软件(例如单元测试框架或单元测试工具)执行的,而不是由开发人员手动执行的。这意味着单元测试允许自动化、可重复、连续的测试。
作为开发人员,一旦编写完一段代码,就会编写单元测试。我们可能会问,这不是测试员的工作吗?在某种程度上,是的,测试人员负责测试软件。但是,覆盖每一行代码给测试人员增加了很大的压力。因此,开发人员为自己的代码编写测试也是最佳实践。

😘优点:

  1. 质量问题识别:单元测试有助于识别与安全性、保密性和可靠性相关的缺陷,确保更高的代码质量。
  2. 功能需求的验证:单元测试确保满足各个代码单元的功能需求,从而促进准确的功能。
  3. 结构代码覆盖率:单元测试有助于实现全面的代码覆盖率,确保测试不同的代码路径。
  4. 合规性要求:单元测试有助于满足合规性要求,确保代码符合监管标准。
  5. 回归测试:可以重复使用单元测试来检测代码回归,从而确保更改或更新不会破坏现有功能。
  6. 简化调试:通过单独测试各个单元,单元测试简化了调试过程,从而更容易定位和修复问题。
  7. 应用程序指标:单元测试提供了有关应用程序运行状况的有价值的指标,并突出显示了值得关注的领域或潜在的瓶颈。
    提示:概念这个东西大概理解是什么意思即可~

🚀 为什么要进行单元测试?

单元测试的目标是确保任何新功能不会破坏现有功能。它还有助于在开发过程的早期发现任何问题或错误,并有助于确保代码符合组织制定的质量标准。
单元测试很重要,因为软件开发人员有时会尝试通过最少的单元测试来节省时间,但这是神话,因为不适当的单元测试会导致在系统测试、集成测试甚至应用程序构建后的 Beta 测试期间修复缺陷的成本很高。如果在早期开发中进行适当的单元测试,那么最终可以节省时间和金钱。
通过进行单元测试,开发人员可以在集成不同组件并进行回归测试之前更好地控制各个代码块的质量。它还有助于及早识别和解决代码级别的错误或缺陷。与在开发周期后期发现缺陷相比,这种主动方法显著降低了成本。

🚀 工作流程

该测试一般分为四个步骤:

  1. 第一步是创建测试用例或弄清楚想要测试什么以及如何测试。
  2. 第二步是编写测试代码。
  3. 第三步是运行测试,如果测试通过,代码就会执行它应该执行的操作,可以继续。如果测试失败,则代码中存在错误,需要在继续之前对其进行调试。
  4. 第四步是维护测试。当代码发生变化时,需要相应地更新测试。这可能意味着添加新的测试用例或修改现有的测试用例。

如何编写

🐬 编写步骤

OK,万丈高楼平地起,我们来看一下单元测试该怎么来一步步编写。
对于一个新手来说,以下是编写单元测试的一般步骤:

  1. 确定被测试的代码:首先,确定要测试的代码。这可以是一个方法、一个类或者一个模块。确保对被测试代码的功能和预期行为有清晰的理解。
  2. 引入测试框架和依赖:选择适合的项目测试框架,如 JUnit、TestNG 等,并将其添加到项目的依赖中。可能还需要引入其他的测试工具或模拟库,如 Mockito、PowerMockito 等。
  3. 创建测试类:创建一个新的测试类,命名规范一般为被测试类名 + “Test” 后缀,例如,如果被测试类是 Calculator,则测试类可以命名为 CalculatorTest。在测试类中,导入相关的测试框架和依赖。
  4. 编写测试方法:在测试类中,编写测试方法来测试被测试代码的各种情况。每个测试方法应该是独立的,测试不同的方面或场景。使用 @Test 注解来标记测试方法。
  5. 准备测试数据:为了执行测试,可能需要准备一些测试数据。这可以是输入参数、模拟对象或者设置测试环境。确保测试数据能够覆盖不同的情况和边界条件。
  6. 执行测试:使用测试框架提供的运行器或者 IDE 的测试运行功能来执行测试。确保所有的测试方法都能够运行,并检查测试结果。
  7. 断言结果:在每个测试方法中,使用断言语句来验证被测试代码的行为和结果。根据预期结果,选择合适的断言方法来判断实际结果是否符合预期。
  8. 处理异常情况:确保我们的测试代码覆盖到被测试代码可能引发的异常情况,并验证被测试代码在异常情况下的行为和处理逻辑。
  9. 重复和扩展:根据需要,编写更多的测试方法来覆盖更多的场景和边界情况。重复执行测试,确保代码的稳定性和可靠性。
  10. 持续集成和自动化:将单元测试集成到持续集成流程中,并使用自动化测试工具和脚本来自动运行测试。这样可以确保每次代码变更都能够及时进行测试,并提供及时反馈。
    这些是编写单元测试的一般步骤,但实际编写过程可能因项目需求和具体情况而有所不同。关键是理解被测试代码的功能和预期行为,并编写针对不同情况的测试方法来验证其正确性。
    好了,现在知道该怎么编写了吧?快去写吧!哈哈,开玩笑的,下面的部分会介绍怎么使用测试框架 Mockito 来进行单元测试的编写。

🐬 编写原则

单元测试用例的原则是什么:

  • 测试类应该只测试一个功能,否则会违反了单一职责原则。
  • 测试代码时,只考虑测试,不考虑内部实现。
  • 数据尽量模拟现实,越靠近现实越好。
  • 充分考虑数据的边界条件下。
  • 对重点、复杂、核心代码、重点测试。
  • 测试、功能开发相结合,有利于设计和代码重构。

🐬 覆盖率

单元测试覆盖率是一种衡量软件测试质量的指标,用于评估在单元测试中对源代码的覆盖情况。它衡量了测试用例能够覆盖代码中多少部分的执行路径和逻辑。单元测试覆盖率可以帮助开发人员确定测试案例是否足够全面,是否覆盖了代码的各个执行分支和边界条件。

原理

单元测试覆盖率的原理是通过分析源代码的执行路径,确定哪些部分被测试用例覆盖,哪些部分未被覆盖。覆盖率工具会在代码执行期间跟踪记录代码的执行情况,生成相应的统计信息。这些统计信息可以用来计算代码行、分支、条件等的覆盖情况。

分类

单元测试覆盖率通常分为以下几种类型:

  1. 语句覆盖(Statement Coverage): 衡量测试用例是否覆盖了源代码中的每一条语句。它关注的是代码中的每个语句是否被执行到。
  2. 分支覆盖(Branch Coverage): 衡量测试用例是否覆盖了源代码中所有的分支。它关注的是条件语句(如 if、switch)中每个可能的分支是否都被覆盖到。
  3. 条件覆盖(Condition Coverage): 衡量测试用例是否覆盖了源代码中的每个条件。它关注的是每个条件表达式的每个可能取值是否都被覆盖到。
  4. 路径覆盖(Path Coverage): 衡量测试用例是否覆盖了源代码中的每条可能路径。它关注的是代码中所有可能的执行路径是否都被覆盖到。

计算方法

单元测试覆盖率通常以百分比的形式表示,表示覆盖的代码部分在总代码中的比例。计算方法可以根据不同的覆盖率类型进行调整。
一般而言,覆盖率计算公式如下:
覆盖率 = (被覆盖的代码部分数量 / 总代码部分数量) * 100%
例如,假设有一段代码有 100 条语句,其中 80 条语句被测试用例覆盖到,那么语句覆盖率为 80%。

对于分支覆盖、条件覆盖和路径覆盖,计算方法略有不同,需要考虑代码中的分支、条件或路径数量。

在实际应用中,可以使用各种测试覆盖率工具来自动计算代码的覆盖率。常见的 Java 测试覆盖率工具包括 JaCoCo、Cobertura、Emma 等。这些工具可以生成详细的覆盖率报告,帮助开发人员分析测试用例的覆盖情况并进行优化。

分支覆盖和条件覆盖的区别

分支覆盖和条件覆盖是单元测试覆盖率中的两个相关概念,它们有以下区别:

分支覆盖(Branch Coverage):
分支覆盖衡量测试用例是否覆盖了源代码中的所有分支。分支是指条件语句(如 if、switch)中的每个可能的路径。分支覆盖要求每个条件语句的每个分支都至少被测试用例执行一次。它关注的是代码中分支的执行情况,以确保测试用例能够覆盖不同的条件路径。
示例代码:

if (x > 0) {
// 分支1
System.out.println("x is positive");
} else {
// 分支2
System.out.println("x is non-positive");
}

在这个例子中,分支覆盖要求至少有一个测试用例覆盖 x > 0 条件为真的情况(分支1)和至少有一个测试用例覆盖 x > 0 条件为假的情况(分支2)。

条件覆盖(Condition Coverage)
条件覆盖衡量测试用例是否覆盖了源代码中的每个条件。条件是指条件语句中的每个条件表达式。每个条件表达式通常包含一个或多个子条件,例如逻辑运算符(如 &&、||)组合的多个条件。条件覆盖要求每个条件表达式的每个可能取值至少被测试用例覆盖一次。它关注的是代码中条件的执行情况,以确保测试用例能够覆盖不同的条件组合。
示例代码:

if (x > 0 && y < 10) {
// 条件1为真,条件2为真
System.out.println("Both conditions are true");
} else {
// 条件1或条件2至少有一个为假
System.out.println("At least one condition is false");
}

在这个例子中,条件覆盖要求至少有一个测试用例覆盖 x > 0 条件为真且 y < 10 条件为真的情况,以及至少有一个测试用例覆盖其他三种情况(其中一个条件为假)。

总结:

  • 分支覆盖关注的是代码中分支的执行情况,要求每个分支至少被一个测试用例执行一次。
  • 条件覆盖关注的是代码中条件的执行情况,要求每个条件表达式的每个可能取值至少被一个测试用例执行一次。
    通常情况下,条件覆盖是分支覆盖的一种更严格的要求。也就是说,如果测试用例满足了条件覆盖,那么它一定也满足了分支覆盖。但是,分支覆盖可能包含一些额外的情况,因为一个分支中可能包含多个条件表达式。因此,在设计测试用例时,可以根据具体情况选择使用分支覆盖或条件覆盖,以满足测试的要求。

mock 和 mockito

🛵 基础知识

在软件开发中提及”mock”,通常理解为模拟对象。

🤔为什么需要模拟(mock)?

在我们一开始学编程时,我们所写的对象通常都是独立的,并不依赖其他的类,也不会操作别的类。但实际上,软件中是充满依赖关系的,比如我们会基于service类写操作类,而service类又是基于数据访问类(DAO)的,依次下去,形成复杂的依赖关系。
单元测试的思路就是我们想在不涉及依赖关系的情况下测试代码。这种测试可以让你无视代码的依赖关系去测试代码的有效性。核心思想就是如果代码按设计正常工作,并且依赖关系也正常,那么他们应该会同时工作正常。
有些时候,我们代码所需要的依赖可能尚未开发完成,甚至还不存在,那如何让我们的开发进行下去呢?使用mock可以让开发进行下去,mock技术的目的和作用就是模拟一些在应用中不容易构造或者比较复杂的对象,从而把测试与测试边界以外的对象隔离开。
我们可以自己编写自定义的Mock对象实现mock技术,但是编写自定义的Mock对象需要额外的编码工作,同时也可能引入错误。现在实现mock技术的优秀开源框架有很多,Mockito就是一个优秀的用于单元测试的mock框架。
比如,在使用spring开发时有时候我们并不想把整个spring容器跑起来。有了mockito我们就可以方便的对一些依赖项进行mock,这样我们只需要验证需要测试的一小部分代码逻辑,测试类也会跑的很快。

小贴士
JUnit 是单元测试框架。Mockito 与 JUnit 不同,并不是单元测试框架(这方面 JUnit 已经足够好了),它是用于生成模拟对象或者直接点说,就是”假对象“的工具。两者定位不同,所以一般通常的做法就是联合 JUnit + Mockito 来进行测试。

小记
一、为什么需要 mock ?
Mock 可以理解为一个虚假的对象,模拟出一个对象,在测试环境中用来替换掉真实的对象,以达到我们可以:
1、验证该对象某些方法的调用情况,调用了多少次,参数是什么。
2、给这个对象的行为做一个定义,来指定返回结果或者是指定特定的动作。

二、Mock方法
Mockito.mock(类):Mock 方法模拟出一个对象或者接口

三、验证和断言
Mockito.verify()方法:校验待验证的的对象是否发生过某些行为。verify 配合 time() 方法可以校验某些操作发生的次数:Mockito.verify(对象, Mockito.times(1)).nextInt()

四、给 Mock 对象打桩
意思就是给 mock 对象的行为做定义,让 mock 对象执行操作后返回提前规定好的值。Mockito.when(对象. 方法). 要求的相应。

五、@Mock 注解
可以理解为快速创建 mock 对象的一种方法,之前是用 Mockito.mock(对象)方法,现在定义一个类直接在类上添加 @Mock 注解来代替。
要注意的是 mock 注解要配合 MockitoAnnototions.openMocks(类) 方法一起使用,来使 mock 注解生效。

六、@BeforeEach 和 @AfterEach 注解
beforeEach 注解的方法是执行测试前的准备。
beforeAfter 注解的方法是执行测试后执行的。

七、Spy 方法和 @Spy 注解
Spy 方法和 mock 方法不同的是 spy 的对象会走真实的方法,而 mock 的对象不会;
Spy 方法的参数是对象实例,mock 方法的参数是类。

🛵 基础使用

下面将介绍 Mockto 的基本使用,包括如何利用 Mockito 创建模拟对象,如何打桩,调用完方法后怎么做验证。在对应的代码片段中将展示如何使用。

添加依赖

在 Maven 项目中使用 Mockito,将以下依赖项添加到 pom 文件中

<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-inline</artifactId>
<version>{mockitoversion}</version>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
<version>{mockitoversion}</version>
<scope>test</scope>
</dependency>

mockito-inline 是 mockito-core 的 Java 16 就绪版本。它还支持静态方法和最终类的模拟,在第四部分会说明。

使用 Mockito API 创建模拟对象

Mockito 提供了几种创建模拟对象的方法:

  • 将JUnit 5 的扩展与字段注释@ExtendWith(MockitoExtension.class)结合使用@Mock
  • 使用静态mock()方法。
  • 使用@Mock注释。
    如果使用@Mock注解,则必须触发注解字段的初始化。MockitoExtension通过调用静态方法来完成此操作MockitoAnnotations.initMocks(this)。

现在假如我们有一个 Database 类,Service 类,在 Service 类中使用了 Database 类。

public class Database {

public boolean isAvailable() {
return false;
}
public int getUniqueId() {
return 42;
}
}
public class Service {

private Database database;

public Service(Database database) {
this.database = database;
}

public boolean query(String query) {
return database.isAvailable();
}


@Override
public String toString() {
return "Using database with id: " + String.valueOf(database.getUniqueId());
}
}

然后我们来使用 Mockito 模拟对象的单元测试Database

@ExtendWith(MockitoExtension.class)                         
class ServiceTest {

@Mock
Database databaseMock;

@Test
public void testQuery() {
assertNotNull(databaseMock);
when(databaseMock.isAvailable()).thenReturn(true);
Service t = new Service(databaseMock);
boolean check = t.query("* from t");
assertTrue(check);
}
}

配置mock对象方法调用的返回值

Mockito 允许配置通过 Fluent API 在模拟上调用的方法的返回值。

  • 🍓 when(….).thenReturn(….)
    @ExtendWith(MockitoExtension.class)
    class ServiceDatabaseIdTest {

    @Mock
    Database databaseMock;

    @Test
    void ensureMockitoReturnsTheConfiguredValue() {

    // 为方法getUniqueId()定义返回值
    when(databaseMock.getUniqueId()).thenReturn(42);

    Service service = new Service(databaseMock);
    // 在测试中使用mock
    assertEquals(service.toString(), "Using database with id: 42");
    }

    }
  • 🍓 doReturn when和doThrow when
    方法doReturn(…).when(…)配置可用于配置模拟方法调用的回复。这类似于when(….).thenReturn(….).
    class MockitoSpyWithListTest {

    @Test
    void ensureSpyForListWorks() {
    var list = new ArrayList<String>();
    var spiedList = spy(list);

    doReturn("42").when(spiedList).get(99);
    String value = (String) spiedList.get(99);

    assertEquals("42", value);
    }
    }

用 Spy 包装 Java 对象

@Spy 或该spy() 方法可用于包装真实对象。除非另有指定,否则每次调用都会委托给该对象。

@ExtendWith(MockitoExtension.class)
class MockitoSpyTest {

@Spy
List<String> spy = new LinkedList<>();

@BeforeEach
void setup() {
// 也可以在里创建 spy 对象
// List<String> list = new LinkedList<>();
// List<String> spy = spy(list);
}

@Test
void testLinkedListSpyCorrect() {
doReturn("foo").when(spy).get(0);

assertEquals("foo", spy.get(0));
}

}

verify() 验证

Mockito 会跟踪模拟对象的所有方法调用及其参数。
可以使用verify()模拟对象上的方法来验证是否满足指定的条件。例如,可以验证是否已使用某些参数调用了方法。这种测试有时称为行为测试。行为测试不检查方法调用的结果,但它检查是否使用正确的参数调用方法。

@ExtendWith(MockitoExtension.class)
public class MockitoVerifyTest {


@Test
public void testVerify(@Mock Database database) {
// 打桩
when(database.getUniqueId()).thenReturn(43);

// 使用参数12用模拟对象调用方法
database.setUniqueId(12);
database.getUniqueId();
database.getUniqueId();

// 现在检查是否使用参数12调用了方法
verify(database).setUniqueId(ArgumentMatchers.eq(12));

// 验证这个方法的调用次数,这里是2就代表验证是否调用了两次
verify(database, times(2)).getUniqueId();

// 其他的验证次数方法
verify(database, never()).isAvailable();
verify(database, never()).setUniqueId(13);
verify(database, atLeastOnce()).setUniqueId(12);
verify(database, atLeast(2)).getUniqueId();

// times(numberOfTimes)
// atMost(numberOfTimes)
// 验证在此对象上没有调用其他方法
// 在验证了预期的方法调用后调用它
verifyNoMoreInteractions(database);
}

}

使用 @InjectMocks 进行依赖注入

被@InjectMocks注解的对象就是要被我们测试的对象,它会被自动的实例化。且其包含的成员变量会被相应的@Mock注解的对象自动赋值。
必须使用 Mockito.initMocks(this) 初始化这些模拟对象并注入它们。

@ExtendWith(MockitoExtension.class)
class ArticleManagerTest {

@InjectMocks
private ArticleManager manager;

@Mock
ArticleDatabase database;

@Mock
User user;

@Test
void shouldDoSomething() {
// 测试代码
}
}

Mockito 和 JUnit5

🍎 使用

All right ! 上面已经介绍了 Mockito 的基本使用方法,下面来个相对完整的。

环境准备

Spring Boot 2.4.2 + H2 + Lombok + Spring Boot Test (默认包含了Junit5 + Mockito)

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>com.example</groupId>
<artifactId>springboot-mockito</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>

<name>springboot-mockito</name>
<description>Demo project for Spring Boot</description>

<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.4.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>

<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
</properties>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
<version>2.21.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.8.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
<version>2.3.0</version>
</dependency>

</dependencies>

<build>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.2.1</version>
</plugin>
</plugins>
</pluginManagement>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>


</project>

提供以下类,然后使用 Mockito 测试 VolumeUtil。

package com.vogella.mockito.audio;

public enum RINGER_MODE {
RINGER_MODE_NORMAL, RINGER_MODE_SILENT
}
package com.vogella.mockito.audio;

public class AudioManager {
private int volume = 50;
private RINGER_MODE mode = RINGER_MODE.RINGER_MODE_SILENT;

public RINGER_MODE getRingerMode() {
return mode;
}
public int getStreamMaxVolume() {
return volume;
}
public void setStreamVolume(int max) {
volume = max;
}
public void makeReallyLoad() {
if (mode.equals(RINGER_MODE.RINGER_MODE_NORMAL)) {
setStreamVolume(100);
}
}

}
package com.vogella.mockito.audio;

public class MyApplication {

public int getNumberOfThreads() {
return 5;
}

}
package com.vogella.mockito.audio;

public class ConfigureThreadingUtil {
public static void configureThreadPool(MyApplication app){
int numberOfThreads = app.getNumberOfThreads();
// TODO
}
}
package com.vogella.mockito.audio;

public class VolumeUtil {
public static void maximizeVolume(AudioManager audioManager) {
if (audioManager.getRingerMode() != RINGER_MODE.RINGER_MODE_SILENT) {
int max = audioManager.getStreamMaxVolume();
audioManager.setStreamVolume(max);
}
audioManager.setStreamVolume(50);
}
}

开始测试 VolumeUtil 类

package com.vogella.mockito.audio;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import static org.mockito.Mockito.*;
import static org.mockito.Mockito.verifyNoMoreInteractions;

@ExtendWith(MockitoExtension.class)
class VolumeUtilTests {

@Mock
AudioManager audioManager;


@Test
void ensureThatMaximizeVolumeUseConfiguredValueFromAudiomanager() {
// 1. 打桩
when(audioManager.getRingerMode()).thenReturn(RINGER_MODE.RINGER_MODE_NORMAL);
when(audioManager.getStreamMaxVolume()).thenReturn(100);

// 2. 测试感兴趣的代码即可
VolumeUtil.maximizeVolume(audioManager);

// 3. 验证
verify(audioManager).setStreamVolume(100);
}

@Test
void ensureSilentModeWillNotSetVolumeIsNotDisturbed() {
// 1. mock 对象
AudioManager audioManager = mock(AudioManager.class);
when(audioManager.getRingerMode()).thenReturn(RINGER_MODE.RINGER_MODE_SILENT);
// 2. 测试
VolumeUtil.maximizeVolume(audioManager);
// 3. 验证
verify(audioManager).getRingerMode();
verifyNoMoreInteractions(audioManager);
}
}

第二个报错,发现 VolumeUtil 的错误。

修改 VolumeUtil 类

为ConfigureThreadingUtil编写一个新的Mockito测试

package com.vogella.mockito.audio;

import static org.mockito.Mockito.only;
import static org.mockito.Mockito.verify;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

@ExtendWith(MockitoExtension.class)
class ConfigureThreadingUtilTests {

@Mock
MyApplication app;

@Test
void ensureThatThreadPoolCanBeConfigured() {

ConfigureThreadingUtil.configureThreadPool(app);
verify(app, only()).getNumberOfThreads();
}
}

🍎 高级使用

模拟 final 类和 static 方法

Mockito 的最新版本还可以模拟 final 类和静态方法。对于 final 类和 final 方法,Mockito 可以正常工作,对于静态方法,必须使用mockStatic方法。

package com.vogella.mockito.mockstatic;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.ArgumentMatchers.anyString;

import org.junit.jupiter.api.Test;
import org.mockito.MockedStatic;
import org.mockito.Mockito;

class MyStaticDemoTest {

@Test
void testStaticMockVoid() {
try (MockedStatic<Dummy> dummy = Mockito.mockStatic(Dummy.class)) {
dummy.when(Dummy::foo).thenReturn("mocked");
dummy.when(() -> Dummy.foo(anyString())).thenReturn("mockedValue");

assertEquals("mocked", Dummy.foo());
assertEquals("mockedValue", Dummy.foo("para"));
dummy.verify(() -> Dummy.foo());
dummy.verify(() -> Dummy.foo(anyString()));
}
}

static final class Dummy {
public int testing() {
return var1.length();
}

static String var1 = null;

static String foo() {
return "foo";
}

static String foo(String var2) {
var1 = var2;
return "SUCCESS";
}
}
}

使用反射和 Spy 来更改私有字段

Mockito 目前无法模拟私有字段或方法。应避免在测试期间更改私有字段或方法,而是在写测试时尝试重构代码。
不过从技术上讲,可以结合使用 @Spy 和 Java 反射访问。下面进行演示。
类准备

package com.vogella.mockito.withprivate;

public class MyClassWithPrivateFieldAndMethod {

public String field1 = "";
public String valueSetByPrivateMethod = "";
private String hiddenField = "initial";

public String getValue() {
return hiddenField;
}

public String getValueSetByPrivateMethod() {
return valueSetByPrivateMethod;
}

public String toBeMockedByMockito() {
return "stuff";
}

private void meineMethod() {
valueSetByPrivateMethod = "lalal";
}
}

假设现在想要编写一个测试并模拟该 toBeMockedByMockito 方法,使用反射和 Spy 来更改私有字段hiddenField。

package com.vogella.mockito;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.when;

import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Spy;
import org.mockito.junit.jupiter.MockitoExtension;

import com.vogella.mockito.withprivate.MyClassWithPrivateFieldAndMethod;

@ExtendWith(MockitoExtension.class)
class MyClassWithPrivateFieldAndMethodTest {

@Spy
MyClassWithPrivateFieldAndMethod mock = new MyClassWithPrivateFieldAndMethod();

@Test
void ensureSpyAndReflectiveAccessCanChangeAPrivateField() throws NoSuchFieldException, SecurityException,
IllegalArgumentException, IllegalAccessException, NoSuchMethodException, InvocationTargetException {
assertEquals("initial", mock.getValue());

mock.field1 = "Hello";

when(mock.toBeMockedByMockito()).thenReturn("mocked by Mockito");
Field declaredField = MyClassWithPrivateFieldAndMethod.class.getDeclaredField("hiddenField");
declaredField.setAccessible(true);

declaredField.set(mock, "changed");

assertEquals("Hello", mock.field1);
assertEquals("changed", mock.getValue());
assertEquals("mocked by Mockito", mock.toBeMockedByMockito());
}

}

使用反射调用私有方法

使用反射还可以调用私有方法并验证内部值是否已更改,这里演示一下。

package com.vogella.mockito;

import org.junit.Test;
import org.mockito.Mockito;

import java.lang.reflect.Method;

import static org.junit.Assert.assertEquals;

public class MyClassWithPrivateFieldAndMethodTest {

@Test
public void testPrivateMethodInvocation() throws Exception {
// 创建真实对象的 spy(代理)对象
MyClassWithPrivateFieldAndMethod myObjectSpy = Mockito.spy(new MyClassWithPrivateFieldAndMethod());

// 使用反射获取私有方法
Method privateMethod = MyClassWithPrivateFieldAndMethod.class.getDeclaredMethod("meineMethod");
privateMethod.setAccessible(true);

// 调用私有方法
privateMethod.invoke(myObjectSpy);

// 验证内部值是否已更改
assertEquals("lalal", myObjectSpy.getValueSetByPrivateMethod());
}
}

或者,还可以为要模拟的类实现一个包装类,它以不同的方式实现内部或重构代码。