Day 19-重构 (Refactoring) 与接缝 (Seam) - 1 (核心技术-11)

程序码设计框架对测试码可测试性的影响

今天进到核心技术的第三个系列—重构 (Refactoring) 与接缝 (Seam),那不免俗的先来看 Roy Osherove 提供的定义:

重构:在不改变程序码功能的前提下,修改程序码的动作。

接缝:程序码中可以抽换不同功能的地方,这些功能例如:使用虚设常式或模拟物件类别;增加一个建构函式(Constructor)参数;增加一个可设定的公开属性;把一个方法改成可供覆写的虚拟方法;又或是把一个委派拉出来变成一个参数或属性供类别外部来决定内容。

换言之,重构就是程序码改变(如精简化等)但程序码的行为与原先的一样;而接缝就比较复杂了,在探讨的是如何在一个类别或介面找到适合的切入点注入假物件的方法,单元测试要写得好,相伴的也要有清楚的设计模式(若采用紧耦合的写法则会像 Day-10 一样,无法找到适当的接缝点),同时,接缝也需符合开放封闭原则(Open-Closed Principle)的方式实作,可进行扩充但不直接修改内部原始码功能。

因此,我们在接手已撰写好的原始码,若无接缝点则一开始先需进行重构,而单元测试的艺术提供了两种重构方式解除依赖,其中後者是相依於前者,如下:

  • A 型:将具象类别(concrete class)抽象成介面(interfaces)或委派(delegates)

    • 撷取介面已便替换底层实作内容
  • B 型:重构程序码,以便将委派或介面的伪实作注入至目标物件中 (Ex: 被测试类别注入虚设常式)

    • 在建构函式注入一个假物件
    • 从属性的读取或设定中注入一个假物件
    • 在方法被呼叫前注入一个假物件

那一样,我们还是用程序码来说会比较清楚。


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

首先,先介绍今天的商业逻辑程序码 — LogAnalyzer,其实单元测试的艺术都是以这只类别做出发及扩充(但前几天我想说来设计看看自己的商业逻辑原始码,所以就没拿出来提了XD),其原始码如下:

public class LogAnalyzer
{
    public bool IsValidLogFileName(string fileName)
    {
        FileExtensionManager Manager = new FileExtensionManager();
        
        return Manager.IsValid(fileName);
    }
}

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

从这段原始码可以看出 LogAnalyzer 的 IsValidLogFileName 方法已经跟 FileExtensionManager 的 IsValid 方法产生紧耦合了(如同 Day-10 的情境)。那在进行 B 型重构之前,我们先需进行 A 型重构,产生接缝点:

public class LogAnalyzer
{
    private IExtensionManager Manager;

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

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

接下来来示范 B 型重构,第一个方式是在一开始初始化的时候就先注入相对应的物件(Day-11 也有类似的范例),程序码如下:

  1. 在建构函式注入一个假物件
public class LogAnalyzer
{
    private IExtensionManager Manager;
    
    public LogAnalyzer(IExtensionManager inManager)
    {
        Manager = inManager;
    }

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

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

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

除了像上述使用建构函式的方式处理外,还有属性注入的方式(get & set),程序码如下:

  1. 从属性的读取或设定中注入一个假物件
public class LogAnalyzer
{
    private IExtensionManager Manager;
    
    public LogAnalyzer()
    {
        Manager = new FileExtensionManager();
    }
    
    public IExtensionManager ExtensionManager
    {
        get { return Manager; }
        set { Manager = value; }
    }

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

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

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

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

这两种方式没有谁优谁劣,我的习惯是看所待的专案的风格大多采用哪一种,就采用那种(保持程序码风格一致)。而最後一种在方法被呼叫前注入一个假物件,则会在明天做介绍(这个可以写一个很深的坑,明天来讨论吧)。


<<:  自动化 End-End 测试 Nightwatch.js 之踩雷笔记:输入

>>:  第十四天:在 TeamCity 上执行程序码风格检查

Day 4 jest的生命周期

BeforeAll、BeforeEach、AfterEach、AfterAll 的四个生命周期,这四...

C语言杂谈01---如何理解条件编译

架构图 前言 由於地区翻译关系,有些书籍将macro翻译成"巨集",有些翻译成&...

[从0到1] C#小乳牛 练成基础程序逻辑 Day 8 - 变数 运算子 运算元

变数..装啊~再装啊~ | 一元 二元 ++ -- | 算术->数值 | 逻辑->T/...

番外:用Excel做资料处理的碎碎念

目前工作上大部分都是用excel在做资料上的处理。以前虽然也有用过excel做过分析,单都仅限於少量...

安装程序开发工具(IDE) Visual Studio 2019

Visual Studio 是微软开发的整合开发环境(IDE),简称 VS。 VS 能开发的程序语言...