Mockito 框架用于单元测试

单元测试中的Mock

Mock和Stub是单元测试工作中经常听到的名词。那么什么是Mocking?简而言之,就是创建一个受自己控制的实例(instance)或实现(implemention),替换被测代码中的依赖部分。

所谓受控的实例,是指依赖的行为是按自己所预期那样设计的,因而可以消除运行时的不确定性,解决测试用例运行时的环境依赖等问题,只关注核心代码逻辑的测试。

用一个实际业务系统来举例子,大多数的应用分为三层,分别是用户接口(User Interface),业务层(Business Layer)和数据接入层(Data Access Layer)。如下图所示。

Example: UnitTesting Dependencies

 

在图中可以看到,业务层有3个依赖,分别是数据接入层和两个其他的服务。设想这是一个地图App,那么它们包括:

  1. 一个比如Mysql的数据库或其他NoSql的数据库,用来存储数据。
  2. 一个外部的服务,例如定位服务,提供经纬度信息。
  3. 一个外部的服务,例如交通信息服务,提供实时交通信息。

若要用单元测试来测试这样一个地图App的业务逻辑,除非先部署好这3个依赖,否则测试用例就运行不起来。

采用Mock方法可以应对这种情况,不管依赖服务是否已经运行起来。因为我们将使用自己设定的结果替换掉依赖的服务。

测试替身(Test doubles)的分类

Mock本质上是一种“测试替身”——这是一个技术术语。“测试双”本质上是指一个被等效的实际对象实例或依赖所替代的对象。

测试替身有多种分类,具体来说:

1. Facks(伪替身)

伪依赖是一个和真实依赖一样能运行的实例,唯一的不同是Fack位于本地系统。

例如,在测试中使用一个简单的集合数据结构,或者内存数据,来代替实际依赖的生产数据库。

Type of Test Doubles - Fakes

2. Stubs(打桩)

打桩是使用一个预设的返回值代替调用实际依赖组件而返回的结果。

Type of Test Doubles- Stubs

  3. Spies(监视)

顾名思义,Spy是在调用实际依赖函数的时候,提供一种监视机制。这样就可以验证代码是否调到了某个函数,并拿到传入的参数进行验证。

Type of Test Doubles- Spies

4. Mocks(模拟)

Mock是一种特殊的对象实例,集合了打桩、监视的功能。既可以设定打桩,或者预设的返回值,也可以在事后验证mock对象的方法是否被正确调用到了。

例如,有一个报表生成器的函数,它在运行过程中向指定的地址发送电子邮件。由于我们不想发送实际的电子邮件,在测试期间,EmailService会被一次又一次地mock(发送电子邮件的电子邮件方法在被调用时被配置为不执行任何操作)。在测试结束时,我们只需要验证邮件服务确实调用到了我们mock的方法,并传进去了正确的邮箱地址。

Mock 框架

几乎所有的语言都提供了不同类型的mocking 框架。这里以Java的Mockito来举例。

假设我们要对一个应用程序进行单元测试,该应用程序计算一个学生在所有科目中的总分,并将其写入数据库。函数如下。

    public void calculateSumAndStore(String studentId, int[] scores) {
        int total = 0;
        for (int score : scores) {
            total = total + score;
        } // write total to DB
        databaseImpl.updateScores(studentId, total);
    }

我们要为方法calculateSumAndStore编写单元测试,但我们可能没有一个真正的数据库实现来存储总数。在这种情况下,我们将永远无法对该函数进行单元测试。

但是有了Mock之后,我们可以简单地传递一个用于数据库服务的Mock,并验证其余的逻辑。

测试代码的样例如下:

@Test

public void calculateSumAndStore_withValidInput_shouldCalculateAndUpdateResultInDb() {
    // Arrange
    studentScores = new StudentScoreUpdates(mockDatabase);
    int[] scores = {  60, 70, 90  };
    Mockito.doNothing().when(mockDatabase).updateScores("student1", 220);
 
    // Act
    studentScores.calculateSumAndStore("student1", scores);

    // Assert
    Mockito.verify(mockDatabase, Mockito.times(1)).updateScores("student1", 220);
}

我们在被测代码的父类中提供了一个mock的数据库对象mockDatabase,并在第6行通过打桩设定了它的返回结果:

(Mockito.doNothing().when(mockDatabase).updateScores(“student1”, 220);)

其中的要点是:

  1. 需要给mock对象里可能被调用的函数设置打桩结果。
  2. 创建打桩时的输入参数可以是特定的,也可以是通用化的。在本例中,打桩函数的入参就是特定的student1” & 220,因为我们明确知道代码调用它时的参数。
  3. 在验证结果时,我们检验:
    1. mockDatabase.updateScores方法被调用了
    2. 调用时传入的参数分别是student1” 和220
    3. uodataScores方法总共被调用了1次

我们接着改变一下测试代码,将打桩的入参从特定的“student1”改为anyString()和anyInteger(). anyxxx()是Mockito提供的匹配器,表示该类型任意的值。

测试代码如下:

    @Test
    public void calculateSumAndStore_withValidInput_shouldCalculateAndUpdateResultInDb() {
        // Arrange
        studentScores = new StudentScoreUpdates(mockDatabase);
        int[] scores = {60, 70, 90};
        Mockito.doNothing().when(mockDatabase).updateScores(anyString(), anyInt());
        // Act
        studentScores.calculateSumAndStore("student1", scores);
        // Assert
        Mockito.verify(mockDatabase, Mockito.times(1)).updateScores("student1", 220);
    }

这个时候,测试用例依旧是能通过的。

我们再改变一下代码,这次将验证语句中的值改掉。将220改成230.

    Test
    public void calculateSumAndStore_withValidInput_shouldCalculateAndUpdateResultInDb() {
        // Arrange
        studentScores = new StudentScoreUpdates(mockDatabase);
        int[] scores = {60, 70, 90};
        Mockito.doNothing().when(mockDatabase).updateScores(anyString(), anyInt());

        // Act
        studentScores.calculateSumAndStore("student1", scores);

        // Assert
        Mockito.verify(mockDatabase, Mockito.times(1)).updateScores("student1", 230);
    }

这时测试用例就通不过了,报异常信息:

Argument(s) are different! Wanted:
mockDatabase.updateScores(“student1”, 230);
-> at com.mocking.sampleMocks.StudentScoreUpdatesUnitTests.calculateSumAndStore_withValidInput_shouldCalculateAndUpdateResultInDb(StudentScoreUpdatesUnitTests.java:37)

Actual invocation has different arguments:
mockDatabase.updateScores(“student1”, 220);

告诉我们“student1”, 230这样的组合参数没有被调到。

小结

以上通过一个简单而直观的例子展示了使用Mockito进行mock的方法。

虽然简单,但只要知道了Mockito,其他语言、其他大多数通过mock进行的单元测试,流程和原理都是类似的。Mockito为广泛的mock需求提供了大量高级配置/支持,使用依赖注入注入模拟实例,提供了Spies来监视真正的方法调用并验证调用结果。

示例完整代码

接口 IDatabase.java

public interface IDatabase {
    public void updateScores(String studentId, int total);
}

被测代码 StudentScoreUpdates.java

public class StudentScoreUpdates {
    public IDatabase databaseImpl;

    public StudentScoreUpdates(IDatabase databaseImpl) {
        this.databaseImpl = databaseImpl;
    }

    public void calculateSumAndStore(String studentId, int[] scores) {
        int total = 0;
        for (int score : scores) {
            total = total + score;
        }
        // write total to DB
        databaseImpl.updateScores(studentId, total);
    }
}

测试用例类 – StudentScoreUpdatesUnitTests.java

public class StudentScoreUpdatesUnitTests {

    @Mock
    public IDatabase mockDatabase;

    public StudentScoreUpdates studentScores;

    @BeforeEach
    public void beforeEach()
    {
        MockitoAnnotations.initMocks(this);
    }

    @Test
    public void calculateSumAndStore_withValidInput_shouldCalculateAndUpdateResultInDb()
    {
// Arrange
        studentScores = new StudentScoreUpdates(mockDatabase);
        int[] scores = {60,70,90};
        Mockito.doNothing().when(mockDatabase).updateScores(anyString(), anyInt());

// Act
        studentScores.calculateSumAndStore("student1", scores);

// Assert
        Mockito.verify(mockDatabase, Mockito.times(1)).updateScores("student1", 230);
    }

}

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值