Day 20-重构 (Refactoring) 与接缝 (Seam) - 2 (核心技术-12)

在方法被呼叫前注入一个假物件-前言 (以工厂类别为示范)

今天接下来会探讨第三种型别,并非透过建构函式或属性注入的方式建置假物件,而且在对被测试物件进行操作前才获得该物件的执行个体。因此,接下来会引用单元测试的艺术中提到以工厂类别的例子做示范,所以在这之前要先对工厂类别做个简单的概述,而这边以Yan(砚取歪)撰写的 工厂模式 Factory Pattern 为参考来源。

那为了因应中秋节,我们就以月饼做为工厂类别的范例吧XD,以下为工厂类别要实作的月饼产品:

// 月饼 (MoonCake)
public interface MoonCake 
{
    string moonCakeType();
}

// 月饼-原味 (TraditionalMoonCake)
public class TraditionalMoonCake : MoonCake 
{
    public string moonCakeType() {
        return "原味月饼";
    }
}

// 月饼-芋头 (TaroMoonCake)
public class TaroMoonCake : MoonCake 
{
    public string moonCakeType() {
        return "芋头月饼";
    }
}

以下则是工厂介面与工厂实作类别:

// 月饼工厂
public interface MoonCakeFactory
{
    public MoonCake generateMoonCake();
}

// 月饼工厂-原味 (TraditionalMoonCake)
public class TraditionalMoonCakeFactory : MoonCakeFactory
{
    public MoonCake generateMoonCake() {
        return new TraditionalMoonCake();
    }
}

// 月饼工厂-芋头 (TaroMoonCake)
public class TaroMoonCakeFactory : MoonCakeFactory
{
    public MoonCake generateMoonCake() {
        return new TaroMoonCake();
    }
}

简单测试工厂类别产制的月饼:

public class DemoMoonCakeFactoryTest {
    [Test]
    public void test(){
        // Arrange
        TaroMoonCakeFactory taroMCFactory = new TaroMoonCakeFactory();

        // Act
        var taroSample = taroMCFactory.generateMoonCake();

        // Assert
        Assert.AreEqual("芋头月饼", taroSample.moonCakeType());
    }
}

那简单看完工厂模式,那接下来可以来看工厂模式对於昨天范例的写法。


看程序码说故事 (Refactoring & Seam-2)

那一样,我们先来看昨天 LogAnalyzer 的原始码,只是这次的方式是要用工厂类别的方式新增;於是乎,程序码改写如下:

public class LogAnalyzer
{
    private IExtensionManager Manager;
    
    public LogAnalyzer()
    {
        Manager = ExtensionManagerFactory.Create();
    }

    public bool IsValidLogFileName(string fileName)
    {
        return Manager.IsValid(fileName);
    }
}

public interface IExtensionManager
{
    bool IsValid(string fileName);
}

public class FileExtensionManager
{
    public bool IsValid(string fileName)
    {
        // Read some file here
    }
}

public class ExtensionManagerFactory
{
    private IExtensionManager CustomManager = null;
    
    public IExtensionManager Create()
    {
        return new FileExtensionManager();
    }
    
    public void SetManager(IExtensionManager inCustomManager)
    {
        CustomManager = inCustomManager;
    }
}

透过工厂模式,我们把平常使用 FileExtensionManager 物件与做测试可做为接缝的 SetManager 方法都设想好了,这样的话撰写测试就可抽换成虚设常式,如下:

[TestFixture]
public class LogAnalyzerUnitTests
{
    [Test]
    public void DemoFactoryTest()
    {
        // Arrange
        var manager = Substitute.For<IExtensionManager>();
        
        manager.IsValid(default).ReturnsForAnyArgs(true);
        
        var ExtensionManagerFactory = new ExtensionManagerFactory();
        
        ExtensionManagerFactory.SetManager(manager);
        
        var log = new LogAnalyzer();
        
        // Act
        bool result = log.IsValidLogFileName("short.ext");
        
        // Assert
        Assert.True(result);
    }
}

如此一来,透过工厂模式的方式,就可以在工厂模式里面的 SetManager 抽换档案系统,改写成虚设常式。那在这边会提到第三种方式—在方法被呼叫前注入一个假物件,其原因在於许多已开发的专案伴随着不同的设计模式(又或是根本没有),因应不同的情况我们要思索出相对应的接缝点,以这个用工厂模式的范例,其实有好几个开设接缝点的策略,如被测试类别撰写建构函式、属性注入又或是在工厂模式新设方法注入。

此外,依据不同的接缝策略,所采取的策略也不一样,单元测试的艺术以 Day-19 与 Day-20 的范例提供了三种不同的中间层深度等级(撰写接缝的地方),见以下表单:

被测试类别 可以进行的操作
层次深度 1:针对类别的 FileExtensionManager 类别 新增建构函式或属性注入,被测试类别仅一个成员被伪造。
层次深度 2:针对工厂注入被测试类别的相依物件 透过工厂类别的赋值方法设定相对应假的相依物件。此时仅工厂内的成员为伪造的,被测试类别不需要调整。
层次深度 3:伪造假工厂类别 建立假的工厂类别,如此里面所有的工厂方法都可依自己意思撰写,可抽离相依物件的掌握度是最高的,但相对应撰写成本极高且使测试变复杂。

PS:作者未提供第三种写法,其原因在於实务上工厂类别的方法极多,若所有的方法都撰写假方法,仅只用其中的三四成方法又或是更少,其效益极低。


看程序码说故事 (Refactoring & Seam-3)

除了上述提到的三种层次以外,那作者提供了第四种方式(不属於以上三种其中一种):使用一个区域的工厂方法(撷取与覆写),顾名思义,先在被测试类别写一只区域的工厂方法;而要撰写测试的时候,先写一只继承自被测试类别的类别(好像绕口令XD)。

然後再这支类别里面新增建构函式(又或是属性注入),让假物件可注入进去,程序码如下:

public class LogAnalyzerUsingFactoryMethod
{
    public bool IsValidLogFileName(string fileName)
    {
        return GetManager().IsValid(fileName);
    }
    
    public virtual IExtensionManager GetManager()
    {
        return new FileExtensionManager();
    }
}

public class TestableLogAnalyzer : LogAnalyzerUsingFactoryMethod
{
    private IExtensionManager Manager;
    
    public TestableLogAnalyzer(IExtensionManager inManager)
    {
        Manager = inManager;
    }

    protected override IExtensionManager GetManager()
    {
        return Manager;
    }
}

public interface IExtensionManager
{
    bool IsValid(string fileName);
}

public class FileExtensionManager
{
    public bool IsValid(string fileName)
    {
        // Read some file here
    }
}

测试码:

[TestFixture]
public class LogAnalyzerUnitTests
{
    [Test]
    public void DemoExtractAndOverrideTest()
    {
        // Arrange
        var manager = Substitute.For<IExtensionManager>();
        
        manager.IsValid(default).ReturnsForAnyArgs(true);
        
        var log = new TestableLogAnalyzer(manager);
        
        // Act
        bool result = log.IsValidLogFileName("short.ext");
        
        // Assert
        Assert.True(result);
    }
}

这个手法的好处在於,如果原本的程序码没有适当的接缝点,新增继承的类别手法可避免原生的程序码;然而,若原本的程序码已经有适当的接缝点,且具备可测试性的性质,这样就反而多此一举。所以,接缝也算是单元测试中一门很重要的学问,写出好的接缝点也是需要经验累积。


<<:  【DAY 6】沟通 0 距离 - Micorsoft Teams 的应用技巧

>>:  Day6:今天来聊一下如何布署Microsoft Defender for Endpoints

企划实现(30)

止损 止损顾名思义就是停止损失,今天在做企划的同时,世界并不会停下来等你发展,所以如果在做企划的同时...

第 13 集:Bootstrap 客制化 Sass 环境

此篇会介绍 Bootstrap 客制化所需的环境设置。 想先谈谈关於 Bootstrap 5 客制...

Day16 职训(机器学习与资料分析工程师培训班): MVC & MTV架构

上午: AIoT资料分析应用系统框架设计与实作 今天教学一些Django的一些格式用法,及MTV架构...

Day26-TypeScript(TS)的函式多载(Overloads)

前面讲了那麽多函式希望大家都有好好吸收, 那麽我们来到了基本函式的最後一个环节了喔。 也就是Type...

香港政府旗下的创科生活基金,如何申请,申请资格细观

fundable.hk 实测创科局旗下创科生活基金 (FBL)开发 ”人工智能” App睇真D 资料...