Mockito是一款开源的Mocking测试框架(Open Source Mocking Testing Framework)。它常常与单元测试框架JUint结合使用,以帮助开发人员更好了完成单元测试。在单元测试中,开发人员常常使用测试覆盖率(Test Coverage)来量化单元测试的质量。测试覆盖率有多种计算方法,最为常见的方法有如下几种。
从上述的测试覆盖率的定义来看,我们可以粗略的认为测试覆盖率越高,单元测试的质量越高。但是,在某些情况下,做到100%测试覆盖是比较困难的。因此,开发人员需要使用Mocking框架来帮助我们模拟(Mock)各种可能出现的场景。
针对被测试的类,Mocking框架会帮助开发人员生成一个Mock对象。该Mock对象与被测试的类对象有着相同的接口。开发人员能够灵活的设置Mock对象的行为,用以测试各种不同的场景。
如下面的代码所示。该代码示例使用了JDBC(Java Database Connectivity)技术连接一个本地运行的MySQL数据库。StudentManager类中的成员方法exists()可用于查看Student表中是否存在给定的id;方法getName()可用于查询id对应的学生的姓名。当id不存在时,返回null。StudentInfoHandler类中的成员方法process()会根据exists()方法的结果分别处理id存在、id不存在和抛出异常三种情况;当StudentManger::getName()方法返回null时,StudentInfoHandler::getName()方法会进行必要的错误处理。
为了在单元测试中达到100%分支测试覆盖率,在exists()和getName()方法中,开发人员需要测试id存在、id不存在和抛出异常三种情况。类似的,在process()方法中,开发人员也需要测试这三种情况。然而,在同一个环境下,同时测试这三种情况是困难的、耗时的。因此,开发人员设计了Mocking框架,创建出一系列Mock对象来替代真实对象,帮助开发人员模拟各种可能出现的情况。
package com.littlewaterdrop;
import java.sql.*;
public class StudentManager {
private static String dbEndpoint = "jdbc:mysql://localhost:3306/lw";
public boolean exists(String id) throws Exception {
Class.forName("com.mysql.jdbc.Driver");
try (Connection con = DriverManager.getConnection(dbEndpoint,"root","root")) {
Statement stmt=con.createStatement();
ResultSet rs=stmt.executeQuery("select * from Student where id = '" + id + "'");
while(rs.next()) {
return true;
}
return false;
}
}
public String getName(String id) {
String name = null;
try {
Class.forName("com.mysql.jdbc.Driver");
try (Connection con = DriverManager.getConnection(dbEndpoint,"root","root")) {
Statement stmt=con.createStatement();
ResultSet rs=stmt.executeQuery("select name from Student where id = '" + id + "'");
while(rs.next()) {
name = rs.getString("name");
break;
}
}
} catch (Exception ex) {
System.err.println(ex.getMessage());
}
return name;
}
}
public class StudentInfoHandler {
public int process (StudentManager manager, String id) {
try {
if (manager.exists(id)) {
return 0; // id 存在
} else {
return 1; // id 不存在
}
} catch (Exception ex) {
return 2; // 查询出错
}
}
public String getName(StudentManager manager, String id) {
String name = manager.getName(id);
if (name == null) {
// 必要的错误处理
}
return name;
}
public void main(String[] args) {
StudentManager manager = new StudentManager();
StudentInfoHandler handler = new StudentInfoHandler();
handler.process(manager, "0001");
}
}
如果在Maven项目中使用Mockito的话,需要在pom.xml中添加如下依赖。
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>3.5.15</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
<version>3.5.15</version>
<scope>test</scope>
</dependency>
如果在Gradle项目中使用Mockito的话,需要在build.gradle中添加如下依赖。
repositories {
mavenCentral()
}
dependencies {
testCompile group: 'org.mockito', name: 'mockito-core', version: '3.5.15'
testCompile group: 'org.mockito', name: 'mockito-junit-jupiter', version: '3.5.15'
}
本章将使用以下几个示例介绍Mockito的基本使用方法。更多的高级用法可参考Mockito文档。本章内容假设读者已熟悉JUnit 5的工作原理和使用方法。
下面的示例可用于测试StudentInfoHandler类。在使用Mockito进行测试时,开发人员需要先创建一个Mock对象用来模拟真实StudentManager对象。所以,在BeforeEach方法中,通过调用mock()方法创建一个mock对象。该对象"自动实现"了StudentManager类的方法。
然后,在expectReturnTrue()测试用例中,通过使用when().thenReturn()两个方法设置当在指定的函数调用发生时,返回指定的值。这种设置操作被称为打桩(Stubbing)。因此,我们可以在随后的代码中断言在Mock对象上调用方法getName("0000")将返回设置的学生姓名。所以,我们可以进一步测试StudentInfoHandler::getName()函数的逻辑。一旦在Mock对象上设置了返回值之后,无论调用多少次,Mock对象都会返回这个设置的值。
类似的,在expectThrowSQLException()测试用例中,通过使用when().thenThrow()两个方法设置当在指定的函数调用发生时,抛出指定的异常。因此,我们可以在随后的代码中断言在Mock对象上调用方法exists("0002")将抛出SQLException。所以,我们可以进一步测试process()函数的逻辑。
import static org.mockito.Mockito.*;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.fail;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.junit.jupiter.MockitoExtension;
import java.sql.SQLException;
import com.littlewaterdrop.StudentManager;
import com.littlewaterdrop.StudentInfoHandler;
@ExtendWith(MockitoExtension.class)
@DisplayName("Test StudentInfoHandler")
public class StudentInfoHandlerTest {
private StudentManager manager = null;
private StudentInfoHandler handler = null;
@BeforeEach
void init() {
manager = mock(StudentManager.class); // 创建一个StudentManager的Mock对象。
handler = new StudentInfoHandler();
}
@Test
@DisplayName("Test getName(\"0000\"), return Adam")
void expectReturnAdam() {
// 打桩:当调用 manager.getName("0000") 时,返回Adam。
when(manager.getName("0000")).thenReturn("Adam");
// 打桩后,manager.getName("0000")返回Adam。
assertEquals("Adam", manager.getName("0000"));
assertEquals("Adam", handler.getName(manager, "0000"));
}
@Test
@DisplayName("Test getName(\"0001\"), return null")
void expectReturnNull() {
// 打桩:当调用 manager.getName("0001") 时,返回null。
when(manager.getName("0001")).thenReturn(null);
// 打桩后,manager.getName("0001")返回null。
assertEquals(null, manager.getName("0001"));
assertEquals(null, handler.getName(manager, "0001"));
}
@Test
@DisplayName("Test exists(\"0002\"), throw SQLException")
void expectThrowSQLException() {
// 打桩:当调用 manager.exists("0002") 时,抛出SQLException。
try {
doThrow(new SQLException("DB connection interrupted.")).when(manager).exists("0002");
} catch (Exception ex) {
fail("Failed in stubbing SQLException");
}
// 打桩后,manager.exists("0002")抛出SQLException异常。
assertThrows(SQLException.class, () -> {manager.exists("0002");});
assertEquals(2, handler.process(manager, "0002"));
}
@AfterEach
void tearDown() {
manager = null;
handler = null;
}
}
Mockito框架为开发人员提供了方便的生成Mock对象的标注。开发人员可以使用@Mock标注来创建Mock对象。
import static org.mockito.Mockito.*;
import static org.junit.jupiter.api.Assertions.assertEquals;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import com.littlewaterdrop.StudentManager;
import com.littlewaterdrop.StudentInfoHandler;
@ExtendWith(MockitoExtension.class)
@DisplayName("Test StudentInfoHandler")
public class StudentInfoHandlerTest {
@Mock private StudentManager manager = null; // 使用@Mock标注
private StudentInfoHandler handler = null;
@BeforeEach
void init() {
handler = new StudentInfoHandler();
}
@Test
@DisplayName("Test getName(\"0000\"), return Adam")
void expectReturnAdam() {
// 打桩:当调用 manager.getName("0000") 时,返回Adam。
when(manager.getName("0000")).thenReturn("Adam");
// 打桩后,manager.getName("0000")返回Adam。
assertEquals("Adam", manager.getName("0000"));
assertEquals("Adam", handler.getName(manager, "0000"));
}
@AfterEach
void tearDown() {
manager = null;
handler = null;
}
}
在某些情况下,开发人员可以希望调用原代码的实现,而不希望调用打桩代码的实现。因此,Mockito还提供了doCallRealMethod(),用来调用原函数的实现。
import static org.mockito.Mockito.*;
import static org.junit.jupiter.api.Assertions.assertEquals;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.junit.jupiter.MockitoExtension;
import com.littlewaterdrop.StudentManager;
import com.littlewaterdrop.StudentInfoHandler;
@ExtendWith(MockitoExtension.class)
@DisplayName("Test StudentInfoHandler")
public class StudentInfoHandlerTest {
private StudentManager manager = null;
private StudentInfoHandler handler = null;
@BeforeEach
void init() {
manager = mock(StudentManager.class);
handler = new StudentInfoHandler();
}
@Test
@DisplayName("Call StudentManager.getName(\"0003\")")
void doARealCallToGetName() {
// 打桩:当调用 manager.getName("0003") 时,会调用StudentManager::getName()方法
doCallRealMethod().when(manager).getName("0003");
// 调用StudentManager::getName()方法来测试StudentInfoHandler::getName()方法。
assertEquals("Amy", handler.getName(manager, "0003"));
}
@AfterEach
void tearDown() {
manager = null;
handler = null;
}
}
本章介绍了Mockito的基础使用方法。在Mocking技术的帮助下,开发人员可以任意设置函数的行为,以帮助更好的完成单元测试。在Java项目中,开发人员常常结合使用JUnit和Mockito来完成单元测试。
注册用户登陆后可留言