[Unit Test]單元測試簡介-Slide

  • 17663
  • 0

[Unit Test]單元測試簡介-Slide

前言

整理文件時發現一年前在部門分享的『單元測試簡介』投影片,就順便整理成文章,供朋友參考一下。

 

簡報分享

 

Demo說明

3A原則(Arrange, Act, Assert)

目標程式:


    public class Operation
    {
        public int Add(int a, int b)
        {
            return a + b;
        }
    }

測試程式


        /// <summary>
        ///Add 的測試
        ///</summary>
        [TestMethod()]
        public void AddTest()
        {
            //arrange
            Operation target = new Operation();
            int a = 1;
            int b = 2;
            int expected = 3;
            int actual;

            //act
            actual = target.Add(a, b);

            //assert
            Assert.AreEqual(expected, actual);
        }

Arrange指的是:

  1. 初始化測試目標類別,若為靜態方法則不需初始化
  2. 初始化測試目標類別所相依的介面instance,也就是所謂的stub/mock object
  3. 初始化測試目標類別上,要測試的方法,所需要使用到的參數
  4. 初始化預期結果

Act指的是:

  1. 呼叫測試目標類別上,要測試的方法

Assert指的是:

  1. 驗證預期結果與Act所得到的實際結果,是否符合

 

測試目標類別與外部類別直接相依的情況

說明:以一個透過RSA的token,進行驗證的簡化範例來進行說明。

重點在於:RSA的token,就像每秒會改變的亂數產生器。如果驗證的方法,直接相依於Token類別,則幾乎無法進行測試,因為每次的值都不一樣。

驗證的程式


    public class Authentication
    {
        /// <summary>
        /// 透過id去尋找設定的password,透過id尋找token對應的password,兩者相加才是合法的密碼
        /// </summary>
        /// <param name="id">The id.</param>
        /// <param name="inputPassword">The input password.</param>
        /// <returns></returns>
        /// <history>
        /// 1. joeychen, 2011/5/12, Created
        /// </history>
        public bool Verify(string id, string inputPassword)
        {
            Token token = new Token();
            string tokenPassword = token.Generate(id);

            Person person = new Person(id);
            string password = person.GetPassword();

            string validPassword = password + tokenPassword;

            return (inputPassword == validPassword);
        }
    }

Token的程式(以亂數產生器模擬)


    internal class Token
    {
        internal string Generate(string id)
        {
            return new Random().Next(0, 999999).ToString("000000");
        }
    }

Person的程式(以一個回傳hardcode的類別來當作DAO)


    public class Person
    {
        string _id = string.Empty;

        public Person(string id)
        {
            this._id = id;
        }

        /// <summary>
        /// 根據this._id去DB撈Password資料
        /// </summary>
        /// <returns></returns>
        internal string GetPassword()
        {
            return "91";
        }
    }

幾乎每次都會測試失敗的單元測試程式


        /// <summary>
        ///Verify 的測試
        ///</summary>
        [TestMethod()]
        public void VerifyTest()
        {
            //Arrange
            Authentication target = new Authentication();

            //Act
            bool result = target.Verify("joey", "123456789");

            //Assert
            //將驗證失敗,因為每一次的亂數,都不固定,所以Act結果是123456789的機率幾乎為0
            Assert.AreEqual(true, result);
        }

 

測試目標類別相依於介面,透過mock framework進行測試

原本的Authetication類別,將相依於Token與Person,改相依於IToken與IPerson


    public class AuthenticationInteractWithInterface
    {
        public IToken MyToken { get; set; }

        public IPerson MyPerson { get; set; }

        public bool Verify(string id, string inputPassword)
        {
            //Token token = new Token();
            string tokenPassword = MyToken.Generate(id);

            //Person person= new Person(id);
            string password = MyPerson.GetPassword();

            string validPassword = password + tokenPassword;

            return (inputPassword == validPassword);
        }
    }

使用mock framework來進行測試的單元測試程式(這邊以Rhino.Mock為例)


        /// <summary>
        ///Verify 的測試
        ///</summary>
        [TestMethod()]
        public void VerifyTest()
        {
            //Arrange

            //當呼叫IPerson.GetPassword()的時候,回傳"123"
            IPerson person = MockRepository.GenerateStub<IPerson>();
            person.Stub(x => x.GetPassword()).Return("123");

            //當呼叫IToken.Generate(),且參數為"joey"時,回傳"456789"
            IToken token = MockRepository.GenerateStub<IToken>();
            token.Stub(x => x.Generate("joey")).Return("456789");

            AuthenticationInteractWithInterface target = new AuthenticationInteractWithInterface { MyPerson = person, MyToken = token };

            //Act
            //預期結果為true,因為inputPassword為"123456789",而在白箱測試中,我們可以預期Person+Token所組出的password也為"123456789",所以為true
            bool result = target.Verify("joey", "123456789");

            //Assert
            Assert.AreEqual(true, result);
        }

結論

測試程式一點都不難寫,因為測試程式通常相當短,而且不具邏輯,就是一段直述型的function。

當您發現您的測試程式相當難寫、很難透過stub/mock來測試、測試程式相當不穩定時,就代表您實際運行的程式碼,設計得不夠好。

底下歸納出幾種狀況,以供參考。

  1. 測試程式很長:可能代表測試目標類別的內聚力不夠高。
  2. 很難透過stub/mock來測試:可能代表測試目標類別之間耦合性太高,類別之間直接相依。
  3. 測試程式很不穩定:可能代表測試目標類別職責太複雜,或是抽象地不夠漂亮,導致某一種職責變更時,測試程式就需要跟著改變。

單元測試應該具有FIRST特性:

  1. Fast:快速
  2. Independent:獨立
  3. Repeatable:可重複
  4. Self-Validating:可反應驗證結果。單元測試不論成功或失敗,都應該要從測試的reporting直接瞭解其意義或失敗原因
  5. Timely:及時。

Reference

  1. 單元測試的意義
  2. 自動化測試經驗分享

blog 與課程更新內容,請前往新站位置:http://tdd.best/