變異測試:基于故障的試驗
基于 .NET 的 xUnit.net 測試框架,開發(fā)一款自動貓門的邏輯,讓門在白天開放,夜間鎖定。
在本系列的第一篇文章中,我演示了如何使用設(shè)計的故障來確保代碼中的預(yù)期結(jié)果。在第二篇文章中,我將繼續(xù)開發(fā)示例項目:一款自動貓門,該門在白天開放,夜間鎖定。
在此提醒一下,你可以按照此處的說明使用 .NET 的 xUnit.net 測試框架。
關(guān)于白天時間
回想一下,測試驅(qū)動開發(fā)(TDD)圍繞著大量的單元測試。
第一篇文章中實現(xiàn)了滿足 Given7pmReturnNighttime
單元測試期望的邏輯。但還沒有完,現(xiàn)在,你需要描述當(dāng)前時間大于 7 點時期望發(fā)生的結(jié)果。這是新的單元測試,稱為 Given7amReturnDaylight
:
[Fact]
public void Given7amReturnDaylight()
{
var expected = "Daylight";
var actual = dayOrNightUtility.GetDayOrNight();
Assert.Equal(expected, actual);
}
現(xiàn)在,新的單元測試失敗了(越早失敗越好!):
Starting test execution, please wait...
[Xunit.net 00:00:01.23] unittest.UnitTest1.Given7amReturnDaylight [FAIL]
Failed unittest.UnitTest1.Given7amReturnDaylight
[...]
期望接收到字符串值是 Daylight
,但實際接收到的值是 Nighttime
。
分析失敗的測試用例
經(jīng)過仔細檢查,代碼本身似乎已經(jīng)出現(xiàn)問題。 事實證明,GetDayOrNight
方法的實現(xiàn)是不可測試的!
看看我們面臨的核心挑戰(zhàn):
-
GetDayOrNight
依賴隱藏輸入。dayOrNight
的值取決于隱藏輸入(它從內(nèi)置系統(tǒng)時鐘中獲取一天的時間值)。 -
GetDayOrNight
包含非確定性行為。從系統(tǒng)時鐘中獲取到的時間值是不確定的。(因為)該時間取決于你運行代碼的時間點,而這一點我們認為這是不可預(yù)測的。
-
GetDayOrNight
API 的質(zhì)量差。該 API 與具體的數(shù)據(jù)源(系統(tǒng)
DateTime
)緊密耦合。 -
GetDayOrNight
違反了單一責(zé)任原則。該方法實現(xiàn)同時使用和處理信息。優(yōu)良作法是一種方法應(yīng)負責(zé)執(zhí)行一項職責(zé)。
-
GetDayOrNight
有多個更改原因。可以想象內(nèi)部時間源可能會更改的情況。同樣,很容易想象處理邏輯也將改變。這些變化的不同原因必須相互隔離。
-
當(dāng)(我們)嘗試了解
GetDayOrNight
行為時,會發(fā)現(xiàn)它的 API 簽名不足。最理想的做法就是通過簡單的查看 API 的簽名,就能了解 API 預(yù)期的行為類型。
-
GetDayOrNight
取決于全局共享可變狀態(tài)。要不惜一切代價避免共享的可變狀態(tài)!
-
即使在閱讀源代碼之后,也無法預(yù)測
GetDayOrNight
方法的行為。這是一個嚴重的問題。通過閱讀源代碼,應(yīng)該始終非常清晰,系統(tǒng)一旦開始運行,便可以預(yù)測出其行為。
失敗背后的原則
每當(dāng)你遇到工程問題時,建議使用久經(jīng)考驗的分而治之策略。在這種情況下,遵循關(guān)注點分離的原則是一種可行的方法。
關(guān)注點分離(SoC)是一種用于將計算機程序分為不同模塊的設(shè)計原理,以便每個模塊都可以解決一個關(guān)注點。關(guān)注點是影響計算機程序代碼的一組信息。關(guān)注點可以和要優(yōu)化代碼的硬件的細節(jié)一樣概括,也可以和要實例化的類的名稱一樣具體。完美體現(xiàn) SoC 的程序稱為模塊化程序。
(出處)
GetDayOrNight
方法應(yīng)僅與確定日期和時間值表示白天還是夜晚有關(guān)。它不應(yīng)該與尋找該值的來源有關(guān)。該問題應(yīng)留給調(diào)用客戶端。
必須將這個問題留給調(diào)用客戶端,以獲取當(dāng)前時間。這種方法符合另一個有價值的工程原理——控制反轉(zhuǎn)。Martin Fowler 在這里詳細探討了這一概念。
框架的一個重要特征是用戶定義的用于定制框架的方法通常來自于框架本身,而不是從用戶的應(yīng)用程序代碼調(diào)用來的。該框架通常在協(xié)調(diào)和排序應(yīng)用程序活動中扮演主程序的角色。控制權(quán)的這種反轉(zhuǎn)使框架有能力充當(dāng)可擴展的框架。用戶提供的方法為框架中的特定應(yīng)用程序量身制定泛化算法。
重構(gòu)測試用例
因此,代碼需要重構(gòu)。擺脫對內(nèi)部時鐘的依賴(DateTime
系統(tǒng)實用程序):
DateTime time = new DateTime();
刪除上述代碼(在你的文件中應(yīng)該是第 7 行)。通過將輸入?yún)?shù) DateTime
時間添加到 GetDayOrNight
方法,進一步重構(gòu)代碼。
這是重構(gòu)的類 DayOrNightUtility.cs
:
using System;
namespace app {
public class DayOrNightUtility {
public string GetDayOrNight(DateTime time) {
string dayOrNight = "Nighttime";
if(time.Hour >= 7 && time.Hour < 19) {
dayOrNight = "Daylight";
}
return dayOrNight;
}
}
}
重構(gòu)代碼需要更改單元測試。 需要準(zhǔn)備 nightHour
和 dayHour
的測試數(shù)據(jù),并將這些值傳到GetDayOrNight
方法中。 以下是重構(gòu)的單元測試:
using System;
using Xunit;
using app;
namespace unittest
{
public class UnitTest1
{
DayOrNightUtility dayOrNightUtility = new DayOrNightUtility();
DateTime nightHour = new DateTime(2019, 08, 03, 19, 00, 00);
DateTime dayHour = new DateTime(2019, 08, 03, 07, 00, 00);
[Fact]
public void Given7pmReturnNighttime()
{
var expected = "Nighttime";
var actual = dayOrNightUtility.GetDayOrNight(nightHour);
Assert.Equal(expected, actual);
}
[Fact]
public void Given7amReturnDaylight()
{
var expected = "Daylight";
var actual = dayOrNightUtility.GetDayOrNight(dayHour);
Assert.Equal(expected, actual);
}
}
}
經(jīng)驗教訓(xùn)
在繼續(xù)開發(fā)這種簡單的場景之前,請先回顧復(fù)習(xí)一下本次練習(xí)中所學(xué)到的東西。
運行無法測試的代碼,很容易在不經(jīng)意間制造陷阱。從表面上看,這樣的代碼似乎可以正常工作。但是,遵循測試驅(qū)動開發(fā)(TDD)的實踐(首先描述期望結(jié)果,然后才描述實現(xiàn)),暴露了代碼中的嚴重問題。
這表明 TDD 是確保代碼不會太凌亂的理想方法。TDD 指出了一些問題區(qū)域,例如缺乏單一責(zé)任和存在隱藏輸入。此外,TDD 有助于刪除不確定性代碼,并用行為明確的完全可測試代碼替換它。
最后,TDD 幫助交付易于閱讀、邏輯易于遵循的代碼。