2015年3月5日

[軟體自動測試] - Stub, Mock in Unit Test

"自動測試" 這個名詞對我來說不太陌生,在剛進公司時當時有位資深工程師寫了一支資料傳輸的程式,而當時菜鳥的我們被要求要寫替這支程式寫測試案例,並且做Unit Test。事實上我當時還一頭霧水,什麼是Unit Test,連聽都沒聽過


時光飛逝後的現在,一兩年時間過去了,這段時間內做的專案"測試工作"算是開發中很重要的公事,最近再回頭看網路上IN91的教學文章特別有感觸以及體會其中的奧妙,因此想要寫下些紀錄



在TDD(Test-Driven Development)中分成了幾個步驟:
1. ATDD & BDD
2. TDD
3. Testing
4. Refactoring

在3項中Test還分成"驗收測試"、"整合測試"及"單元測試",在這一篇中主要講的是Testing中的單元測試中的Stub及Mock

Unit Test是對單一目標物件做測試,基本原則如下:
1.  一個測試案例只能有一個方法
2.  最小的測試單位
3.  不與外部(包含檔案、資料庫、網路、服務、物件或類別)直接相依
4.  不具備邏輯
5.  測試案例之間相依性為0

另外還有Stub, Mocks 及Fake

Stub - 用來驗證受測目標物件的回傳值 以及 驗證受測目標狀態改變
Mock - 則可用來驗證測試目標物件與其相依物件互動
Fake - 在測試中若需要用到.Net Framework原生套件,則可以用Fake來建立一個假的來用

用上一篇<Interface使用>來做一下練習。在這有一個Interface叫INewThings,他的實作則在Company裡。

    public interface INewThings
    {
        string CreateNewMember(PersonInfo person);
    };

    public class PersonInfo
    {
        public string Name { get; set; }
        public int age { get; set; }
    }

    public class Company : INewThings
    {
        string INewThings.CreateNewMember(PersonInfo PI)
        {
            if(PI.age <18)
            {
                return "too young to work";
            }
            else if(PI.age >65)
            {
                return "too old to work";
            }
            else
            {
                return PI.Name + "-" + PI.age;
            }
        }

        public string humanresource(INewThings NewThings, PersonInfo Newguy)
        {
            return NewThings.CreateNewMember(Newguy);
        }
    }

接著建立測試專案並撰寫測試案例,依照這程式它共有三個狀態,基於單元測試原則一次只驗證一件事情,因此TestMethod會有三個。

在Visual Studio中要使用Stub Mocks需先透過NuGet安裝,在測試專案上按右鍵選擇管理NuGet套件,並搜尋RhinoMocks下載。



接著撰寫測試程式
[TestMethod]
        public void TestAgeisInRange()
        {
            //arrange 
            INewThings stubNewThing = MockRepository.GenerateStub< INewThings >();
            stubNewThing.Stub(x => x.CreateNewMember(Arg.Is.Anything)).Return("Frank-27");
            Company cp = new Company();

            PersonInfo PI = new PersonInfo();
            PI.Name = "Frank";
            PI.age = 27;

            //act
            var actualWork = cp.humanresource(stubNewThing, PI);
            Assert.AreEqual("Frank-27", actualWork);

        }

        [TestMethod]
        public void TestAgeLessThan18()
        {
            //arrange 
            INewThings stubNewThing = MockRepository.GenerateStub< INewThings >();
            stubNewThing.Stub(x => x.CreateNewMember(Arg.Is.Anything)).Return("too young to work");
            Company cp = new Company();

            PersonInfo PI = new PersonInfo();
            PI.Name = "Brown";
            PI.age = 10;

            //act
            var actualWork = cp.humanresource(stubNewThing, PI);
            Assert.AreEqual("too young to work", actualWork);
        }

        [TestMethod]
        public void TestAgeGreatThan80()
        {
            //arrange 
            INewThings stubNewThing = MockRepository.GenerateStub< INewThings >();            
            stubNewThing.Stub(x => x.CreateNewMember(Arg.Is.Anything)).Return("too old to work");
            Company cp = new Company();

            PersonInfo PI = new PersonInfo();
            PI.Name = "Chen";
            PI.age = 85;

            //act
            var actualWork = cp.humanresource(stubNewThing, PI);
            Assert.AreEqual("too old to work", actualWork);
        }

在程式中可以看到因為humanresource這個方法其中一個參數是Interface INewThings,因此透過MockRepository.GenerateStub< INewThings >() 來生成一個獨立的物件提供測試使用,透過Mock產生取代物件後,還要透過.Return來設定預期回傳的值。

以上是Stub的寫法,接著以同樣範例來以Mock方式來撰寫

        [TestMethod]
        public void TestViaMocks()
        {
            MockRepository mock = new MockRepository();
            INewThings stubNewThings = mock.StrictMock();

            Company cp = new Company();

            List People = new List();
            PersonInfo person1 = new PersonInfo();
            person1.Name = "Frank";
            person1.age = 27;
            People.Add(person1);

            PersonInfo person2 = new PersonInfo();
            person2.Name = "Brown";
            person2.age = 12;
            People.Add(person2);

            PersonInfo person3 = new PersonInfo();
            person3.Name = "Chen";
            person3.age = 89;
            People.Add(person3);

            using (mock.Record())
            {
                cp.humanresource(stubNewThings, People[0]);
                LastCall
                    .Return("Frank-27");

                cp.humanresource(stubNewThings, People[1]);
                LastCall
                    .Return("too young to work");

                cp.humanresource(stubNewThings, People[2]);
                LastCall
                    .Return("too old to work");
            }

            using (mock.Playback())
            {
                var target1 = cp.humanresource(stubNewThings, People[0]);
                var target2 = cp.humanresource(stubNewThings, People[1]);
                var target3 = cp.humanresource(stubNewThings, People[2]);
            }

        }

結論:

根據In91大大的經驗
使用 stub 的比例大概是8~9成,使用mock的比例大概僅1~2成。而 fake 的方式,則用在特例,例如靜態方法跟 .net framework 原生組件。
而本身實務經驗上也很少用到這三個,也許打滾的還不夠久,但先熟悉起來未來也許會用到的!另外最近還摸了BDD、Selenium及重構,在另外做個紀錄。

資料參考
In91 - [30天快速上手TDD]

沒有留言:

張貼留言

<Javascript> How to uncompressed GZIP at front-end using Javascript

It's been a while I haven't share my coding work. In this article I would like to share how to receive a Gzip file via stream, unzip...