zoukankan      html  css  js  c++  java
  • [Test] 单元测试艺术(2) 打破依赖,使用模拟对象,桩对象,隔离框架

    在上节中,完成了第一个单元测试,研究了各种特性,在本节,将介绍一些更实际的例子。SUT依赖于一个不可操控的对象,最常见的例子是文件系统,线程,内存和时间等。

    本系列将分成3节:

    1. 单元测试基础知识
    2. 打破依赖,使用模拟对象,桩对象,隔离框架
    3. 创建优秀的单元测试

    本节索引:

    伪对象(fake) 桩对象(stub) 模拟对象(mock)

    伪对象是一个通用术语,它即可指桩对象,也可指模拟对象。

    桩对象是指对系统中现有依赖项的一个替代品,可人为控制。

    模拟对象是用来决定一个单元测试是通过还是失败的伪对象。

    说明:fake是stub和mock的统称,因为看起来都像是真的对象。如果是用来检查交互的就是模拟对象,否则就是桩对象

    桩对象:

    模拟对象:

    为什么需要伪对象

    1. 外部依赖(系统中代码与其交互的对象,而且无法对其做人为控制)
    2. 反测试(而一旦测试中存在外部依赖,那么这个测试就是一个集成测试。运行慢,需要配置,依赖异常)

    如何处理?

    本质上都是外部依赖导致的,所以要做的是消除依赖。

    1. 分析接口
    2. 实现可人为控制的接口 

    注入桩对象

    1. 在构造函数上接受一个接口,并保存在一个字段里,以备后用。
    2. 保存在属性上
    3. 在调用方法前,使用方法参数,工厂类,依赖注入等

    隐藏桩对象(由于生产环境等其他原因,我们不希望暴露桩对象)

    1. 使用条件编译
    2. 使用条件特性
    3. 使用internal和[InternalVisibleTo]

    手工新建伪对象

    使用桩对象(适用于模拟返回值,不适用于检查对象间的交互情况。)

    这是非常常见的方式,但是这种方式受限制很多,如文件需要配置,运行慢。

      public class Config
        {
            public bool IsCheck(string name)
            {
                var str = File.ReadAllText("1.txt");
                return str == name;//此处可能是大量的逻辑处理
            }
        }
    

    改写注入

        public class Config
        {
            private IManager manager;
    //提供注入接口
            public Config(IManager manager)
            {
                this.manager = manager;
            }
            public bool IsCheck(string name)
            {
                var str = manager.GetConfig();
                return str == name;
            }
        }
    //真实的实现
        public class FileManager : IManager
        {
            public string GetConfig()
            {
                return File.ReadAllText("1.txt");
            }
        }
    //测试使用的实现
        public class StubManager : IManager
        {
            public string GetConfig()
            {
                return "str";
            }
        }
    //抽象出的接口
        public interface IManager
        {
            string GetConfig();
        }

    测试代码

        [TestClass]
        public class ConfigTests
        {
            private Config config;
    
            [TestInitialize]
            public void Init()
            {
                config = new Config(new StubManager());
            }
    
            [TestMethod]
            public void IsCheckTest()
            {
                Assert.IsTrue(config.IsCheck("str"));
            }
    
            [TestCleanup]
            public void Clean()
            {
                config = null;
            }
        }
    

    使用模拟对象(适用于对象之间的交互)

    当上面的方法返回false的时候,需要调用别的web服务记录下。而web服务还未开发好,即使开发好了,测试的时间也会变长很多。

    这里其实也体现了,stub的优点,可以任意的控制返回结果。

    新建一个mock

        public class Config
        {
            private IManager manager;
            public IWeb Web { get; set; }
            public Config(IManager manager)
            {
                this.manager = manager;
            }
            public bool IsCheck(string name)
            {
                var str = manager.GetConfig();
                var rst = str == name;
                if (!rst)
                    Web.Log("错误");
                return rst;
            }
        }
        /// <summary>
        /// 模拟对象
        /// </summary>
        public class MockWeb : IWeb
        {
            public string Erro { get; set; }
            public void Log(string erro)
            {
                Erro = erro;
            }
        }
    
        public interface IWeb
        {
            void Log(string erro);
        }
    

    测试代码

        [TestClass]
        public class WebTests
        {
            [TestMethod]
            public void LogTest()
            {
                var web = new MockWeb();
                //注入的方式非常多
                var config = new Config(new StubManager()) { Web = web };
                config.IsCheck("s");
                //最终断言的是模拟对象。
                Assert.AreEqual("错误", web.Erro);
            }
        }
    

    注意:一个测试只有一个mock,其他伪对象都是stub,如果存在多个mock,说明这个单元测试是在测多个事情,这样会让测试变得复杂和脆弱。

    使用隔离框架创建伪对象

    隔离框架简介

    手写stub和mock非常麻烦耗时,而且不易看懂等缺点。

    隔离框架是可以方便的新建stub和mock的一组可编程API。

    .net下常见的有Rhino Mocks,Moq

    这里使用RhinoMocks做示例(将使用录制回放模式和操作断言2种)

     

    录制回放

    新建mock对象

    来实现一个和上面mock的例子

            [TestMethod]
            public void LogMockTest()
            {
                var mocks = new MockRepository();
          //严格模拟对象 var mockWeb = mocks.StrictMock<IWeb>(); using (mocks.Record())//录制预期行为 { mockWeb.Log("错误"); } var config = new Config(new StubManager()) { Web = mockWeb }; config.IsCheck("s"); mocks.Verify(mockWeb); }

    严格模拟对象:是指只要出现预期行为以外的情况,就报错。

    非严格模拟对象:是指执行到最后一行,才会报错。

    新建stub对象

            [TestMethod]
            public void LogStubTest()
            {
                var mocks = new MockRepository();
                //非严格对象
                var mockWeb = mocks.DynamicMock<IWeb>();
                //桩对象
                var stubManager = mocks.Stub<IManager>();
                using (mocks.Record())
                {
                    mockWeb.Log("错误1");
                    stubManager.GetConfig();
                    LastCall.Return("str1");            //录制桩对象返回值
                }
                var config = new Config(stubManager) { Web = mockWeb };
                config.IsCheck("str");
                mocks.Verify(stubManager);          //桩对象不会导致测试失败
                mocks.VerifyAll();                  //启用非严格对象,测试直到这里才会确认是否报错
            }
    

     操作断言

            [TestMethod]
            public void LogReplayTest()
            {
                var mocks = new MockRepository();
                var mockWeb = mocks.DynamicMock<IWeb>();
                var config = new Config(new StubManager()) { Web = mockWeb };
                //开始操作模式
                mocks.ReplayAll();
                config.IsCheck("str1");
                //使用Rhino Mocks断言
                mockWeb.AssertWasCalled(o => o.Log("错误"));
            }
    

      

    注意:使用框架创建的动态伪对象,肯定没手工编写的伪对象执行效率高。

    本文作者:Never、C

    本文链接:http://www.cnblogs.com/neverc/p/4749197.html

  • 相关阅读:
    Qt 学习
    Qt 串口连接
    GitKraken使用教程-基础部分(9)
    GitKraken使用教程-基础部分(8)
    GitKraken使用教程-基础部分(7)
    GitKraken使用教程-基础部分(6)
    GitKraken使用教程-基础部分(5)
    GitKraken使用教程-基础部分(4)
    GitKraken使用教程-基础部分(3)
    C++ 利用template给函数中的变量赋初值
  • 原文地址:https://www.cnblogs.com/neverc/p/4749197.html
Copyright © 2011-2022 走看看