# 如何以 NSubstitute 處理 Expression & IQueryable ###### tags: `UnitTest` ## 關於 NSubstitute 本篇將討論以下幾個問題 > ### 1. 什麼是 NSubstitute? > ### 2. 如何處理 Expression & IQueryable? > ### 3. NSubstitute 學習資源 & 參考資料 --- ## 測試環境: >OS:Windows 10 >IDE:Visual Studio 2019 >UnitTest:xUnit --- ## 1. 什麼是 NSubstitute? 舉例來說,我們寫一個稍微複雜的方法 A,通常不可避免的會需要呼叫其他外部方法,此時若要對這個方法 A 撰寫單元測試時,就需要寫個假的實體(Mock、stub)來取代被呼叫的外部方法,以隔離外部方法,避免因外部方法而造成測試執行失敗。 但呼叫的外部方法一多,我們寫假的實體(Mock、stub)就會寫到天荒地老,好不快樂,此時我們就可以使用 NSubstitute 來幫我們建立假的實體(Mock、stub),將無數行程式碼濃縮成短短幾行! --- ## 2. 如何處理 Expression & IQueryable? ### 在開始寫單元測試前,我們先建立一個情境 #### 1. 情境 資料庫有個 User 資料表,裡面存著 User Id 跟 User Name,今天接到兩個需求 - 取得所有 User 資料 - 依據 UserId 取得一筆 User 資料 要實作功能,我們預期會有 - UserModel:對應資料庫中的資料 - GetDataService:取得資料庫中的 User 資料並傳回 - IRepository:GetDataService 中呼叫的外部方法取得 DB 中的資料 #### 2. 建立 UserModel & GetDataService & 外部方法 IRepository ```C# // UserModel public class User { public int Id { get; set; } public string Name { get; set; } } ``` ```C# // GetDataService public class GetDataService { private IRepository<User> _UserRepo { get; } public GetData (IRepository<User> userRepo) { _UserRepo = userRepo; } public IEnumerable<User> GetUsers() { var data = _UserRepo.Query(); return data.AsEnumerable(); } public User GetUserById(int id) { var data = _UserRepo.Query(x => x.Id == id); return data.FirstOrDefault(); } } ``` ```C# // IRepository public interface IRepository<T> { IQueryable<T> Query(Expression<Func<T, bool>> filter = null); } ``` #### 3. 單元測試 ```C# [Fact] public void GetUsers_取得兩筆UserData_回傳兩筆UserData() { try { //Arrange var userRepo = Substitute.For<IRepository<User>>(); var userData = new List<User> { new User { Id = 1, Name = "User01" }, new User { Id = 2, Name = "User02" } }; // 不包含搜尋條件,filter = null,直接將 userData 轉 IQueryable 回傳 userRepo.Query(Arg.Any<Expression<Func<User, bool>>>()) .Returns(userData.AsQueryable()); var getData = new GetDataService(userRepo); //act var result = getData.GetUsers(); //assert Assert.Equal(result.Any(x => x.Id == 1 && x.Name == "User01"), true); Assert.Equal(result.Any(x => x.Id == 2 && x.Name == "User02"), true); } catch (Exception e) { Console.WriteLine(e); throw; } } [Fact] public void GetUser_輸入Id_回傳UserData() { //Arrange var userRepo = Substitute.For<IRepository<User>>(); var userData = new List<User> { new User { Id = 1, Name = "User01" }, new User { Id = 2, Name = "User02" } }; // 包含搜尋條件,將 userData.Where 使用 filter 條件搜尋後回傳 userRepo.Query(Arg.Any<Expression<Func<User, bool>>>()) .Returns(arg => userData.Where(arg.ArgAt<Expression<Func<User, bool>>>(0).Compile()).AsQueryable()); var getData = new GetDataService(userRepo); //act var result = getData.GetUsersById(1); //assert Assert.Equal(result.Id == 1 && result.Name == "User01", true); } ``` #### 4. 說明 ```C# // 不包含搜尋條件,filter = null,直接將 userData 轉 IQueryable 回傳 userRepo.Query(Arg.Any<Expression<Func<User, bool>>>()) .Returns(userData.AsQueryable()); ``` 取得所有 User 時不包含搜尋條件,filter = null,直接將 userData 轉 IQueryable 回傳,所以呼叫外部方法時會回傳全部 userData 的資料 ```C# // 包含搜尋條件,將 userData.Where 使用 filter 條件搜尋後回傳 userRepo.Query(Arg.Any<Expression<Func<User, bool>>>()) .Returns(arg => userData.Where(arg.ArgAt<Expression<Func<User, bool>> ``` 將外部方法 Returns 回傳的 userData 集合使用 LinQ Where,依據 GetUser 中所設定 filter 條件來篩選,以此方式隔離外部方法,因為外部方法是否正常運作不該影響當前測試方法 --- ## 3. NSubstitute 學習資源 1. [NSubstitute 官網](https://nsubstitute.github.io/) 2. [聊聊程式 NSub說明書](https://dotblogs.com.tw/initials/Series?qq=NSub%E8%AA%AA%E6%98%8E%E6%9B%B8) 3. [余小章 @ 大內殿堂 #NSubstitute](https://dotblogs.com.tw/yc421206/Tags?qq=NSubstitute) --- ## 完整程式碼 & 單元測試 GitHub:[NSubstituteWithExpression](https://github.com/darionnnnnn/blog/tree/master/Blog/NSubstituteWithExpression) GitHub:[NSubstituteWithExpressionTests](https://github.com/darionnnnnn/blog/tree/master/Blog/NSubstituteWithExpressionTests) --- ## 總結 ### NSubstitute 或是其他隔離工具對於有在寫單元測試的開發者來說應該都不陌生,所以對於一般使用方法就沒有多做說明,最近剛好寫到使用 Expression 的外部方法,但討論 & 教學好像滿少的(?),所以就記錄一下。 --- ### 參考資料 1. [stack overflow](https://stackoverflow.com/questions/46926953/how-to-use-the-expression-given-as-an-argument-to-generate-return-of-substitute) --- ## 新手上路,若有錯誤還請告知,謝謝