關於 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
// UserModel
public class User
{
public int Id { get; set; }
public string Name { get; set; }
}
// 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();
}
}
// IRepository
public interface IRepository<T>
{
IQueryable<T> Query(Expression<Func<T, bool>> filter = null);
}
3. 單元測試
[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. 說明
// 不包含搜尋條件,filter = null,直接將 userData 轉 IQueryable 回傳
userRepo.Query(Arg.Any<Expression<Func<User, bool>>>())
.Returns(userData.AsQueryable());
取得所有 User 時不包含搜尋條件,filter = null,直接將 userData 轉 IQueryable 回傳,所以呼叫外部方法時會回傳全部 userData 的資料
// 包含搜尋條件,將 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 學習資源
完整程式碼 & 單元測試
GitHub:NSubstituteWithExpression
GitHub:NSubstituteWithExpressionTests