--- tags: Pluralsight Note, Writing Testable Code --- ## 前言 ![UnitTesting](https://github.com/s0920832252/C_Sharp/blob/master/Files/Writing_Testable_Code/UnitTesting.png?raw=true) ### Why We Should Test Code ? - Reduce Bugs - Reduce Costs - Improve Design - 當你發現測試很難寫時, 需要開始擔心架構設計上是否有問題. - Documentation - 好的測試, 應該就像是文件 - Eliminate Fear - 有了測試, 才敢放心重構 ### How Do We Write Testable Code ? - Create seams(縫隙) in code - Simplify construction - Work with dependencies - Decouple from global state - Maintain single responsibility - Use Test-driven Development ## Create seams in code ![BeforeSeam](https://github.com/s0920832252/C_Sharp/blob/master/Files/Writing_Testable_Code/BeforeSeam.png?raw=true) ![AfterSeam](https://github.com/s0920832252/C_Sharp/blob/master/Files/Writing_Testable_Code/AfterSeam.png?raw=true) #### seam 是指程式碼中可以抽換不同功能的地方 * 使用虛設常式類別 * 虛設常式(stub)是指系統中產生一個可控的替代物件, 來取代相依物件, 以方便測試。 * 增加一個建構子 (透過建構子參數傳入可控物件) * 增加一個可設定的公開屬性 * 把一個方法改成可 override 的虛擬方法 * 把一個委派拉出來變成一個參數或屬性供類別外部來決定內容 (個人不推薦) **增加 seam 可能會破壞類別的封裝, 需謹慎之** ### Problem ##### 當程式碼中沒有 Seam 的存在時, 會有下列問題 - Can not pull apart code - 沒有縫隙, 意即高耦合 ? - Can not connect a test harness to the class we want to test - 無法測試類別行為, 因為你可能沒辦法控制它的行為. - Can not replace dependencies - 無法透過替換依賴物件去控制類別行為 - Can not test in isolation - 當所有東西都耦合再一起, 你就無法單獨測試某一段邏輯. ### Symptoms ##### 當你遇到下列情境時需先思考一下, 這個地方是否真的不需要 Seam - Keyword "new" in code - new is glue !!! new 出來的物件, 是沒辦法被外界(UT) 替換的. - Static method call - 靜態成員是不能被 override 的. 但你若需要控制此方法成員的回傳值, 則會有問題 ```csharp // DoSomething() 無法測試. 因為你無法控制 DataTime.Now 的輸出結果. public class MyClass{ public void DoSomething() { var time = DataTime.Now; // <-- DataTime.Now 是靜態成員 if(time is 星期一) { 猴子去跳舞 } if(time is 星期二) { 猴子去跑步 } } } // 可考慮將抽一個介面 public interface IDateTimeWrapper{ DataTime GetNow(); } public class DateTimeWrapper : IDateTimeWrapper{ public DataTime GetNow()=> DataTime.Now; } ``` - 當靜態方法的使用是不需要被替換, 且通常此靜態方法已經被別的 UT 測試過時, 請放心使用. e.g. Math.Pow() 等等 - Direct coupling - 你必須判斷對於某個外部資源或是套件的直接依賴是否會影響你的開發. - e.g. Log/File/Database 的操作使用. ```csharp // 建立一個介面叫做 ILogHelper , 並且讓程式全部相依於 ILogHelper // 未來不論你想使用哪一個 Log 套件, 都不會影響現在的程式. // 只需要新增一個類別(繼承ILogHelper) , 再修改 IOC 套件的註冊即可. interface ILogHelper{ void Log(); } calss NlogerHelper : ILogHelper{ void Log() { // 使用 Nlog 套件的方法去 log } } ``` ### Solution - Create seams in code - Decouple Dependencies - Program to interfae (we should follow DIP) - Inject Dependencies * 依賴注入的幾種方式 * 擷取與覆寫 (Extract and Override) * 工廠模式 * 屬性注入 * 方法注入 * 建構子注入 (常用) - Test in isolation ### 值得思考的範例(?) ![](https://hackmd.io/_uploads/H1zRrpLtn.png) 實務上我覺得還是盡可能不要使用 Setup() , 也盡可能不要使用 field 去儲存 mock 物件會比較好. 因為隨著 Test 的增加, Setup() 可能變得越來越複雜 == ```csharp void Setup(){ if(A 情況) _fieldMockA = Mock.Of<Class>(o => o.bool == true) if(B 情況) _fieldMockA = Mock.Of<Class>(o => o.bool == false) 成長ing ... } void TestA(){ // 期望拿到 true, 但因為某些未知原因導致 B 情況發生, 所以拿到 false // 此測試噴錯 var a = _fieldMockA; } ``` ### Moq.AutoMock An automocking container for Moq. Use this if you're invested in your IoC container and want to decouple your unit tests from changes to their constructor arguments. #### 這個套件能幫你模擬 DI 注入的情境. 太神啦~~~傑克 ##### 參考資源 [Moq.AutoMock](https://www.nuget.org/packages/Moq.AutoMock) [Moq.AutoMock Git hub](https://github.com/moq/Moq.AutoMocker) [自动Mock,让编写单元测试更简单](https://blog.csdn.net/sD7O95O/article/details/120051616) [(如何模擬使用 IIndex) How to unit test with Keyed Registrations?](https://stackoverflow.com/questions/47798273/automock-how-to-unit-test-with-keyed-registrations) ## Constructing Testable Objects #### Constructors - Used to build objects - Prepares object for use 通常我們使用建構子去設定物件的初始狀態. 但當你在你的建構子內放太多的**設定邏輯**時, 這可能會導致一些問題. ### Problems - Creates tight coupling - 你可在建構子內建立任何物件, 但這導致強相依. ```csharp // City 強相依於可樂. 沒有可樂, 沒有 City. public class City好帥 { private 可樂 _可口可樂; public City好帥(){ _可口可樂 = new 冰冰的可口可樂(); // "new is glue !" } } ``` - 盡可能遵守 DIP 原則, 並使用 DI 技術實現 - Logic is difficult to test - 建構子內的設定邏輯通常是很難驗證的. 因為這些"設定邏輯" 很常是一些設定 private 變數的邏輯. ```csharp // 外界的 UT 沒辦法取得 _可口可樂的狀態, 意即沒辦法驗證 City好帥是否建立正確 QAQ public class City好帥 { private 可樂 _可口可樂; public City好帥(){ _可口可樂 = new 冰冰的可口可樂(); // "new is glue !" } } ``` - Logic is difficult to set up - 想在 UT 設定物件的初始狀態是困難的 ```csharp // 外界的 UT 沒辦法設定 _可口可樂的狀態, 意即沒辦法透過 _可口可樂去影響 City 的行為. // 以這個例子, 你亙本沒辦法控制 city 喝完可樂後會不會拉肚子, // 自然沒辦法驗證 City 是否有正確的拉肚子. public class City好帥 { private 可樂 _可口可樂; public City好帥(){ if( 某個情況 A ) _可口可樂 = new 冰冰的可樂(); // "new is glue !" if( 某個情況 B ) _可口可樂 = new 壞掉的可樂(); // "new is glue !" } public void 喝可樂() => _可口可樂 switch { 冰冰的可樂 => 感謝可樂感謝老天爺(); 壞掉的可樂 => 拉肚子(); _ => throw InvalidOperationException(); } } ``` ### Symptoms - Keyword "new" - 在建構子內透過 new 物件去初始化的場合, 都需要謹慎考慮這是否利大於弊 - 簡單型別, 字串或是 IEnumerable 型別的 new , 是完全可以接受的. - 防禦性 coding 那堂課有講到, 如果你有一個 IEnumerable 變數, 請記得初始化它. 不然外界不會知道你的 IEnumerable 變數尚未被 new. 使用前需要自己 new == ```csharp // 別這樣寫, 這樣寫, 亙本是在挖地洞給別人跳. public class City好帥{ public List<可樂> City的大秘寶 { get ; set;} // 建議初始化 // public List<可樂> City的大秘寶 { get } = new List<可樂>(); } ``` - Logic in constructor - 理論上建構子就是負責物件的初始化, 應該就只要簡單的初始化或是設定物件所需要的東西就好. 若是有過多的邏輯判斷( if/switch/for/while ... ) , 可能導致你之後很難測試. - Any non-assignment code - 理論上建構子就是負責物件的初始化, 應該就只要簡單的初始化或是設定物件所需要的東西就好. ### Solution - Inject depenedencies - Avoid logic in constructor - Use factory, builder or IOC/DI - Don't mix construction and logic - **Serarate Injectables vs. Newables** > - An injectable is an object that is composed of other injectables and performs work on newable objects. Injectables are generally services that implement interfaces. For example, our Database, Printer and InvoiceWriter classes are all examples of injectables. > - An newable is an object at the end of your object graph. these are generally things like entities and value objects. For example, invoice, customer, address and credit card numbers are all newables. > - An injectable class can ask for other injectables in its constructor; however it should not ask for any newables in its constructor. Inversely, a newable can ask for other newables in its constructor; however, it should not ask for an injectable in its constructor. ## Working with Dependencies #### Law of Demeter > - Only talk to your immediate friends > - Don't talk to strangers ### Problems of violating the Law of Demeter - Tight coupling - 耦合它人的機率增加了 ```csharp= public class 筆記本{ public void 寫筆記(string content); } public class 學生 { public 筆記本 筆記本 {get;} = new 筆記本(); } // 使用方式為 : 學生.筆記本.寫筆記("筆記內容"); // 外界使用者耦合於它的 immediate friends(學生), 同時耦合 strangers (筆記本) // 故違反 Law of Demeter // 如果要修改成不違反 Law of Demeter , 可能可以這樣改 // 使用方式為 : 學生.寫筆記("筆記內容") public class 學生 { private 筆記本 _筆記本 = new 筆記本(); public void 寫筆記(string content) => _筆記本.寫筆記(content); } ``` - Difficult to set up - 當你的耦合增加, 這代表你要 Mock 的東西也變多了 XD - Dependencies not explicit - 原本你可以從建構子參數快速知道你要 mock 甚麼東西, 但因為違反了 Law of Demeter, 這代表會有 strangers 存在. 故你必須要進去看 code 才知道有哪些 strangers 是你需要 mock 的 ### Symptoms - Series of appended methods (不是 Method Chain 不好, 而是當你看到時, 需要小心) ```csharp! public class MyClass{ private TheObj _theObj; public void MyMethod() => _theObj.GetUser().GetUserInfo().GetAddress(); } ``` - it is okay for method chain when using the builder pattern with fluent notation - Container dependency - 容器(immediate friends) & 容器的回傳值(strangers) ![](https://hackmd.io/_uploads/rJRq4Fc9n.png) - 範例 ```csharp= public class MyContainer{ public TheObj TheObj; public TheObj2 TheObj2; public TheObj3 TheObj3; public TheObj4 TheObj4; ... } // 你需要的是 TheObj & TheObj2 , 所以它們對你來說不應該是 strangers. public class YourClass{ private MyContainer _conatiner; public YourClass(MyContainer container) => _conatiner = conatiner; public XXX ReturnSomeThing() => _conatiner.TheObj.XXX屬性; public YYY ReturnSomeThing2() => _conatiner.TheObj2.YYY屬性; } ``` - Suspicious names (Container dependency 常有一些名字) - container, contextes, environnent, or service Locator… ### Solution 1. Follow the Law of Demeter - 試圖封裝那些 strangers. 讓 user 只需要認識 immediate friends. 2. Inject dependencies we need - 你需要啥, 就注入啥就好. 貪多嚼不爛 XD 4. Use Dependencies directly - 直接使用, 不要在那邊 點點點(...) XD 5. Inject only what is needed - 你注入的容器最好只提供你所需要的資訊就好. e.g. 你可能注入一個有一百個屬性的容器類, 但你只需要其中的五個屬性, 這就很不好 == 6. Make dependencies explicit - 透過建構子注入的參數/公開屬性/私有field, 可以讓我們清楚的知道這個類**僅依賴於哪些相依**. ## Managing Application State ### Global State - Set of variables - 其實就是一群變數, 這些變數控制著系統的狀態 - 實現機制 - Global variables - Application-State object (Asp.Net 專用) - > Application-State object 是一種在 C# 中用於在整個應用程式範圍內儲存和共享資料的機制. 它允許資料在不同的網頁或網頁請求之間保持持久狀態, 並可由所有使用者共享. 在 ASP.NET 中, 可以使用 Application-State object 在整個應用程式的生命週期內儲存和存取資料. 這些資料將對所有連線到應用程式的使用者可見, 因此要小心使用, 避免過度使用或存放敏感資料。 - [參考資訊- Asp.Net重要物件HttpApplication(一) 初始化建立IHttpMoudule](https://ithelp.ithome.com.tw/articles/10215676) - 範例 code ```csharp public class Global : HttpApplication { void Application_Start(object sender, EventArgs e) { // 在應用程式啟動時初始化 Application-State Application["TotalUsers"] = 0; } void Session_Start(object sender, EventArgs e) { // 新的使用者連線時,增加總使用者數並儲存到 Application-State Application.Lock(); // 鎖定 Application-State,確保同步存取 Application["TotalUsers"] = (int)Application["TotalUsers"] + 1; Application.UnLock(); // 解鎖 Application-State } void Session_End(object sender, EventArgs e) { // 使用者結束連線時,減少總使用者數並儲存到 Application-State Application.Lock(); Application["TotalUsers"] = (int)Application["TotalUsers"] - 1; Application.UnLock(); } } ``` - Only a single instance - 不論你如何實作 Global State , 這變數同時間應該只有一個 instance 存在記憶體. ```csharp= // 舉例 public sealed class MyGlobalState { private MyGlobalState(){} // 私有建構子, 防止外部使用 new , 產生物件 public static MyGlobalState StateObj { get; } = new MyGlobalState(); public bool IsOk {get;set;} public int SuccessCount {get;set;} // 諸如此類的屬性... } ``` [參考資訊-Singleton Pattern](https://hackmd.io/@CityChen/Sk5xRFLk9) #### WPF 沒有 Asp.Net 的 Application-State 可以使用. 但有別種方式可以實現資料交換. ##### 可能的 WPF 資料交換的實作方式 - MVVM - 舉例 : 三個 View 使用同一個大VM , 三個 View 的資訊溝通會透過大VM 傳遞 (不推) - Static Class (全域變數存起來) - Dependency Injection - 資料放在 Service , 再注入 Service - Singleton Pattern ### Problem * Coupling to global state ```csharp= public sealed class MyGlobalState{ public static string UserName {get;set;} } // UserA 和 UserB 都耦合於 MyGlobalState public class UserA{ public void ActionA() => MyGlobalState.UserName = "A"; } public class UserB{ public void ActionB() => MyGlobalState.UserName = "B"; } ``` * Difficult to set up tests * 再執行 UT 之前, 需要先設定這些 Global State * Prevents parallel tests * UT1 可能需要設定 State Varable 為 A , 但 UT2 可能需要設定為 B. 此時若此兩個 UT 為平行執行測試, 會出現問題. e.g. UT1 設定完 State Varable, 然後 UT2 跑完全部的測試. 此時 UT2 必定 failed. * Spooky action at a distance (若有看到這現象, 這必定是一個 anti-pattern) * Changes to a class in one part of the application should generally not affect classes in other parts of the application in unpredictable way * 這代表你每一個耦合 Global State 的類別的測試, 都必須要設定 Global State, 因為這些 UT 不再具有隔離性(彼此之間獨立互不影響). UT1 對於 Global State 的設定可能會影響到 UT2 * 舉例 : Driver 若改變 PassengerDoorLock 的變數, Passenger 會再不知道變數被改變的情境下繼續使用 PassengerDoorLock 的變數. ```mermaid graph TB; DriverClass--使用存取-->PassengerDoorLockClass PassengerClass--使用存取-->PassengerDoorLockClass ``` ### Symptoms - Global variables - Static methods and fields - GoF - Singleton Pattern - Unit Test randomly fail ### Solution - Avoid coupling to global state - Keep state local if possible - 除非必要, 不然盡可能讓變數存在 Local 處.(instance) , 少用 static - 原則上我們應該讓變數的生命週期越短越好. 像是 Clean Code 那堂課講過的浮游變數 ! - Inject a wrapper class - 建立 Facade 去封裝外界不必要知道的資訊. e.g. 靜態方法的使用 ```C# public interface ILogHelper{ public Log(); } // 以下兩個類別都可以透過注入取得, 取得哪一個全取決於對於 Ioc 容器的註冊是哪一個. public class NlogHelper : ILogHelper{ public Log() => NLog.LogManager.GetCurrentClassLogger().Info("訊息"); } public class SerilogHelper : ILogHelper{ public Log() => Log.Information("訊息"); } ``` - Use IoC/DI Singleton instead of original implementation of Singleton Pattern. ## Maintaining Single Responsibility ### Single Responsibility Principle - Only one reason to change - Cohesion and coupling - 希望增加類別之間的內聚力 --- 由於相同原因而變化的事物之間的凝聚力 - 希望減少不同類別間的耦合程度 --- 因不同原因而變化的事物之間的耦合 - Do one thing well ### Problem of violating SRP - Many Tests per class - 一個類別越複雜, 自然需要為其寫更多的 Unit Tests e.g. 想像一個 class 的 unit test 有一百多個, 這類別的功能該有多複雜 ? - Complex setup - 一個複雜的類, 妳為了控制它能執行某個行為, 你可能需要付出比較多的前置動作. - Frequently changing tests - 一個類別的職責越多意味著你改變它的可能性越高, 也就代表你修改 UT 的頻率越高 ### Symptoms - Described with "and" / "or" - Class or Method is large - 大就代表複雜, 就可能會有上述提到的三個問題 - 一個複雜的方法或是類別可以考慮將其重構成數個方法或類別 (大拆小) - Many Injected dependencies - 一般來說, 一個類的依賴關西越多, 可能就代表這個類的職責越大 - class changes frequently ### Solution 1. Identify responsibilities - 辨認總共有哪些職責或是任務. 2. Label responsibilities - 當你發現隱藏在一個類裡面的職責時, 可以試圖去為它們取一個可以簡潔描述其職責的名稱. 3. Decompose into SRP classes - 重構, 把大類別或方法拆成數個小類別或方法 --- ###### Thank you! You can find me on - [GitHub](https://github.com/s0920832252) - [Facebook](https://www.facebook.com/fourtune.chen) 若有謬誤 , 煩請告知 , 新手發帖請多包涵 # :100: :muscle: :tada: :sheep: <iframe src="https://skilltree.my/c67b0d8a-9b69-47ce-a50d-c3fc60090493/promotion?w=250" width="250" style="border:none"></iframe>