小酌重构系列[9]——分解依赖

概述

编写单元测试有助于改善代码的质量,在编写单元测试时,某些功能可能依赖了其他代码(比如调用了其他组件)。
通常我们只想测试这些功能本身,而不想测试它所依赖的代码。

为什么呢?
单元测试的目标是验证该功能是否正确,然而功能所依赖的代码是处于功能范围外的,这些代码可能是一些外部的组件,单元测试无法验证这些外部组件的准确性。
单元测试因调用“依赖的代码”出错而失败时,会影响测试结果的判断,我们无法确定功能本身是否是正确的。
也许功能是正确的,但调用依赖的代码出错时,这个单元测试仍然会被认为是失败的。
如果要测试这些被依赖的代码,我们应该另外地为这些代码编写单元测试。

如何解决?
“依赖的代码”成了我们编写这类单元测试的拦路石,我们可以通过2种方式来解决这个问题:
1. Mock依赖的代码
2. 分解依赖

Mock的强大是毋庸置疑的,然而Mock不是万能的,它也是有限制的,我们不能在单元测试中Mock静态类
但是,通过“分解依赖”可以解决这个问题,本文将通过一个示例来演示这2种方式。

示例

重构前

这段代码描述了一个场景——“饲养动物”,它包含2个类:
AnimalFeedingService(动物饲养服务),以及静态类Feeder(饲养员)。
AnimalFeedingService描述了“饲养”行为,Feeder类描述了“补充食物”行为。
在饲养动物时,如果动物的食盆是空的,则饲养员需要补充食物。

/// <summary>
/// 动物饲养服务
/// </summary>
public class AnimalFeedingService
{
    private bool FoodBowlEmpty { get; set; }

    public void Feed()
    {
        if(FoodBowlEmpty)
            Feeder.ReplenishFood();
    }
}

/// <summary>
/// 饲养员
/// </summary>
public static class Feeder
{
    /// <summary>
    /// 补充食物
    /// </summary>
    public static void ReplenishFood()
    {
        
    }
}

单元测试代码(基于xUnit和Rhino.Mocks框架)

public class AnimalFeedingServiceTests
{
    [Fact]
    public void TestFeed()
    {
        AnimalFeedingService service = new AnimalFeedingService();
        // 测试Feed()方法
        service.Feed();
    }
}

由于Feeder是一个静态类,所以在为AnimalFeedingService编写单元测试时,我们无法mock Feeder类,而且Feeder类的功能我们也不想在这个单元测试中验证。
如果在调用Feeder.ReplenishFood()时就出错了,这个单元测试的执行就是失败的。
同时,Feed()方法的正确性也无法验证。

重构后

为了能在单元测试中解除对Feeder类的依赖,我们可以为Feeder类添加包装接口IFeederService,然后让AnimalFeedingService依赖于这个包装接口。这样在AnimalFeedingServiceTest类中,我们就不需要考虑Feeder类了。

显示代码
/// <summary>
/// 动物饲养服务
/// </summary>

public class AnimalFeedingService
{
    private bool FoodBowlEmpty { get; set; }

    public IFeederService FeederService { get; set; }

    public AnimalFeedingService(IFeederService feederService)
    {
        this.FeederService = feederService;
    }

    public void Feed()
    {
        if (FoodBowlEmpty)
            FeederService.ReplenishFood();
    }
}


/// <summary>
/// 饲养服务接口
/// </summary>
public interface IFeederService
{
    void ReplenishFood();
}

/// <summary>
/// 饲养服务实现
/// </summary>
public class FeederService : IFeederService
{
    public void ReplenishFood()
    {
        Feeder.ReplenishFood();
    }
}

/// <summary>
/// 饲养员
/// </summary>
public static class Feeder
{
    public static void ReplenishFood()
    {

    }
}

单元测试代码(基于xUnit和Rhino.Mocks框架)

public class AnimalFeedingServiceTests
{
    [Fact]
    public void TestFeed()
    {
        // 基于Rhino.Mocks框架的MockRepository
        var mocks = new MockRepository();
        // Mock IFeederService
        var feederService = mocks.DynamicMock<IFeederService>();
        AnimalFeedingService service = new AnimalFeedingService(feederService);
        // 测试Feed()方法
        service.Feed();
    }
}

重构后的AnimalFeedingServiceTests,由于IFeederService是Mock的,所以IFeederService的ReplenishFood()方法根本不会被调用,因此Feeder的ReplenishFood()方法也不会被调用。
调用Feed()方法时,Feeder.ReplenishFood()方法被忽略了,这可以让我们更加关注于Feed()方法本身的逻辑。

小酌重构系列[9]——分解依赖_第1张图片

包装模式

以上这段代码,实际上是一个简单的包装模式(Wrapper Pattern),包装模式不能算是一个具体的设计模式,因为在适配器模式(Adapter Pattern)、装饰模式(Decorator Pattern)、代理模式(Proxy Pattern)、门面模式(Facade Pattern)等都使用了包装类,这几个设计模式我在这里就不具体描述了。通常情况下,包装类用于封装其他class或component,包装类可以为上层调用提供便利性,并使底层class或component的运用变得更安全。

包装模式的UML结构大致如下。它包含3个部分:调用方(Client)、包装(Wrapper)、组件(Component)

小酌重构系列[9]——分解依赖_第2张图片

再来谈谈“分解依赖”这个概念,依照这个结构,我们可以很清楚地知道:为了解除Client和Component之间的依赖,我们在Client和Component之上添加了一个Wrapper,让Client依赖于Wrapper。请注意,由于我们更加倾向于面向接口编程,所以通常这个Wrapper是一个包装接口。再举一个常用的例子,当我们在编写一些API时,我们只需要通过Wrapper向Client公开API的规格(名称、输入输出参数),API的内部实现则通过Wrapper隔离开来。

你可能感兴趣的:(小酌重构系列[9]——分解依赖)