TDD 强迫你 Program to Interface

还是接上次的内容,继续测试Dollar class

先在有个新的需求--在使用Times方法之前,必须要做用户的身份验证,有权限的人才可以用这个方法,反之则不行。(后面称 需求(1)

在做完设计后,我们界定有个class 叫LoginChecker中的方法CheckPass将用来做权限的审查,返回值为bool型,如果有权限返回True, 反之为false。

首先看一下 如果不用TDD 我们脑中第一反应的功能代码实现,应该会是下面的样子--我们去new 了一个LoginChecker的实例,然后调用CheckPass的方法。

        //method to be tested
        public int Times(int multiplier)
        {
            var checker = new LoginChecker();
            if (checker.CheckPass())
            {
                return this.amount *= multiplier;
            }
            return 0;           
        }

这样我们Dollar class就紧密依赖LoginChecker class了。 如果我来实现 times方法,我可能会有以下两种处理方式:1.实现功能我自己Times的功能,但不做测试(理由是:CheckPass 还没写好,我怎么测试啊,测了也没用,可能CheckPass会抛异常)2. 等CheckPass写完了,我再写Times方法。你是否有嗅出这两种方式写出来的测试都很像集成测试?!TDD是讲究Isolation(独立,隔离)的。这里你要测的就是Times方法,其它所有的dependency(依赖)都应该用Stub(mock,fake找一个你喜欢的词,不过他们是有区别的)来替代。现在的问题是“这个怎么测呢?”一般有这个问题出现时,你的第一反应应该是 “设计是否有问题?为什么要绑定到一个特定的class?”。我们来看看怎么解决,把开始的问题换个方式问“能否不绑定到特定的class?”,可以的 那就是要把功能抽象出来,抽象成Abstract class或者Interface咯。那我们就抽象出一个IChecker的Interface吧

    public interface IChecker
    {
        bool CheckPass();
    }

让LoginChecker实现一下,不用写功能哦。

   public class LoginChecker : IChecker
    {
        #region IChecker Members

        public bool CheckPass()
        {
            throw new NotImplementedException();
        }

        #endregion
    }

下面看看如果光实现 Dollar class的话,实现会是什么样的


    public class Dollar
    {
        int amount;
        IChecker checker = null;

        ////method to be tested
        public int Times(int multiplier)
        {
            if (checker.CheckPass())
            {
                return this.amount *= multiplier;
            }
            return 0;
        }

        #endregion
    }

从上面代码中,有人肯定要问了,checker这个Instance 怎么来呢?这里可以用到dependency Injection了,一般depency Injection有两种 分别为contructor和setter, 当然factory 也是可以实现的,这里我们就用contructor injection来实现吧。

看看现在的 真正实现代码是什么?其实应该是以下这个样子,因为我们还没有 需求(1)。只是一番思考之后这个需求引导我们 应该Program to Interface然后用Depency Injection,这样才好测。


    public class Dollar
    {
        int amount;
        IChecker checker = null;


        public Dollar(int amount, IChecker checker)
        {
            this.amount = amount;
            this.checker = checker;
        } 


        ////method to be tested
        public int Times(int multiplier)
        {

           return this.amount *= multiplier;

        }

        #endregion
    }

那我们现在开始写需求(1)的测试代码,比方说先测一个简单的:如果Check没过,就总是返回 0

第一步,我们先要写一个stub,让它来替换掉,LoginChecker的CheckPass方法,有了stub你就可以完全控制你的测试了,即使LoginChecker还根本没实现

       class LoginCheckerStub : IChecker
        {

            #region IChecker Members

            public bool CheckPass()
            {
                return false;
            }

            #endregion
        }

真正的test case

        ///// <summary>
        /////A test for times
        /////</summary>
        [TestMethod()]
        public void TesttimesWithChecker()
        {
            int amount = 5;
            IChecker chcker = new LoginCheckerStub(); // new一个stub
            Dollar target = new Dollar(amount, chcker); //用stub来控制你的测试,返回你想要的值
            int actual = target.Times(10);
            Assert.AreEqual(0, actual);
        }
真正的实现代码

        ////method to be tested
        public int timesWithChekerFalseReturn0(int multiplier)
        {
            if (!checker.CheckPass())
            {
               return 0;
            }           
        }

如果checker过了,那5Times10应该返回50

第一步,我们先要写一个stub,让它来替换掉,LoginChecker的CheckPass方法,有了stub你就可以完全控制你的测试了,即使LoginChecker还根本没实现

       class LoginCheckerStub : IChecker
        {

            #region IChecker Members

            public bool CheckPass()
            {
                return true;
            }

            #endregion
        }

真正的test case

        ///// <summary>
        /////A test for times
        /////</summary>
        [TestMethod()]
        public void TesttimesWithCheckerTrueToCheck()
        {
            int amount = 5;
            IChecker chcker = new LoginCheckerStub(); // new一个stub
            Dollar target = new Dollar(amount, chcker); //用stub来控制你的测试,返回你想要的值
            int actual = target.Times(10);
            Assert.AreEqual(50, actual);
        }
真正的实现代码

        ////method to be tested
        public int timesWithCheker(int multiplier)
        {
            if (!checker.CheckPass())
            {
               return 0;
            }
           return this.amount *= multiplier;
           
        }

 那可能有些人会说,我就是不想设计什么接口,用什么依赖注入,我还能用TDD 来写我的实现吗?回答是可以的,不过要用到额外的工具,下面用Typemock实现一下,如果实现绑定某一特定的class 测试代码该怎么写。这段测试对应到文章开头的实现。


        /// <summary>
        /// Test with Typemock
        /// </summary>
        [TestMethod]
        public void TesttimesWithCheckerByTypemock()
        {
            MockManager.Init();
            Mock CheckerMock = MockManager.MockAll(typeof(LoginChecker));
            CheckerMock.ExpectAndReturn("checkPass", true);


            int amount = 5; // TODO: Initialize to an appropriate value
            Dollar target = new Dollar(amount); // TODO: Initialize to an appropriate value
            int actual = target.Times(2);
            Assert.AreEqual(10, actual);
            MockManager.ClearAll();
        }

本文介绍了,TDD会引导你面向接口编程,思考你的设计是否合理,是否高耦合。当然也介绍了 代码一定要高耦合情况下,怎么测试(TypeMock),后面将介绍测试中比较头痛的的问题 -- 处理系统时间和外部资源

posted @ 2011-05-23 22:06  麦克*堂  阅读(344)  评论(0编辑  收藏  举报