基於 .NET 的 xUnit.net 測試框架,開發一款自動貓門的邏輯,讓門在白天開放,夜間鎖定。
在本系列的中,我演示了如何使用設計的故障來確保程式碼中的預期結果。在第二篇文章中,我將繼續開發範例專案:一款自動貓門,該門在白天開放,夜間鎖定。
在此提醒一下,你可以按照使用 .NET 的 xUnit.net 測試框架。
回想一下,測試驅動開發(TDD)圍繞著大量的單元測試。
第一篇文章中實現了滿足 Given7pmReturnNighttime
單元測試期望的邏輯。但還沒有完,現在,你需要描述當前時間大於 7 點時期望發生的結果。這是新的單元測試,稱為 Given7amReturnDaylight
:
[Fact] public void Given7amReturnDaylight() { var expected = "Daylight"; var actual = dayOrNightUtility.GetDayOrNight(); Assert.Equal(expected, actual); }
現在,新的單元測試失敗了(越早失敗越好!):
Starting test execution, please wait...[Xunit.net 00:00:01.23] unittest.UnitTest1.Given7amReturnDaylight [FAIL]Failed unittest.UnitTest1.Given7amReturnDaylight[...]
期望接收到字串值是 Daylight
,但實際接收到的值是 Nighttime
。
經過仔細檢查,程式碼本身似乎已經出現問題。 事實證明,GetDayOrNight
方法的實現是不可測試的!
看看我們面臨的核心挑戰:
GetDayOrNight
依賴隱藏輸入。
dayOrNight
的值取決於隱藏輸入(它從內建系統時鐘中獲取一天的時間值)。
GetDayOrNight
包含非確定性行為。
從系統時鐘中獲取到的時間值是不確定的。(因為)該時間取決於你執行程式碼的時間點,而這一點我們認為這是不可預測的。
GetDayOrNight
API 的品質差。
該 API 與具體的資料來源(系統 DateTime
)緊密耦合。
GetDayOrNight
違反了單一責任原則。
該方法實現同時使用和處理資訊。優良作法是一種方法應負責執行一項職責。
GetDayOrNight
有多個更改原因。
可以想象內部時間源可能會更改的情況。同樣,很容易想象處理邏輯也將改變。這些變化的不同原因必須相互隔離。
當(我們)嘗試了解 GetDayOrNight
行為時,會發現它的 API 簽名不足。
最理想的做法就是通過簡單的檢視 API 的簽名,就能了解 API 預期的行為型別。
GetDayOrNight
取決於全域性共用可變狀態。
要不惜一切代價避免共用的可變狀態!
即使在閱讀原始碼之後,也無法預測 GetDayOrNight
方法的行為。
這是一個嚴重的問題。通過閱讀原始碼,應該始終非常清晰,系統一旦開始執行,便可以預測出其行為。
每當你遇到工程問題時,建議使用久經考驗的分而治之策略。在這種情況下,遵循關注點分離的原則是一種可行的方法。
關注點分離(SoC)是一種用於將計算機程式分為不同模組的設計原理,以便每個模組都可以解決一個關注點。關注點是影響計算機程式程式碼的一組資訊。關注點可以和要優化程式碼的硬體的細節一樣概括,也可以和要範例化的類的名稱一樣具體。完美體現 SoC 的程式稱為模組化程式。
(出處)
GetDayOrNight
方法應僅與確定日期和時間值表示白天還是夜晚有關。它不應該與尋找該值的來源有關。該問題應留給呼叫用戶端。
必須將這個問題留給呼叫用戶端,以獲取當前時間。這種方法符合另一個有價值的工程原理——控制反轉。Martin Fowler 在這裡詳細探討了這一概念。
框架的一個重要特徵是使用者定義的用於客製化框架的方法通常來自於框架本身,而不是從使用者的應用程式程式碼呼叫來的。該框架通常在協調和排序應用程式活動中扮演主程式的角色。控制權的這種反轉使框架有能力充當可延伸的框架。使用者提供的方法為框架中的特定應用程式量身制定泛化演算法。
因此,程式碼需要重構。擺脫對內部時鐘的依賴(DateTime
系統實用程式):
DateTime time = new DateTime();
刪除上述程式碼(在你的檔案中應該是第 7 行)。通過將輸入引數 DateTime
時間新增到 GetDayOrNight
方法,進一步重構程式碼。
這是重構的類 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; } }}
重構程式碼需要更改單元測試。 需要準備 nightHour
和 dayHour
的測試資料,並將這些值傳到GetDayOrNight
方法中。 以下是重構的單元測試:
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); } }}
在繼續開發這種簡單的場景之前,請先回顧複習一下本次練習中所學到的東西。
執行無法測試的程式碼,很容易在不經意間製造陷阱。從表面上看,這樣的程式碼似乎可以正常工作。但是,遵循測試驅動開發(TDD)的實踐(首先描述期望結果,然後才描述實現),暴露了程式碼中的嚴重問題。
這表明 TDD 是確保程式碼不會太凌亂的理想方法。TDD 指出了一些問題區域,例如缺乏單一責任和存在隱藏輸入。此外,TDD 有助於刪除不確定性程式碼,並用行為明確的完全可測試程式碼替換它。
最後,TDD 幫助交付易於閱讀、邏輯易於遵循的程式碼。
在本系列的下一篇文章中,我將演示如何使用在本練習中建立的邏輯來實現功能程式碼,以及如何進行進一步的測試使其變得更好。