單元測試的藝術 ==== - 什麼是單元測試? - '優秀'單元測試有哪些特質? - 是單元測試?還是整合測試? - 第一個單元測試 - 虛設常式 - Dependency Injection --- - 什麼是單元測試? >一個單元測試是一段程式呼叫另一個工作單元,並驗證工作單元的一個具體最終結果。如果對這個最終結果的假設是錯誤的,那單元測試就失敗了。一個單測試的範圍,可以小到一個方法,大到多個類別 --- 單元測試的特質 - 可被自動化,重複執行 - 任何人都可以按個按鈕就執行它 - 執行速度應該很快 - 多次執行結果一致 - 每個測試都該獨立 --- 想想看,如果無法滿足這些特性,我們寫的測試應該是哪一種? --- 整合測試 - 測試速度不夠快,結果較不穩定 - 測試單元中需要用到一個或多個真實相依物件 - 例如:依賴檔案系統、時間、資料庫等等 --- ![](https://i.imgur.com/MeMdfnf.jpg) --- 測試驅動開發(TDD) ![](https://i.imgur.com/yeMAur2.jpg) --- - 第一個單元測試 --- ```csharp= public class LogAnalyzer { public book IsValidLogFilename(string filename) { if fileName.EndWith(".SLF") { return false; } return true; } } ``` --- ```csharp= public class LogAnalyzerTests { public void IsValidFileName_BadExtension_RetrunFalse() { ... } } ``` --- - UnitOfWorkName: 被測試的方法、一組方法或一組類別 - Scenario: 假設條件,例如:「登入失敗」、「無效的使用者」 - ExpectedBehavior: 預期被測試的方法行為,例如:「回傳值」、系統狀態的改變 --- ```csharp= public void IsValidFileName_BadExtension_RetrunFalse() { LogAnalyzer analyzer = new LogAnalyzer(); bool result = analyzer.IsValidLogFilename("filewithbadextension.foo"); Assert.False(result); } ``` --- 增加正向測試 ```csharp= public void IsValidFileName_GoodExtension_RetrunFalse() { LogAnalyzer analyzer = new LogAnalyzer(); bool result = analyzer.IsValidLogFilename("filewithgoodextension.foo"); Assert.True(result); } ``` --- ![物件相依關係](https://i.imgur.com/zUKZqhd.jpg) --- - 虛設常式(stub) --- 虛設常式(stub) >是在系統中產生一個可控的替代物件,來取代一個外部相依物件。你可以再測試程式中,透過虛設常式來避免必須直接相依物件所造成的問題。 --- ![stub示意](https://i.imgur.com/pUTOsBo.jpg) --- ```csharp= public class FileExtensionManager IExtensionManager { public bool IsValid(string fileName) { ... } } public interface IExtensionManager { bool IsValid(string fileName); } public bool IsValidLogFileName(string fileName) { IExtensionManager mgr = new FileExtensionManager(); return mgr.IsValid(fileName); } ``` --- Dependency Injection - 方法的參數 - 工廠模式 - 偽造工廠類別 --- - 方法的參數 --- stub ```csharp= public class AlwaysValidFakeExtensionManager:IExtensionManager { public bool IsValid(string fileName) { return true; } } ``` --- ```csharp= public class LogAnalyzer { private IExtensionManager manager; public LogAnalyzer(IExtensionManager mgr) { manager = mgr } public bool IsValidLogFileName(string fileName) { return manager.IsValid(fileName); } } ``` --- ```csharp= public class LogAnalyzerTests { public void IsValidFileName_NameSupportExtension_RetrunTrue() { FakeExtensionManager myFakeMgr = new FakeExtensioManager(); myFakeMgr.WillBeValid = true; LogAnaylzer log = new LogAnaylzer(myFakeMgr); bool result = log.IsValidLogFileName("short.ext"); Assert.True(result); } } internal class FakeExtensionManager: IExtensionManager { public bool WillBeValid = false; public bool IsValid(string filename) { return WillBeValid; } } ``` --- - 工廠類別 --- ```csharp= public class LogAnalyzer { private IExtensionManager manager; public LogAnalyzer() { manager = ExtensionManagerFactory.Create(); } public bool IsValidLogFileName(string fileName) { return manager.IsValid(fileName); } } ``` --- ```csharp= public void IsValidFileName_SupportExtension_RetrunTrue() { // setup the stub to use, make sure it return true ... ExtensionManagerFactory.SetManager(myFakeManager); myFakeMgr.WillBeValid = true; LogAnaylzer log = new LogAnaylzer(); ... // Assert logic... } ``` --- ```csharp= class ExtensionManagerFactory { private IExtensionManagr customManager=null; public IExtensionManagr Create() { If (customManager!=null) return customManager; return new FileExtensionManager(); } public void SetManager(IExtensionManager mgr) { customManager = mgr; } } ``` --- - 偽造工廠類別 --- ```csharp= public class LogAnalyzerUsingFactoryMethod { public bool IsValidLogFileName(string filename) { return GetManager.IsValid(filename); } protected virtual IExtensionManager GetManager() { return new FileExtensionManager(); } } ``` --- ```csharp= class TestableLogAnalyzer: LogAnaylzerUsingFactoryMethod { public TestableLogAnalyzer(IExtensionManager mgr) { Manger = mgr; } public IExtensionManager Manager; protected override IExtensionManager GetManager() { return Manager; } } internal class FakeExtensionManager: IExtensionManager { // no changes from the previous samples // ... } ``` --- ```csharp= public class LogAnalyzerTests { public void overrideTests() { FakeExtensionManager stub = new FakeExtensioManager(); stub.WillBeValid = true; TestableLogAnalyzer logan = new TestableLogAnalyzer(stub); bool result = logan.IsValidLogFileName("file.ext"); Assert.True(result); } } ``` --- 注意: 這會讓測試程式變得更不容易理解,建議最好還是勁量避免。 --- End😎 謝謝 !! ---
{"metaMigratedAt":"2023-06-14T22:46:18.365Z","metaMigratedFrom":"Content","title":"單元測試的藝術","breaks":true,"contributors":"[{\"id\":\"ec31673e-c7c8-4b72-8b89-eeb85bc7db71\",\"add\":6461,\"del\":1189}]"}
    600 views