二、隔离框架

  1、手写模拟对象和桩对象的问题

  写单元测试的时候,如果不借助框架和工具,有一部分时间必然花在构造模拟对象和桩对象上,不是说这样的工作没有意义,主要是哼哧哼哧写这么多代码显而易见会导致如下几个问题:

  (1)又要写单元测试代码,还要造N个伪对象?我TMD又不是苦力,写不过来啊写不过来;

  (2)有些类非常坑爹,一堆方法、事件和属性,除此之外,还有很多私有方法,写不过来啊写不过来;

  (3)某些方法参数对象属性很多,不知要写多少断言才能验证结束,写不过来啊写不过来;

  (4)如果模拟的方法被多次调用,一旦涉及状态保存问题,必须在内部写很多代码,写不过来啊写不过来;

  (5)写了一个模拟对象和桩对象,换到另一个测试中又TMD需要写一遍,写不过来啊写不过来。

  大家都知道一个道理,人不可靠,写的越多出现问题的概率越大,而且免不了要写很多相同或相似的代码我们自然会想到自动化,相信以开发人员的智慧,应该都会有这个觉悟,而且无数实践证明写程序当然高度自动化好。

  你要让程序员手动去写所有单元测试,好吧,有人肯定会先上网看看新闻渴了倒杯水顺便看看心仪的美女不渴也要去看美女一定要看到看不到会想什么时候能来然后再去看...

  单元测试很重要,但是没有多少人愿意投入精力去写,原因无它,唯懒而已。

  2、使用隔离框架

  还是根据一中的示例代码,我们采用隔离框架Rhino Mocks(历史悠久,资料丰富),看看如何通过隔离框架自动生成桩对象和模拟对象进行单元测试:

  (1)构造桩对象LogService

var mock = new Rhino.Mocks.MockRepository();
            var logService = mock.Stub<ILogService>();
            CurrentFileMonitor.LogService = logService;
            Assert.IsNotNull(CurrentFileMonitor.LogService, "ConfigFileMonitor stub LogService is not initialized");

  (2)构造模拟对象EmailService

var mock = new Rhino.Mocks.MockRepository();
            var emailService = mock.DynamicMock<IEmailService>();
            CurrentFileMonitor.EmailService = emailService;
            Assert.IsNotNull(CurrentFileMonitor.EmailService, "ConfigFileMonitor mock EmailService is not initialized");

  (3)单元测试

  a、经典的录制-回放模型

/// <summary>
        /// 使用录制-回放模型测试示例
        /// </summary>
        [Test]
        public void RhinoMocks_Analyze_WebServiceThrows_SendEmail()
        {
            var mock = new Rhino.Mocks.MockRepository();
            var logService = mock.Stub<ILogService>();
            var emailService = mock.DynamicMock<IEmailService>();
            using (mock.Record())
            {
                logService.AppendLog("input filename(abc.txt) is too short");
                LastCall.Throw(new NotImplementedException("fake exception")); //记录指定日志,抛出异常
            }
            CurrentFileMonitor.LogService = logService;
            CurrentFileMonitor.EmailService = emailService;

            var shortFileName = "abc.txt";
            CurrentFileMonitor.Analyze(shortFileName);

            mock.Verify(emailService);//VerifyAll()或者Verify(stub)不会对桩对象做任何断言,它们只适用于模拟对象
        }

  b、设置-操作-断言(AAA)模型

/// <summary>
        /// 设置-操作-断言模型测试示例 (Arrange-Act-Assert AAA)
        /// </summary>
        [Test]
        public void RhinoMocks_Analyze_WebServiceThrows_SendEmail_AAA()
        {
            var mock = new Rhino.Mocks.MockRepository();
            var logService = mock.Stub<ILogService>();
            logService.Expect(svc => svc.AppendLog("input filename(abc.txt) is too short"))
                      .Throw(new NotImplementedException("fake exception")); //记录指定日志,抛出异常

            var emailService = mock.DynamicMock<IEmailService>();
            CurrentFileMonitor.LogService = logService;
            CurrentFileMonitor.EmailService = emailService;

            mock.ReplayAll(); //移到操作模式

            var shortFileName = "abc.txt";
            CurrentFileMonitor.Analyze(shortFileName);

            //使用Rhino Mocks来断言
            emailService.AssertWasCalled(m => m.SendEmail(mailto:jeffwong@cnblogs.com">jeffwong@cnblogs.com;, "filename check", "fake exception"));//成功
            //emailService.AssertWasCalled(m => m.SendEmail(mailto:it@cnblogs.com it@cnblogs.com;"filename check", "fake exception"));//失败
        }

  不论是经典的录制-回放模式还是AAA(Arrange-Act-Assert,即设置-操作-断言)语法,使用Rhino Mocks生成伪对象进行测试可读性都是比较强的。目前可能AAA模式更加流行,写起来也确实简洁不少。本文示例只是小试牛刀,感兴趣可以下载本文demo试验一下。

  其他比较成熟的隔离框架还有Moq,Typemock Isolator,EasyMock.NET,NMock,NUnit.Mocks等等。目前Moq以其简洁方便无痛的学习曲线而迅速崛起,参考了一些资料,发现它的某些写法确实比Rhino Mocks简单直接可读性更强,尤其Moq天然的AAA写法,Rhino Mocks正是借鉴它而加入AAA,关于Moq的使用大家可以参考官方文档。

  后这里还是要提一下,示例中给出的代码都非常简单,而实际开发中进行单元测试远比理论中来的困难和复杂,比如文档很少或没有文档被人遗忘的历史遗留系统,项目中包含错综复杂的业务逻辑,生僻的应用系统领域知识以及频繁打补丁的需求变更等等,这样合理利用框架和工具写出的单元测试尤其重要,否则维护单元测试的代码也是一个极大的负担。