image.png
👆Spock支持第三种虚拟对象:spiesSpy对象中一部分是真实方法,一部分是虚拟方法。之所以不在第六章介绍是因为Spy具有争议性——意味着代码本身有问题。通常使用在无法重构的遗留代码中。

例子

现有一安全工具——从外部录像机获取视频、检测闯入者并从硬盘从删除证据。应用由Java实现,第一个类负责删除文件,第二个类使用人脸识别算法检测敌人:

  1. public class CameraFeed {//获取录像帧
  2. [...code redacted for brevity...]
  3. public void setCurrentFrame(Image image){
  4. [...code redacted for brevity...]
  5. }
  6. }
  7. public class HardDriveNuker {//从硬盘删除文件
  8. public void deleteHardDriveNow(){//立即删除
  9. [...code redacted for brevity...]
  10. }
  11. }
  12. public class SmartHardDriveNuker extends HardDriveNuker{//实现人脸识别算法
  13. public void activate(CameraFeed cameraFeed){调用deleteHardDriveNow
  14. [...code redacted for brevity...]
  15. }
  16. }

👆上面设计不太好——人脸识别和删除文件在一个类里实现。你可以重构,但是不幸地是,代码被加密。
image.png
👆流程
现在不得不硬着头皮编写单元测试。

Spy

显然需要测试的是SmartHardDriveNuker类的activate()方法,activate()中调用deleteHardDriveNow()方法。每一次调用人脸识别方法就删除文件是不现实的,需要在不影响人脸识别算法正常运行的情况下mock删除文件方法。Spock支持Spy——调用真实方法,除非手工指定返回值:

def "automatic deletion of hard disk when agents are here"() {
    given: "a camera feed"
    CameraFeed cameraFeed = new CameraFeed()

    and: "the auto-nuker program"
    SmartHardDriveNuker nuker = Spy(SmartHardDriveNuker)//Spy
    nuker.deleteHardDriveNow() >> {println "Hard disk is cleared"}//mock不需要实际运行的方法

    when:"agents are knocking the door"
    cameraFeed.setCurrentFrame(ImageIO.read(getClass().getResourceAsStream( "agents.jpg")))
    nuker.activate(cameraFeed);//人脸识别算法运行

    then: "all files of hard drive should be deleted"
    1 * nuker.deleteHardDriveNow()//验证文件删除被调用
}

👆activate()执行人脸识别算法实际代码,而不执行删除文件的实际代码。

Spy意味着问题

image.png
只有在不能重构代码的情况下才使用Spy,一旦使用Spy就意味着代码违反OOP原则。

使用Mock/Stub代替Spy

假设现在可以重构代码,那么应该是这个样子的:

public class SmartHardDriveNuker{//不使用继承
    private final HardDriveNuker hardDriveNuker;//通过组合方式重用
    public SmartHardDriveNuker(final HardDriveNuker hardDriveNuker) {//构造方法注入
        this.hardDriveNuker = hardDriveNuker;
    }
    public void activate(CameraFeed cameraFeed) {
        [...code redacted for brevity..]
        hardDriveNuker.deleteHardDriveNow();//调用外部依赖方法
        [...code redacted for brevity..]
    }
}

def "automatic deletion of hard disk when agents are here"() {
    given: "a camera feed and a fake nuker"
    CameraFeed cameraFeed = new CameraFeed()
    HardDriveNuker nuker = Mock(HardDriveNuker)//Mock代替Spy

    and: "the auto-nuker program"
    SmartHardDriveNuker smartNuker = new SmartHardDriveNuker(nuker)//诸如

    when:"agents are knocking the door"
    cameraFeed.setCurrentFrame(ImageIO.read(getClass().getResourceAsStream("agents.jpg")))
    smartNuker.activate(cameraFeed);//调用算法

    then: "all files of hard drive should be deleted"
    1 * nuker.deleteHardDriveNow()//验证
}

👆可以看到使用Mock替代了Spy,源码也更符合OOP原则。