zoukankan      html  css  js  c++  java
  • 小酌重构系列[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()方法本身的逻辑。

    image

    包装模式

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

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

    image

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

  • 相关阅读:
    TreeSet类的排序问题
    TreeSet()详解
    css中vertical-align垂直居中的认识
    CSS3 float深入理解浮动资料整理
    层叠顺序与层叠上下文
    jquery.zclip轻量级复制失效问题
    文章转载利用border、transparent实现微风
    转载利用线性渐变实现晴天、多云特效
    转载利用伪元素单个颜色实现 hover 和 active 时的明暗变化效果
    MUI-最接近原生App体验的前端框架
  • 原文地址:https://www.cnblogs.com/keepfool/p/5476039.html
Copyright © 2011-2022 走看看