NSubstitute TDD C#
▌背景
在上上份工作的經驗,有點愚蠢的直接在測試環境建立單元測試的資料,測試後一併刪除。
雖然仍然可以達到Unit Test的目的,但整體開發效率上被拖慢了,另外維護單元測試的程式碼相對也複雜許多。
Chris 的文章 :
▌目標
這邊簡單實作一個可以計算全公司員工和高階主管的平均年齡、最大年齡和最小年齡。
至於員工的資料是從資料庫帶出來的; 可以區分職責如下:
DAL Repository : 負責從資料庫帶出資料
Service : 負責計算的邏輯
假設我們現在要對計算年齡的Service做單元測試,則必須Mock DAL Repository,讓單元測試的代碼不用真的到資料庫取得資料。
▌實作
▋Test Driven Development
以TDD的方式,先完成單元測試的代碼。
注意要Mock的物件必須從virtual class建立,請參考如下官方說明。
Warning: Substituting for classes
can have some nasty side-effects. For starters, NSubstitute can only work
with virtual members of the class, so any non-virtual code in the class will
actually execute! If you try to substitute for your class that formats your
hard drive in the constructor or in a non-virtual property setter then you’re
asking for trouble. If possible, stick to substituting interfaces.
|
所以DAL Repository請以Interface的方式建立,至於實作它的Class在單元測試根本就不需要了。
單元測試的代碼如下:
[TestClass]
public class UnitTest
{
private List<User> myUsers = null;
private IDalRepository dalRepository = null;
private AgeStatisticService stService = null;
public UnitTest()
{
myUsers = new List<User>() {
new User {Level=8, Age=20
},
new User {Level=8, Age=30
},
new User {Level=11, Age=40
},
new User {Level=13, Age=50
},
new User {Level=20, Age=60
} };
this.dalRepository = Substitute.For<IDalRepository>();
stService = new AgeStatisticService();
stService.DalRepository = this.dalRepository;
}
[TestMethod]
public void TestAll()
{
this.dalRepository.GetAll().Returns(myUsers);
int expectedAvg = 40;
int actualAvg = stService.CompanyAverageAge;
Assert.AreEqual(expectedAvg, actualAvg);
int expectedMax = 60;
int actualMax = stService.CompanyMaxAge;
Assert.AreEqual(expectedMax, actualMax);
int expectedMin = 20;
int actualMin = stService.CompanyMinAge;
Assert.AreEqual(expectedMin, actualMin);
}
}
|
重點在於黃色的部分:
1.
建立Mock-up 物件
this.dalRepository
= Substitute.For<IDalRepository>();
|
2.
指定Mock-up object裡面的properties/fields/methods回傳的值,至於回傳的值就在單元測試程式碼裡面指定。
this.dalRepository.GetAll().Returns(myUsers);
|
3.
最後將此Mock-up object注入到 Service實體物件(AgeStatisticService)使用
▋完成主程式
l Model
public class User
{
public int Age { get; set; }
public int Level { get; set; }
}
|
l DAL repository
public interface IDalRepository
{
List<User> GetAll();
}
|
l Service : 主要邏輯
public class AgeStatisticService
{
public IDalRepository DalRepository;
public int CompanyAverageAge
{
get
{
var users = this.DalRepository.GetAll();
return this.calAvgAge(users);
}
}
public int CompanyMaxAge
{
get
{
var users = this.DalRepository.GetAll();
return this.calMaxAge(users);
}
}
public int CompanyMinAge
{
get
{
var users = this.DalRepository.GetAll();
return this.calMinAge(users);
}
}
private int calAvgAge(List<User> users)
{
int sum = users.Sum(x => x.Age);
return sum / users.Count;
}
private int calMaxAge(List<User> users)
{
return users.Max(x => x.Age);
}
private int calMinAge(List<User> users)
{
return users.Min(x => x.Age);
}
}
|
▋Run the unit test
執行測試程式時,可以發現在未實作DAL的情況下,我們仍然可以透過NSubstitute來模擬它。 如此就可以達成單元測試的decoupling, 讓測試程式專注在我們要測試的程式碼。
▋Furthermore : 設定不同參數條件下,Mock object回傳的值
例如:
l 當該方法帶入參數
>= 10時,回傳X。
this.dalRepository.GetSupervisors(Arg.Is<int>(x => x
>= 10)).Returns(X);
|
l 當該方法帶入參數 ==
8 時,回傳Y。
this.dalRepository.GetUsersOfLevel(Arg.Is<int>(x => x ==
8)).Returns(Y);
|
l 當該方法帶入參數無論為任何值,回傳Z。
this.dalRepository.Get(Arg.Any<int>()).Returns(Z);
|
▌Reference
沒有留言:
張貼留言