TDD学习笔记【六】一Unit Test - Stub, Mock, Fake 简介

这篇文章简介一下,如何通过 mock framework,来辅助我们更便利地模拟目标对象的依赖对象,而不必手工敲堆只为了这次测试而存在的辅助类型。

而模拟目标对象的部分,常见的有 stub object, mock object, fake object,本文也会简单介绍一下三者的不同点,并且通过实例,帮助读者快速的 pick up 实战经验。

安装与范例说明

本文的范例,使用 VS2013 为开发工具,mock framework 则是使用 Rhino.Mocks,通过 IoC 的方式,由构造函数来传入 stub/mock/fake object。

注:在 Microsoft Fakes 里面也有内建的 stub object,但是是类似 fake object 的方式产生,而非 Rhino.Mocks, moq 这类 mock framework 是使用动态产生 stub/mock object的方式。Isolating Code under Test with Microsoft Fakes

  1. 效益:顾客入场时,帮助店员统计出门票收入,确认是否核帐正确
  2. 角色:Pub 店员
  3. 目的:根据顾客与相关条件,算出对应的门票收入总值
public interface ICheckInFee
    {
        decimal GetFee(Customer customer);
    }

    public class Customer
    {
        public bool IsMale { get; set; }

        public int Seq { get; set; }
    }

    public class Pub
    {
        private readonly ICheckInFee _checkInFee;
        private decimal _inCome;

        public Pub(ICheckInFee checkInFee)
        {
            this._checkInFee = checkInFee;
        }

        /// <summary>
        /// 入场
        /// </summary>
        /// <param name="customers"></param>
        /// <returns>收费的人数</returns>
        public int CheckIn(List<Customer> customers)
        {
            var result = 0;

            foreach (var customer in customers)
            {
                var isFemale = !customer.IsMale;

                //女生免费入场
                if (isFemale)
                {
                    continue;
                }
                else
                {
                    //for stub, validate status: income value
                    //for mock, validate only male
                    this._inCome += this._checkInFee.GetFee(customer);

                    result++;
                }
            }

            //for stub, validate return value
            return result;
        }

        public decimal GetInCome()
        {
            return this._inCome;
        }
    }

CheckIn 说明:

当顾客进场时,如果是女生,则免费入场。若为男生,则根据 ICheckInFee 接口来取得门票的费用,并累计到 inCome 中。通过 GetInCome() 方法取得这一次的门票收入总金额。

Stub

Stub 通常使用在验证目标回传值,以及验证目标对象状态的改变。

这两种验证方式的重点,都摆在目标对象自身的逻辑。

即测试目标对象时,并不在乎目标对象与外部依赖对象如何互动,关注在当外部相依对象回传什么样的数据时,会导致目标对象内部的状态或逻辑变化。

所以这类的验证方式,是通过 stub object 直接模拟外部依赖回传的数据,来验证目标对象行为是否如同预期。

范例:

第一个测试,是验证收费人数是否符合预期,代码如下:

        [TestMethod]
        public void Test_Charge_Customer_Count()
        {
            //Arrange
            var stubCheckInFee = MockRepository.GenerateMock<ICheckInFee>();
            var target = new Pub(stubCheckInFee);
            var customers = new List<Customer>
            {
                new Customer {IsMale = true},
                new Customer {IsMale = false},
                new Customer {IsMale = false},
            };
            decimal expected = 1;
            //Act
            var actual = target.CheckIn(customers);
            //Assert
            Assert.AreEqual(expected, actual);
        }

使用 Rhino.Mocks 相当简单,步骤如下:

  1. 通过 MockRepository.GenerateStub<T>(),来建立某一个 T 类型的 stub object,以上面例子来说,是建立实现 ICheckInFee 接口的子类。
  2. 把该 stub object 通过构造函数,传给测试目标对象。
  3. 定义当调用到该 stub object 的哪一个方法时(例子中是GetFee方法),传入的参数是什么, stub 要回传是什么。

通过 Rhino.Mocks,就这么简单地通过 Lambda 的方式定义 stub object 的行为,取代了原本要自己建立一个实体类型,并实现ICheckInFee 接口,定义 GetFee 要回传的值。

上面的测试案例,是入场顾客人数3人,一男两女,因为目前 Pub 的 CheckIn 方法,只针对男生收费,所以回传收费人数应为1人。

第二个测试,则是验证收费的总数,是否符合预期。测试案例一样是一男两女,而通过 stub object模拟每一人收费为100元,所以预期结果门票收入总数为100。测试程序如下:

      [TestMethod]
        public void Test_Income()
        {
            //Arrange
            var stubCheckInFee = MockRepository.GenerateMock<ICheckInFee>();
            var target = new Pub(stubCheckInFee);
            stubCheckInFee.Stub(x => x.GetFee(Arg<Customer>.Is.Anything)).Return(100);
            var customers = new List<Customer>
            {
                new Customer {IsMale = true},
                new Customer {IsMale = false},
                new Customer {IsMale = false},
            };             //Act
            decimal inComeBeforeCheckIn = target.GetInCome();            //Assert
            Assert.AreEqual(0, inComeBeforeCheckIn);
            decimal expectedIncome = 100;
            //Act
            int chargeCustomerCount = target.CheckIn(customers);
            var actualIncome = target.GetInCome();
            //Assert
            Assert.AreEqual(expectedIncome, actualIncome);
        }

可以看到这里有两个 Assert,因为我们这里是验证状态的改变,期望在调用目标对象的 CheckIn 方法之前,取得的门票收入应为0。而调用之后,依照这个测试案例,门票收入应为100。

通过这两个测试案例,其实实际要测试的部分是,CheckIn 的方法只针对男生收费这一段逻辑。不管实际 production code,门票一人收费多少,都不会影响到这一份商业逻辑。

怎么根据环境或顾客来进行计价,那是在 production code 中,实现 ICheckInFee 接口的子类,要自己进行测试的,与 Pub 对象无关。这样一来,才能隔离 ICheckInFee 背后的变化。

Mock

使用时机:

上面提到验证对象的第三种方式:「验证目标对象与外部依赖接口的互动方式」,如下图所示:

这听起来可能相当抽象,但在开发中,的确可能会碰到这样的测试需求。

Mock 的验证比起 stub 要复杂许多,变动性通常也会大一点,但往往在验证一些 void 的行为会使用到,例如:在某个条件发生时,要记录 Log。这种情境,用 stub 就很难验证,因为对目标对象来说,没有回传值,也没有状态变化,就只能透过 mock object 来验证,目标对象是否正确的与Log 接口进行互动。

范例:

以这个范例来说,我们想验证的是:在2男1女的测试案例中,是否只呼叫 ICheckInFee 接口两次。程序代码如下:

  [TestMethod]
        public void Test_CheckIn_Charge_Only_Male()
        {
            //Arrange
            //两男一女
            var customers = new List<Customer>
            {
                new Customer {IsMale = true},
                new Customer {IsMale = true},
                new Customer {IsMale = false},
            };
            MockRepository mock=new MockRepository();
            ICheckInFee stubCheckInFee = mock.StrictMock<ICheckInFee>();
            using (mock.Record())
            {
                //期望调用ICheckInFee的GetFee()次数为2
                //Assert
                stubCheckInFee.GetFee(customers.ElementAt(0));
                LastCall.IgnoreArguments()
                    .Return((decimal) 100)
                    .Repeat.Times(2);
            }
            using (mock.Playback())
            {
                var target = new Pub(stubCheckInFee);
                //Act
                target.CheckIn(customers);
            }
        }

Fake

使用时机:

当目标对象使用到静态方法,或 .net framework 本身的对象,甚至于针对一般直接相依的对象,我们都可以透过 fake object 的方式,直接仿真相依对象的行为。

范例:

以这例子来说,假设 CheckIn 的需求改变,从原本的「女生免费入场」变成「只有当天为星期五,女生才免费入场」,修改程序代码如下:

view source

print?

01 public int CheckIn(List<Customer> customers)
02 {
03     var result = 0;
04  
05     foreach (var customer in customers)
06     {
07         var isFemale = !customer.IsMale;
08         //for fake
09         var isLadyNight = DateTime.Today.DayOfWeek == DayOfWeek.Friday;
10         //禮拜五女生免費入場
11         if (isLadyNight && isFemale)
12         {
13             continue;
14         }
15         else
16         {
17             //for stub, validate status: income value
18             //for mock, validate only male
19             this._inCome += this._checkInFee.GetFee(customer);
20  
21             result++;
22         }
23     }
24  
25     //for stub, validate return value
26     return result;
27 }

碰到 DateTime.Today 这类东西,测试案例就会卡住。总不能每次测试都去改测试机上面的日期,或是只有星期五或非星期五才执行某些测试吧。

所以,我们得透过 Isolation framework 来辅助,针对使用到的组件,建立 fake object。

首先,因为这个例子建立的 fake object,是针对 System.DateTime,所以在测试项目上,针对System.dll来新增 Fake 组件,如下图所示:

可以看到增加了一个 Fakes 的 folder,其中会针对要 fake 的 dll,产生对应的程序代码,以便我们进行拦截与改写。

使用 fake 对象也相当简单,先以测试星期五为例,程序代码如下:

view source

print?

01 [TestMethod]
02 public void Test_Friday_Charge_Customer_Count()
03 {
04     using (ShimsContext.Create())
05     {
06         System.Fakes.ShimDateTime.TodayGet = () =>
07             {
08                 //2012/10/19為Friday
09                 return new DateTime(2012, 10, 19);
10             };
11  
12         //arrange
13         ICheckInFee stubCheckInFee = MockRepository.GenerateStub<ICheckInFee>();
14         Pub target = new Pub(stubCheckInFee);
15  
16         stubCheckInFee.Stub(x => x.GetFee(Arg<Customer>.Is.Anything)).Return(100);
17  
18         var customers = new List<Customer>
19         {
20             new Customer{ IsMale=true},
21             new Customer{ IsMale=false},
22             new Customer{ IsMale=false},
23         };
24  
25         decimal expected = 1;
26  
27         //act
28         var actual = target.CheckIn(customers);
29  
30         //assert
31         Assert.AreEqual(expected, actual);
32     }
33 }

说明如下:

  1. using (ShimsContext.Create()){} 的范围中,会使用 Fake 组件。
  2. 当在 fake context 环境下,呼叫到 System.DateTime.Today 时,会转呼叫 System.Fakes.ShimDateTime.TodayGet,并定义其回传值为「2012/10/19」,因为这一天是星期五。

接着就跟原本的测试程序代码一样,当星期五时,只对男生收费。

侦错时,可以看到 DateTime.Today 变成我们仿真的「2012/10/19」,但实际系统日期是「2012/10/15」。

再增加一个星期六的测试案例,程序代码如下:

view source

print?

01 [TestMethod]
02 public void Test_Saturday_Charge_Customer_Count()
03 {
04      
05     using (ShimsContext.Create())
06     {
07         System.Fakes.ShimDateTime.TodayGet = () =>
08         {
09             //2012/10/20為Saturday
10             return new DateTime(2012, 10, 20);
11         };
12  
13         //arrange
14         ICheckInFee stubCheckInFee = MockRepository.GenerateStub<ICheckInFee>();
15         Pub target = new Pub(stubCheckInFee);
16  
17         stubCheckInFee.Stub(x => x.GetFee(Arg<Customer>.Is.Anything)).Return(100);
18  
19         var customers = new List<Customer>
20         {
21             new Customer{ IsMale=true},
22             new Customer{ IsMale=false},
23             new Customer{ IsMale=false},
24         };
25  
26         decimal expected = 3;
27  
28         //act
29         var actual = target.CheckIn(customers);
30  
31         //assert
32         Assert.AreEqual(expected, actual);
33     }
34 }

因为是星期六,所以1男2女,收费人数为3人。

补充:

连 System.dll 都可以进行 fake object 仿真了,所以即使是我们自定义,直接相依,也可以透过这种方式来仿真。

这样一来,即便是直接相依的对象,也可以进行独立测试了。

但强烈建议,针对自定义对象的部分,这是黑魔法类型的作法,如果没有包袱,建议对象设计还是要采 IoC 方式设计。如果是 legacy code,想要进行重构,摆脱直接相依的问题,则可先透过 fake object 来建立单元测试,接下来进行重构,重构后当对象不直接相依时,再改用上面的 stub/mock 方式来进行测试。

可以参考这篇在 Martin Fowler 网站上的文章:Modern Mocking Tools and Black Magic

注:即使不是在VS2012的环境底下,也可以到 Microsoft Research 上 download Moles: Moles - Isolation framework for .NET使用

结论

今天这篇文章介绍了 stub, mock 与 fake 的用法,但依笔者实际经验,使用 stub 的比例大概是8~9成,使用mock的比例大概仅1~2成。而 fake 的方式,则用在特例,例如静态方法跟 .net framework 原生组件。

也请读者朋友务必记得几个基本原则:

  1. 同一测试案例中,请避免 stub 与 mock 在同一个案例一起验证。原因就如同一直在强调的单元测试准则,一次只验证一件事。而 stub 与 mock 的用途本就不同,stub 是用来辅助验证回传值或目标对象状态,而 mock 是用来验证目标对象与相依对象互动的情况是否符合预期。既然八竿子打不着,又怎么会在同一个测试案例中,验证这两个完全不同的情况呢?
  2. Mock 的验证可以相当复杂,但越复杂代表维护成本越高,代表越容易因为需求异动而改变。所以,请谨慎使用 mock,更甚至于当发生问题时,针对问题的测试案例才增加 mock 的测试,笔者都认为是合情合理的。
  3. 当要测试一个目标对象,要 stub/mock/fake 的 object 太多时,请务必思考目标对象的设计是否出现问题,是否与太多细节耦合,是否可将这些细节职责合并。
  4. 当测试程序写的一狗票落落长时,请确认目标对象的职责是否太肥,或是方法内容太长。这都是因为目标对象设计不良,导致测试程序不容易撰写或维护的情况。问题根源在目标对象的设计质量。
  5. 将测试程序当作 production code 的一部份,production code 中不该出现的坏味道,一样不该出现在测试程序中,尤其是重复的程序代码。所以测试程序,基本上也需要进行重构。但也请务必提醒自己,测试程序基本上不会包含逻辑,因为包含了逻辑,您就应该再写一段测试程序,来测这个测试程序是否符合预期

原文地址:https://www.cnblogs.com/rohelm/p/3970535.html

时间: 2024-10-17 16:14:26

TDD学习笔记【六】一Unit Test - Stub, Mock, Fake 简介的相关文章

python之raw_input()(学习笔记六)

python之raw_input()(学习笔记六) 我们经常使用raw_input()读取用户的输入,如下例子所示: >>> name = raw_input('please input your name:'),截图如下: 下面简单说下,raw_input()与if搭配使用,脚本如下: #!/usr/bin/env python # -*- coding:utf-8 -*- birth = raw_input('birth:') if birth < 2000: print '0

swift学习笔记(六)析构过程和使用闭包对属性进行默认值赋值

一.通过闭包和函数实现属性的默认值 当某个存储属性的默认值需要定制时,可以通过闭包或全局函数来为其提供定制的默认值. 注:全局函数结构体和枚举使用关键字static标注    函数则使用class关键字标注 当对一个属性使用闭包函数进行赋值时,每当此属性所述的类型被创建实例时,对应的闭包或函数会被调用,而他们的返回值会被作为属性的默认值. ESC: Class SomeCLass{ let someProperty:SomeType={ //给someProperty赋一个默认值 //返回一个与

java之jvm学习笔记六-十二(实践写自己的安全管理器)(jar包的代码认证和签名) (实践对jar包的代码签名) (策略文件)(策略和保护域) (访问控制器) (访问控制器的栈校验机制) (jvm基本结构)

java之jvm学习笔记六(实践写自己的安全管理器) 安全管理器SecurityManager里设计的内容实在是非常的庞大,它的核心方法就是checkPerssiom这个方法里又调用 AccessController的checkPerssiom方法,访问控制器AccessController的栈检查机制又遍历整个 PerssiomCollection来判断具体拥有什么权限一旦发现栈中一个权限不允许的时候抛出异常否则简单的返回,这个过程实际上比我的描述要复杂 得多,这里我只是简单的一句带过,因为这

初探swift语言的学习笔记六(ARC-自动引用计数,内存管理)

Swift使用自动引用计数(ARC)来管理应用程序的内存使用.这表示内存管理已经是Swift的一部分,在大多数情况下,你并不需要考虑内存的管理.当实例并不再被需要时,ARC会自动释放这些实例所使用的内存. 另外需要注意的: 引用计数仅仅作用于类实例上.结构和枚举是值类型,而非引用类型,所以不能被引用存储和传递. swift的ARC工作过程 每当创建一个类的实例,ARC分配一个内存块来存储这个实例的信息,包含了类型信息和实例的属性值信息. 另外当实例不再被使用时,ARC会释放实例所占用的内存,这些

Linux System Programming 学习笔记(六) 进程调度

1. 进程调度 the process scheduler is the component of a kernel that selects which process to run next. 进程调度器需要使 处理器使用率最大化,并且提供 使多个进程并发执行的虚拟 Deciding which processes run, when, and for how long is the process scheduler's fundamental responsibility. 时间片:th

Lua学习笔记(六):函数-续

Lua中的函数是带有词法定界(lexical scoping)的第一类值(first-class values).第一类值指:在Lua中函数和其他值(数值.字符串)一样,函数可以被存放在变量中,也可以存放在表中,可以作为函数的参数,还可以作为函数的返回值.词法定界指:嵌套的函数可以访问他外部函数中的变量.这一特性给Lua提供了强大的编程能力. Lua中关于函数稍微难以理解的是函数也可以没有名字,匿名的.当我们提到函数名(比如print),实际上是说一个指向函数的变量,像持有其他类型的变量一样:

laravel3学习笔记(六)

原作者博客:ieqi.net ==================================================================================================== ORM Laravel3中MVC体系中Model里最重要的组成部分无疑是ORM了,ORM — object-relational mapper — 将数据操作面向对象化,使得整个web框架的核心风格统一,降低整体复杂度,为开发者提供便利. Laravel3中的ORM叫

TDD学习笔记【二】---单元测试简介

大纲 Testing 的第一个切入点:单元测试. 本篇文章将针对单元测试进行简介,主要内容包含了5W: Why What Where Who When 而How 的部分,属于实现部分,将于下一篇文章介绍工具与简单的范例. 最后会提到测试用例所代表的意义与其重要性. 前言 单元测试,是开发人员最该写的测试程序,却也是最容易被忽略的测试. 大家常碰到的测试相关问题是: 往往一堆人写测试程序时,自以为是在写单元测试,却压根就不是单元测试,而是集成测试. 生产代码是我写的,如果测试程序也是我写,那有什么

IBatis.Net学习笔记六--再谈查询

在IBatis.Net学习笔记五--常用的查询方式 中我提到了一些IBatis.Net中的查询,特别是配置文件的写法. 后来通过大家的讨论,特别是Anders Cui 的提醒,又发现了其他的多表查询的方式.在上一篇文章中我提到了三种方式,都是各有利弊:第一种方式当数据关联很多的情况下,实体类会很复杂:第二种方式比较灵活,但是不太符合OO的思想(不过,可以适当使用):第三种方式最主要的问题就是性能不太理想,配置比较麻烦. 下面是第四种多表查询的方式,相对第二种多了一点配置,但是其他方面都很好(当然