# 如何以 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)
---
## 新手上路,若有錯誤還請告知,謝謝