今天文章的内容是参考於 C# - how to inject, mock or stub DateTime for unit tests,今天为接续後面三个方法的探讨。
我们在 Day-20 曾提到 Roy Osherove 在单元测试的艺术提过区域工厂的概念,其意思是在被测试类别针对要被抽离的物件撰写一个区域工厂方法,在写商业逻辑的时候可呼叫该方法新增物件。如此在撰写测试的时候,可写一个类别继承该类别并改写这个工厂方法的内容,已达到假物件注入的手法,同样的手法也可适用在 DateTime.Now,程序码如下:
public class Decision
{
public string WhatToDo()
{
var currentDateTime = GetDateTime();
if (currentDateTime.Hour > 8 && currentDateTime.Hour < 18)
{
return Work();
}
else if (currentDateTime.Hour > 18 && currentDateTime.Hour < 22)
{
return Exercise();
}
else
{
return Sleep();
}
}
protected virtual DateTime GetDateTime()
{
return DateTime.Now;
}
private string Work()
{
return "Work!";
}
private string Exercise()
{
return "Exercise!";
}
private string Sleep()
{
return "Sleep!";
}
}
因此,我们就可以写一只继承该类别的含假物件类别(撷取与覆写),如下:
public class StubDecision : Decision
{
private readonly DateTime thisDateTime;
public StubDecision(DateTime inThisDateTime)
{
thisDateTime = inThisDateTime;
}
protected override DateTime GetDateTime()
{
return thisDateTime;
}
}
好,那最後就是测试码:
[Test]
public void WorkTest()
{
// Arrange
var decision = new StubDecision(new DateTime(2020, 01, 01, 08, 00, 00));
// Act
var whatToDo = decision.WhatToDo();
// Assert
Assert.Equal("Work!", whatToDo);
}
[Test]
public void ExerciseTest()
{
// Arrange
var decision = new StubDecision(new DateTime(2020, 01, 01, 18, 00, 00));
// Act + Assert
// ... 结构与 WorkTest 都一样
}
[Test]
public void SleepTest()
{
// Arrange
var decision = new StubDecision(new DateTime(2020, 01, 01, 23, 00, 00));
// Act + Assert
// ... 结构与 WorkTest 都一样
}
本方法是采用 C# Func 的写法,Func 是委派手法中有回传值的(若想了解什麽是委派可查询关键字 delegate),因次我们可以在建构函式的时候 Func 手法,在撰写商业逻辑时,程序码中代入真实时间,而在测试时,透过委派的手法给予假时间,程序码如下:
public class Decision
{
public string WhatToDo(Func<DateTime> getCurrentDateTime = null)
{
var currentDateTime = getCurrentDateTime == null ? DateTime.Now : getCurrentDateTime();
if (currentDateTime.Hour > 8 && currentDateTime.Hour < 18)
{
return Work();
}
else if (currentDateTime.Hour > 18 && currentDateTime.Hour < 22)
{
return Exercise();
}
else
{
return Sleep();
}
}
private string Work()
{
return "Work!";
}
private string Exercise()
{
return "Exercise!";
}
private string Sleep()
{
return "Sleep!";
}
}
於是乎,在测试的时候就可以透过 Lambda 语法注入虚设常式,测试码如下:
[Test]
public void WorkTest()
{
// Arrange
var decision = new Decision();
DateTimeWrapper.Set(new DateTime(2020, 01, 01, 08, 00, 00));
// Act
var whatToDo = decision.WhatToDo(() => new DateTime(2020, 01, 01, 10, 00, 00));
// Assert
Assert.Equal("Work!", whatToDo);
}
[Test]
public void ExerciseTest()
{
// Arrange
var decision = new Decision();
DateTimeWrapper.Set(new DateTime(2020, 01, 01, 18, 00, 00));
// Act + Assert
// ... 结构与 WorkTest 都一样
}
[Test]
public void SleepTest()
{
// Arrange
var decision = new Decision();
DateTimeWrapper.Set(new DateTime(2020, 01, 01, 23, 00, 00));
// Act + Assert
// ... 结构与 WorkTest 都一样
}
那最後一种写法是使用 static 的方式,相较其他四种方式其缺点较明显如测试不可同时进行,在最後需要做 Reset 的动作等。不过,因其撰写手法简单,相信许多历史较悠久的测试码可以看到其踪影,所以还是值得观看其手法,并理解後可改写其他方式,来看看其原始码:
public class DateTimeWrapper{
private static DateTime? dateTime;
public static DateTime Now { get { return dateTime ?? DateTime.Now; } }
public static void Set(DateTime setDateTime)
{
dateTime = setDateTime;
}
public static void Reset()
{
dateTime = null;
}
}
public class Decision
{
public string WhatToDo()
{
var currentDateTime = DateTimeWrapper.Now;
if (currentDateTime.Hour > 8 && currentDateTime.Hour < 18)
{
return Work();
}
else if (currentDateTime.Hour > 18 && currentDateTime.Hour < 22)
{
return Exercise();
}
else
{
return Sleep();
}
}
private string Work()
{
return "Work!";
}
private string Exercise()
{
return "Exercise!";
}
private string Sleep()
{
return "Sleep!";
}
}
可以看出,我们呼叫 Now 是检查 DateTimeWrapper 类别里面 static dateTime 属性有没有值,没有的话即呼叫现在的时间,而测试码如下:
[Test]
public void WorkTest()
{
// Arrange
var decision = new Decision();
DateTimeWrapper.Set(new DateTime(2020, 01, 01, 08, 00, 00));
// Act
var whatToDo = decision.WhatToDo();
// Assert
Assert.Equal("Work!", whatToDo);
}
[Test]
public void ExerciseTest()
{
// Arrange
var decision = new Decision();
DateTimeWrapper.Set(new DateTime(2020, 01, 01, 18, 00, 00));
// Act + Assert
// ... 结构与 WorkTest 都一样
}
[Test]
public void SleepTest()
{
// Arrange
var decision = new Decision();
DateTimeWrapper.Set(new DateTime(2020, 01, 01, 23, 00, 00));
// Act + Assert
// ... 结构与 WorkTest 都一样
}
要注意的是,使用 static 方法要在最後撰写 TearDown 的方法,如下:
[TearDown]
public void TearDown()
{
// 将 dateTime 重新设定为 null
DateTimeWrapper.Reset();
}
不然下个测试如果没有撰写好设定时间,会用上一个测试的时间去跑结果 。
其实这五个方法看下来,大多就是在探讨如何接缝的问题,不同的手法都可以达到注入假物件的概念;除了第五个 static 手法考虑在记忆体有限的状况下可以使用,若平常没有记忆体的考量,大多采用其他四种。其中,若要程序码简洁且易扩充,可使用第二种假物件框架的手法;而重构时,大多可采用第三种策略,继承完并覆写的手法去撰写 Legacy Code 的测试码。
Load Balancer (负载平衡器) 与 Auto Scaling 都算是云端设备中非常重要的...
今天我们要介绍的是python的集合,所谓的集合就是指将元素用{}包住并且是没有顺序也不会重复的资料...
基於风险的方法已广泛用於各个领域,例如决策,审计,网络安全,银行等。“风险是不确定性对目标的影响。”...
前言 大家好,我是辅大大三的学生,由於课程所需让我接触到铁人赛,因此我和同学一起组队参加,首先谢谢大...
前言 曾几何时,你有没有对元件中资料调用感到困扰呢? 同阶层的元件资料传递,需要使用 Event B...