打破依赖,使用模拟对象,桩对象,隔离框架

打破依赖,使用模拟对象,桩对象,隔离框架

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

本系列将分成3节:

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

本节索引:

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

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

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

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

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

桩对象:

模拟对象:

为什么需要伪对象

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

如何处理?

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

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

注入桩对象

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

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

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

手工新建伪对象

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

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


1

2

3

4

5

6

7

8

public class Config

  {

      public bool IsCheck(string name)

      {

          var str = File.ReadAllText("1.txt");

          return str == name;//此处可能是大量的逻辑处理

      }

  }

改写注入


1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

    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();

    }

测试代码


1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

[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


1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

public class Config

{

    private IManager manager;

    public IWeb Web { getset; }

    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 { getset; }

    public void Log(string erro)

    {

        Erro = erro;

    }

}

public interface IWeb

{

    void Log(string erro);

}

测试代码


1

2

3

4

5

6

7

8

9

10

11

12

13

14

[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的例子


1

2

3

4

5

6

7

8

9

10

11

12

13

[TestMethod]

public void LogMockTest()

{

    var mocks = new MockRepository();<br>      //严格模拟对象

    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对象


1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

[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();                  //启用非严格对象,测试直到这里才会确认是否报错

}

 操作断言


1

2

3

4

5

6

7

8

9

10

11

12

[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("错误"));

}

  

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

时间: 2024-11-08 20:31:00

打破依赖,使用模拟对象,桩对象,隔离框架的相关文章

单元测试(四)-隔离框架NSubstitute

之前学习了单元测试的基础知识,以及桩对象和模拟对象的不同作用.但在实际应用中,往往不会直接手写桩对象或者模拟对象,而是使用隔离框架动态的创建这些对象,这可以让测试变得更简便.快捷,还可以更好地应对复杂的测试.这里学习的便是隔离框架的一种--Nsubstitute. 一 开始使用NSubstitute 在前面有一个测试场景为:使用LogAnalyzer.Analyze方法分析文件,如果文件名过短,就记录日志,为了测试LogAnalyzer类与ILogger实现之间的交互,需要手写一个实现了ILog

读书笔记-单元测试艺术(三)-使用桩对象解除依赖

一.几个概念 1.什么是外部依赖 外部依赖是指在系统中代码与其交互的对象,而且无法对其做人为控制. 最常见的例子是文件系统.线程.内存和时间等,我们使用桩对象来处理外部依赖问题. 2.什么是桩对象 桩对象是对系统中现有依赖的一个替代品,可人为控制. 通过使用桩对象,无需涉及依赖项,即可直接对代码进行测试. 3.什么是重构 重构是指不影响已有功能而改变代码设计的一种行为 4.什么是接缝 接缝是指代码中可以插入不同功能(如桩对象类)的地方. 二.解除依赖 抽象一个接口 namespace LogAn

单元测试(二)-桩对象

在单元测试时,难免会碰到一些外部依赖,外部依赖是指在系统中代码与其交互的对象,而且无法对其做人为控制,比如文件系统.线程.内存.时间.数据库结果集等,这时可以使用伪对象(fake)来替代外部依赖,桩对象(stub)便是其中之一 一 桩对象 a) 桩对象是对系统中现有依赖项的一个替代品,可人为控制.通过使用桩对象,无需涉及依赖项,即可直接对代码进行测试.使用桩对象可以轻松地控制模拟依赖项的返回值会行为(比如模拟内存溢出异常). b) 使用桩对象的前提是要找到原系统中的接缝(Seam).接缝是指代码

ABP文档 - 对象与对象之间的映射

文档目录 本节内容: 简介 IObjectMapper 接口 集成 AutoMapper 安装 创建映射 自动映射的特性 自定义映射 扩展方法 MapTo 单元测试 预定义的映射 LocalizableString -> string 注入 IMapper 简介 把一个对象映射到另一个相似的对象很常见,两个对象(类)具有相似或相同的属性,它们之间要互相映射,其实这项工作重复且无聊,考虑一个典型的应用服务方法,如下: public class UserAppService : Applicatio

O-C相关-05:对象与对象的关系

对象与对象的关系 1.对象与对象的关系 依赖 关联 组合 常常讨论对象与对象关系时会提供两个属于:内聚性,耦合性 内聚一般指功能上的指向性 耦合一般指关联上的依赖性 2.依赖: 对象之间最弱的一种关联方式,是临时性的关联.代码中一般指由局部变量.函数参数.返回值建立的对于其他对象的调用关系. 依赖一般情况下是以下几种情况之一: a.ClassA中某个方法的参数类型是ClassB:  这种情况成为耦合: b.ClassA中某个方法的参数类型是ClassB的一个属性: 这种情况成为紧耦合: c.Cl

JavaScript中通过arguments对象实现对象的重载

<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>Insert title here</title> <script type="text/javascript"> /* *1.js 中不存在函数的重载 2.js函数定义时候的形参个数,和执行时候时,传递的实参的个数可以不一样. 3.js执行时候,会将真实的参数,封

【Hibernate步步为营】--核心对象+持久对象全析(一)

引言 上篇博客对Hibernate进行了基本的解析,并分析了它的一些特性.Hibernate能够如此的流行也是因为它有诸多优点,任何事物都有两面性,Hibernate虽然流行,但是也有很多缺点,其中最主要的是封装问题,想要使用数据库特性的语句,该框架就显得很不成熟了.那接下来讨论下有关Hibernate的核心运行机制. 一.Hibernate核心 1.核心解析图 一个非常简单的Hibernate体系结构图: 从上图可以看出,Hibernate使用数据库和配置信息来为应用程序提供持久化服务.应用程

MVC传递数据-传递对象或对象集合

前言 本文主要介绍从View(或者js)文件向Controller提交对象或者对象集合,比如,将表格中的一行数据作为一个对象提交,或将多行数据作为一个集合提交到Controller. 回顾 从View(或者js)文件向Controller提交数据,你可能见过以下几种方式: 将提交的数据附在url地址后面 $.ajax({ type: "POST", url: "/InviteBid/UpdateBidZRecord/?JudgeBidId=" + JudgeBidI

json数组对象和对象数组

一.Json的简单介绍 从结构上看,所有的数据最终都可以分成三种类型: 第一种类型是scalar(标量),也就是一个单独的string(字符串)或数字(numbers),比如“北京”这个单独的词. 第二种类型是sequence(序列),也就是若干个相关的数据按照一定顺序并列在一起,又叫做array(数组)或List(列表),比如“北京,东京”. 第三种类型是mapping(映射),也就是一个名/值对(Name/value),即数据有一个名称,还有一个与之相对应的值,这又称作hash(散列)或di