今天接下来会探讨第三种型别,并非透过建构函式或属性注入的方式建置假物件,而且在对被测试物件进行操作前才获得该物件的执行个体。因此,接下来会引用单元测试的艺术中提到以工厂类别的例子做示范,所以在这之前要先对工厂类别做个简单的概述,而这边以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());
}
}
那简单看完工厂模式,那接下来可以来看工厂模式对於昨天范例的写法。
那一样,我们先来看昨天 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:作者未提供第三种写法,其原因在於实务上工厂类别的方法极多,若所有的方法都撰写假方法,仅只用其中的三四成方法又或是更少,其效益极低。
除了上述提到的三种层次以外,那作者提供了第四种方式(不属於以上三种其中一种):使用一个区域的工厂方法(撷取与覆写),顾名思义,先在被测试类别写一只区域的工厂方法;而要撰写测试的时候,先写一只继承自被测试类别的类别(好像绕口令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
止损 止损顾名思义就是停止损失,今天在做企划的同时,世界并不会停下来等你发展,所以如果在做企划的同时...
此篇会介绍 Bootstrap 客制化所需的环境设置。 想先谈谈关於 Bootstrap 5 客制...
上午: AIoT资料分析应用系统框架设计与实作 今天教学一些Django的一些格式用法,及MTV架构...
前面讲了那麽多函式希望大家都有好好吸收, 那麽我们来到了基本函式的最後一个环节了喔。 也就是Type...
fundable.hk 实测创科局旗下创科生活基金 (FBL)开发 ”人工智能” App睇真D 资料...