TestDouble Learning Summary

  • 目录
    • 什么是测试替身
      • 概念
      • 原因
      • 测试替身使用流程
    • 测试替身的种类有哪些
      • 测试替身类型1:Test Stub 桩
      • 测试替身的类型2:Fake Object 伪造对象
      • 测试替身的类型3:Test Spy 测试间谍
      • 测试替身的类型4:Mock Object 模拟对象,验证 SUT 和 DOC 的交互是否正确
    • Moq
      • 简述
      • Stub(存根)和Mock(模拟)
      • Setup 伪造方法
      • Setup 伪造属性
      • Raise 伪造事件
      • It 参数匹配 & CallBack 回调
    • 如何使用 & 总结

目录

什么是测试替身

概念

  • 测试替身(Test Double)是测试中用来表示测试意图代替真实组件对象组件
  • 使用测试替身的根本目的是使用替身替换一个模块的真实协作部分,用以隔离被测试对象。即是将测试代码与其周围隔离开

原因

在软件开发中,我们很难避免对象或组件依赖,这些依赖的存在可能增加测试的时间和成本,比如有些被依赖的组件速度很慢或难以控制,也可能存在开发时被依赖的组件还没有完成的情况。这些都会对测试的编写和运行产生负面影响,降低工程效能。使用测试替身可以隔离架构依赖,分离被测对象,避免被测对象受到依赖组件的影响,有效降低测试的运行时间和开发成本。

测试替身使用流程


其中,SUT(System Under Test)为被测对象,DOC(Depended On Component)为被测对象所依赖的组件。引入测试替身(Test Double)替换掉 DOC,这样就可以有效地控制 SUT 的依赖,降低 DOC 自身的复杂性对 SUT 的影响。
通常,准备阶段(Setup)需要创建合适的测试替身并装配 SUT,执行阶段(Exercise)运行 SUT 并调用相关依赖,执行完成后在验证阶段(Verify)验证测试的结果。

测试替身的种类有哪些

测试替身类型1:Test Stub 桩

桩是指用最简单的代码来替代真实实现。当某一段代码A依赖了B的一个较复杂方法的返回值,而我们要测试A,如果我们并不关心B的返回值是多少,就可以写出一个B`(桩函数)。
效果:

  1. 隔离。如果A依赖了B,B依赖了C D,通过对于B打桩,就切断了A和C D的联系
  2. 补齐。如果A依赖了B,而B是其他语言写的,或者必须依赖某运行环境,就可以对B打桩替代B,让A可以运行下去
  3. 控制。如果A依赖B的某个返回值,通过B`,直接设定返回值可以让A运行下去。

例子

public class LogStub : ILogger
{public LogLevel GetLevel(){return LogLevel.WRAN;}public void Log(LogLevel level, string message){}
}

写入日志需要服务器的支持,这里构建LogStub测试桩,实现了ILogger接口,既不调用服务器打印日志,也不做任何事情,仅仅是为了日志的测试而存在。

测试替身的类型2:Fake Object 伪造对象

例子
接口

public interface IUserRepository
{void SaveChange(User user);User FindUserByID(int id);User FindUserByName(string name);
}

实现接口的伪造对象

public class FakeUserRepository : IUserRepository
{private Dictionary<int, User> _dic = new Dictionary<int, User>();public User FindUserByID(int id){User user;_dic.TryGetValue(id, out user);return user;}public User FindUserByName(string name){return _dic.Where(x => x.Value.Name == name).Single().Value;}public void SaveChange(User user){if (_dic.ContainsKey(user.ID) == false){_dic.Add(user.ID, user);}}
}

使用伪造对象同样模拟了数据库的功能,实现最简单的测试要求。

测试替身的类型3:Test Spy 测试间谍

例子
待测试类:

public class DLog
{private DLogTarget[] _targets;public DLog(DLogTarget[] targets){_targets = targets;}public void Write(string message){foreach (var item in _targets){item.Write(message);}}
}
public interface DLogTarget
{void Write(string message);
}

这个类用于向所有的DLogTarget发送日志,如果我们想知道DLog类能否驱动DLogTarget进行正确的操作,正常的DLogTarget不会告诉我们,而间谍DLogTarget会告诉我们。

测试间谍类:

public class SpyTarget : DLogTarget
{private List<string> log = new List<string>();public void Write(string message){log.Add(message);}public bool received(string message){return log.Contains(message);}
}

使用间谍类,模拟了一个DLogTarget,它可以根据我们的需要告诉我们想知道的一切。

测试类:

public class DLogTest
{[Fact]public void WriteToAll(){SpyTarget spy1 = new SpyTarget();SpyTarget spy2 = new SpyTarget();DLogTarget[] targets = { spy1, spy2 };DLog dLog = new DLog(targets);dLog.Write("test");Assert.True(spy1.received("test"));Assert.True(spy2.received("test"));}
}

这里测试到了DLog的行为。当DLog成功向DLogTarget写入信息时,通过SpyTarget的received方法检测是否正确操作。

测试替身的类型4:Mock Object 模拟对象,验证 SUT 和 DOC 的交互是否正确

模拟对象是特殊的间谍对象。他不仅是像间谍对象那样暴露自己的隐私信息以供查询,还会模拟被模拟对象的行为与待测试对象进行交互,他比模Fake多的是“可以配置的针对不同参数的响应”,即 Mock对象可以做到传入不同的参数的时候,给予不同的响应,且如何响应式可以配置的。

例如,一个网络接口,当我们传入1的时候返回A,传入2的时候返回B,要实现这样的一个模拟对象,就叫Mock。

在.NET中,使用Moq

Moq

简述

我们通过上述的Fake的方式将实际的运行环境和测试环境隔离开,但是这对单元测试的成本还是太高了,这就需要一种隔离隔离框架来帮助我们进一步提高我们的测试编写效率—Moq
简单的Moq例子:
完全不用自己实现FakeXXX,并且用一个lambda表达式就完成了我们期望的验证。

[TestMethod]
public void TestMethod2()
{var fakeXXX = new Mock<T>();var xxx = new XXX(fakeLog.Object);fakeXXX.Verify(log => log.方法名("OK"));
}

Stub(存根)和Mock(模拟)

StubMock都是测试方法依赖隔离的伪造对象,不同之处是Stub测试方法运行所需要的依赖Mock测试方法验证所需要的依赖

待测代码:

    public class Foo{private ILog _log1;private ILog _log2;public Foo(ILog log1,ILog log2){_log1 = log1;_log2 = log2;}public void DoB(){//do somethingvar text = _log1.Read();_log2.Write(text);}}

测试代码:

        [TestMethod]public void TestMethod(){var fakeLog1 = new Mock<ILog>();var fakeLog2 = new Mock<ILog>();fakeLog1.Setup(log => log.Read()).Returns("I'm moq");var foo = new Foo(fakeLog1.Object,fakeLog2.Object);foo.DoB();fakeLog2.Verify(log => log.Write("I'm moq"));}

如上,这里的fakeLog1是Stub,而fakeLog2是Mock。1确保方法正常运行,2确保方法正常验证。Stub在Moq中对应方法的是Setup,Mock对应的方法是Verify。

Setup 伪造方法

Setup函数用于伪造特定方法的返回值。其伪造方法返回值的格式为:

伪对象.Setup(fake=>fake.方法名).Returns(返回值)

Moq官方文档例子:

public interface IFoo
{Bar Bar { get; set; }string Name { get; set; }int Value { get; set; }bool DoSomething(string value);bool DoSomething(int number, string value);string DoSomethingStringy(string value);bool TryParse(string value, out string outputValue);bool Submit(ref Bar bar);int GetCount();bool Add(int value);
}public class Bar
{public virtual Baz Baz { get; set; }public virtual bool Submit() { return false; }
}public class Baz
{public virtual string Name { get; set; }
}

如果期望IFoo的GetCount方法返回值为3:

var fakeFoo = new Mock<IFoo>();
fakeFoo.Setup(fake => fake.GetCount()).Returns(3);

期望DoSomething方法传入"fitness"是返回"slim":

fakeFoo.Setup(fake => fake.DoSomething("fitness")).Returns("slim");

如果重复多次的部分是一样的:

fakeFoo.Setup(fake => fake.DoSomething(It.IsAny<string>())).Returns((string value)=>value+"makes  me slim");

可以使用参数匹配It.IsAny(),是任意字符串输入都被伪造。
同时,对于空参数我们也可以抛出异常:

fakeFoo.Setup(fake => fake.DoSomething("")).Throws(new ArgumentException("you must do something to make you slim"));

如果连续调用统一方法,需要返回不同的值。这就需要CallBack函数,可以让你在方法调用的时候执行一个回调函数。

int count=1;
fakeFoo.Setup(fake => fake.DoSomething("fitness")).Returns((string value)=>$"{value} {results} x{count}").CallBack(()=>count++);

Setup 伪造属性

同样利用Moq的官方文档的例子:
期望IFoo的Name方法返回值是"TW-Qi"

var fakeFoo = new Mock<IFoo>();
fakeFoo.Setup(fake => fake.Name).Returns("TW-Qi");

Moq的递归伪造,期望伪造IFoo接口的属性Bar的子属性Baz的Name:

var fakeFoo = new Mock<IFoo>();
fakeFoo.Setup(fake => fake.Bar.Baz.Name).Returns("TW-Qi");

Raise 伪造事件

待学习。。。

It 参数匹配 & CallBack 回调

It:

  • It.IsAny<T>,匹配指定类型参数
  • It.IsNotNull<T>,匹配指定类型参数,Null除外
  • It.Is<T>(Predicate),匹配指定类型参数,满足Predicate的条件
  • It.IsInRange<T>(T from, T to, Range rangeKind),匹配指定类型参数,满足一定的from到to的范围。
    其中Range.Inclusive代表参数在[from,to]之内满足
    其中Range.Exclusive代表参数在(from,to)之内满足
  • It.IsIn<T>(IEnumerable<T> items),匹配指定类型参数,在序列内
  • It.IsIn<T>(params T[] items),匹配指定类型参数,在序列内
  • It.IsNotIn<T>(IEnumerable<T> items),匹配指定类型参数,在序列外
  • It.IsNotIn<T>(params T[] items),匹配指定类型参数,在序列外
  • It.IsRegex(string regex),字符串正则匹配
  • It.IsRegex(string regex, RegexOptions options),字符串正则匹配

CallBack:
无参结构:上述
有参结构:

//普通参数格式
fakeFoo.Setup(fake => fake.DoSomething(It.IsAny<string>())).Returns(true).Callback((string s) => callArgs.Add(s));
//泛型参数格式
fakeFoo.Setup(fake => fake.DoSomething(It.IsAny<string>())).Returns(true).Callback<string>(s => callArgs.Add(s));

如何使用 & 总结

使用场景分析:(如图一)

  • 控制点:在测试的执行过程中,SUT 需要调用 DOC 并完成测试目标。控制点是指通过控制 DOC 对 SUT 的间接输入获得期望的执行结果。比如 SUT 调用 DOC 时,若 DOC 返回 TRUE,SUT 执行成功, 若 DOC 返回 FALSE,SUT 执行失败。此时可以通过测试替身控制 DOC 的返回结果来为测试创造期望的测试条件。Test Stub 的作用是构造控制点。
  • 观察点:观察点是指收集和记录 SUT 对 DOC 的间接输出去验证那些难以观察到的东西,比如执行产生的副作用或者对其它被测对象所产生的影响等,最终通过观察点来验证 SUT 是否与 DOC 发生了期望的交互行为。Mock Object 和 Test Spy 被用来作为观察点使用。
  • 简单实现以完整替代:测试替身不用来作为控制点或观察点,但实现了 SUT 所依赖的组件或者系统的功能,来替代真实的 DOC,使 SUT 可以无感地调用 DOC 并完成功能。Fake Object 就是在这种情况下被使用的。

总结(引用):

  • 桩是最简单的代码代替真实实践,通常都不实现,只做占位,让程序可以编译
  • 伪造对象是加强的桩,它是真实实现的简单版,用简单版模拟真实对象,纸老虎
  • 测试间谍是特殊的伪造对象,玩具老虎
  • 模拟对象是特殊的间谍对象。他可以针对不同的输入进行不同的输出,克隆老虎

TestDouble Learning Summary相关推荐

  1. 20162314 《Program Design Data Structures》Learning Summary Of The First Week

    20162314 2017-2018-1 <Program Design & Data Structures>Learning Summary Of The First Week ...

  2. 机器学习概要(MACHINE LEARNING SUMMARY)

    机器学习概要(MACHINE LEARNING SUMMARY) 监督学习 回归分析与线性回归 1.例如营业额预测,传统算法必须知道计算公式,机器学习可以帮你找到核心的函数关系式,利用它推算未来预测结 ...

  3. Machine Learning Summary

    Machine Learning Summary General Idea No Free Lunch Theorem (no "best") CV for complex par ...

  4. 程序员学英语——In November the English learning summary

    这个月的英语学习又恢复了学习的激情.之前一段时间确实是有点儿懈怠了,虽然每天依旧到机房最先学习的是 英语,但有时候基本上算是无效输入,电脑视频虽然开着,但没有集中注意力去听.时间长了,自己也没有太注意 ...

  5. HTML Learning Summary

    HTML学习总结 一.HTML是什么? 二.HTML文档结构 三.HTML常用元素 1.标题 2.文本 3.空元素 4.区块元素 5.超链接 (1)超链接语法 (2)超链接的作用 (3)超链接元素\常 ...

  6. 测试您的neo4j nest js应用程序

    This article is one of a series of blog posts that accompany the Livestream on the Neo4j Twitch Chan ...

  7. Machine learning week 10(Andrew Ng)

    文章目录 Reinforcement learning 1. Reinforcement learning introduction 1.1. What is Reinforcement Learni ...

  8. 最新Linux教程发布下载【最后更新4月12日

    以下是Linux爱好者最新发布的Linux书籍,本贴定期更新,欢迎下载. 红帽(RedHat)授权Linux认证培训中心.只要你敢来,我就敢让你过! 二本Linux测试题目考察 http://www. ...

  9. jQuery Ajax 方法调用 Asp.Net WebService 的详细例子

    这很常用,搜索了一下博客园的"找找看"和谷歌,看到大部分都是转载于一两篇文章(而且来源还不是博客园),有的是简单的说一点无法运行,给初学者的调试和学习带来不方便,我在这里将jQue ...

最新文章

  1. OSPF分解试验部分-LAB3:OSPF各种网络类型试验
  2. CentOS-7.2部署OpenLDAP服务器以及客户端
  3. linux下yum源软件服务器的搭建与使用
  4. HTML 各种鼠标手势
  5. RocketMQ实战与原理---安装、部署及简单应用
  6. boost::spirit模块实现一个复数微生成器的测试程序
  7. MTK平台Android4.4 拍照默认图片格式修改
  8. 对Session、Cookie的完全理解
  9. Hibernate3.x异常No row with the given identifier exists 解决方法
  10. 数组元素替换_LeetCode基础算法题第183篇:一维数组的重新洗牌
  11. 博弈论 斯坦福game theory stanford week 2.1_
  12. 用vbs脚本实现软件的自动登录
  13. 浅谈CGI基本原理和底层基本实现
  14. ISP - bayer 是什么?
  15. 2021全国人工智能大赛(NAIC)视觉编码赛道初赛一阶段baseline分享
  16. 伦敦城市级MaaS出行服务可行性研究
  17. STM32 I2S学习(一)
  18. 怎么把pdf文件压缩到最小?四招快速压缩!
  19. i710750h和r74800h玩游戏哪个好 r7 4800h和i7 10750h哪个性能好
  20. 苹果x与苹果xs的区别_苹果Xs相比苹果X,两者谁更值得入手?用户:苹果X宝刀未老!...

热门文章

  1. 大叔分几种类型_什么是大叔型男友?什么类型的女孩会喜欢大叔?
  2. 广义拉格朗日与对偶问题
  3. LED控制器使用方法
  4. python的选择结构教学设计_python选择结构教学设计
  5. 数海笔记(3)-极座标
  6. Java实现网络爬虫:爬取京东商品案例
  7. Linux中dd命令详解(转载)
  8. 扑克牌洗牌发牌,并排序输出(c++)
  9. win10安装docker教程、常见问题和原理总结
  10. AssertionError: Duplicate registrations for type ‘experimentalOptimizer‘解决方案