07_project_09_mockito

第七十一章 Mocking框架Mockito

1. 简介

Mockito是一款开源的Mocking测试框架(Open Source Mocking Testing Framework)。它常常与单元测试框架JUint结合使用,以帮助开发人员更好了完成单元测试。在单元测试中,开发人员常常使用测试覆盖率(Test Coverage)来量化单元测试的质量。测试覆盖率有多种计算方法,最为常见的方法有如下几种。

  1. 函数覆盖率(Function Coverage)用来量化单元测试覆盖了多少函数。100%函数覆盖率是指单元测试覆盖测试了所有函数。
  2. 指令覆盖率(Statement Coverage/Instruction Coverage)用来量化单元测试覆盖了多少指令/语句。100%指令覆盖率是指单元测试覆盖测试了所有的指令。
  3. 分支覆盖率(Branch Coverage)用来量化单元测试覆盖了多少分支。100%分支覆盖率是指单元测试覆盖了所有的分支。

从上述的测试覆盖率的定义来看,我们可以粗略的认为测试覆盖率越高,单元测试的质量越高。但是,在某些情况下,做到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'
}

2. 使用方法

本章将使用以下几个示例介绍Mockito的基本使用方法。更多的高级用法可参考Mockito文档。本章内容假设读者已熟悉JUnit 5的工作原理和使用方法。

2.1 基本使用方法

下面的示例可用于测试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;
    }
}

2.2 Mock标注(Mock Annotation)

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;
    }
}

2.3 调用原函数的实现

在某些情况下,开发人员可以希望调用原代码的实现,而不希望调用打桩代码的实现。因此,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;
    }
}

3. 小结

本章介绍了Mockito的基础使用方法。在Mocking技术的帮助下,开发人员可以任意设置函数的行为,以帮助更好的完成单元测试。在Java项目中,开发人员常常结合使用JUnitMockito来完成单元测试。

上一章
下一章

注册用户登陆后可留言

Copyright  2019 Little Waterdrop, LLC. All Rights Reserved.