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

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

 

在上节中,完成了第一个单元测试,研究了各种特性,在本节,将介绍一些更实际的例子。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("错误"));
}

  

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

原文地址:https://www.cnblogs.com/Leo_wl/p/4762793.html