2015年10月13日 星期二

使用NSubstitute輔助單元測試及TDD (1)

 NSubstitute    TDD   C#  

背景


之前和朋友Chris討論,如何針對包含了DB Access的函式做單元測試;
在上上份工作的經驗,有點愚蠢的直接在測試環境建立單元測試的資料,測試後一併刪除。

雖然仍然可以達到Unit Test的目的,但整體開發效率上被拖慢了,另外維護單元測試的程式碼相對也複雜許多。

後來慢慢得知有一些Mock-up物件的library 拜讀Chris研究後的文章後,終於有時間學習一下NSubstitute這套Mock object的函式庫,也順便複習一下TDD

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





沒有留言:

張貼留言