# 使用Mock Object进行TDD

在编写单元测试的时候很容易遇到这样的问题:

  • 对于接口的调用涉及到多层调用
  • 接口的实现还没完成
  • 涉及到对于硬件、数据库的操作,使用大数据量的测试很占用机器资源,测试运行较复杂

这样的问题如果你不以TDD的方式进行编程的话,可能不是一个显著的问题(比如你先把底层实现了之后再写上层),而在TDD中是一定会发生的。这时候就必须引入Mock Object(伪对象)来模拟接口的实现。

# 什么是TDD?

TDD(Test Driven Development),即测试驱动开发。它是敏捷开发的最重要的部分。方法主要是先依据客户的需求编写测试程序,然后再编码使其通过测试。在敏捷开发实施中,开发人员主要从两个方面去理解测试驱动开发:

  • 在测试的辅助下,快速实现客户需求的功能。通过编写测试用例,对客户需求的功能进行分解,并进行系统设计。从使用角度对代码的设计同城更符合后期开发的需求。可测试的要求,对代码的内聚性的提高和复用都非常有益。
  • 在测试的保护下,不断重构代码,提高代码的重用性,从而提高软件产品的质量。可见测试驱动开发实施的好坏确实极大的影响软件产品的质量,贯穿了软件开发的始终。

在实际开发中,对于接口的测试(而不是对于具体类的测试)是TDD的主要方式,因此接口的稳定性非常重要。接口的变化带来的变化是巨大的。在设计阶段就应该保证接口的稳定性。

# 使用Mock Object进行接口测试

如上文所说,在需要和系统内的某个模块或实体进行交互的时候,这些模块和实体可能并不存在,这时候开发人员需要使用MO技术进行单元测试。

例如:我们有一个电视机的接口程序:(源码参考IBM Developer)

public interface VideoCardInterface{
    public void open();
    public void changeChannel(int i);
    public void close();
    public Byte[] read();
    public boolean fault();
}

在编写MO的时候,初学者很容易犯的一个错误是对于MO的轻视所造成的编写的不完整。由于MO是为了测试服务的,在开发人员手动编写MO时,应该保证MO实现原接口的所有逻辑功能。由于测试是以接口方法为单位,并且包括方法间上下文调用的,因此可能会存在上下文的逻辑错误。MO的编写应该能使测试发现这样的问题。

public class MockVCHandler implements VideoCardInterface { 
 
    private boolean initialized = false; 
    private boolean error = false; 
    private int channel; 
    private static final int DEFAULTCHANNEL = 1; 
 
    public void open() { 
        initialized = true; 
        channel = DEFAULTCHANNEL; 
    } 
 
    public void changeChannel(int i) { 
        if (!initialized) { 
            Assert.fail("Trying to change channel before open"); 
        } 
        if (i <= 0) { 
            Assert.fail("Specified channale is out-of-range"); 
        } 
        this.channel = i; 
    } 
 
    public void close() { 
        if (!initialized) { 
            Assert.fail("Trying to close before open"); 
        } 
    } 
 
    public Byte[] read() { 
        if (!initialized) { 
            Assert.fail("Trying to read before open"); 
            return null; 
        } 
        if (channel > 256) { 
            error = true; 
            Assert.fail("Channel is out-of-range"); 
        } 
        return new Byte[] { '0', '1' }; 
    } 
 
    public boolean fault() { 
        return error; 
    } 
}

由此可见,手动编写MO并不是一件容易的事情。实际上我们发现MO可编写应该根据MO的实际作用得到简化。MO的作用无非是:

  • 模拟接口方法的返回值
  • 模拟接口方法调用后可能会有的影响

这意味着,接口方法内部实现不应是MO关心的地方。因此,一些MO的框架应运而生。目前Java的主要Mock Object测试工具有:jMock、MockCreator、MockRunner、EasyMock、MockMaker等。在微软.Net阵营中主要是:NMock、.NetMock、Rhino Mocks和Moq等。

# jMock框架的使用

jMock是一个轻量级的模拟对象技术的实现。它的特点包括:

  • 可以用简单易行的方法定义模拟对象,无需破坏本来的代码结构表。
  • 可以定义对象之间的交互,从而增强测试的稳定性。
  • 可以集成到测试框架。
  • 易扩充。

jMock可以添加maven依赖或者在官网中下载。

# 使用jMock接口

使用框架的好处是,框架使用对象来模拟上下文,上下文可以模拟出对象和对象的输出,并且还可以检测应用是否合法。

需要进行模拟的接口:

public interface ICalculatorService { 
 
    public int add(int a, int b); 
 
}

使用jMock可以对接口方法返回值进行模拟:

import org.jmock.Mockery; 
import org.jmock.Expectations; 
 
public class AJmockTestCase { 
 
    Mockery context = new Mockery(); 
 
    public void testCalcService() { 
 
        // set up 
        final ICalculatorService calcService = context 
                .mock(ICalculatorService.class); 
 
        final int assumedResult = 2; 
 
        // expectations 
        context.checking(new Expectations() { 
            { 
                oneOf(calcService).add(1, 1); 
                will(returnValue(assumedResult)); 
            } 
        }); 
 
        System.out.println(calcService.add(1, 1)); 
 
    } 
 
}