解析 Golang 测试(4)- 一篇文章教你分清 Mock,Stub,Fake
时间:2022-9-2 作者:smarteng 分类: Go语言
今天继续我们的【解析Golang测试】第四篇,对此前文章感兴趣的同学可以点击进入:
日常开发测试中,我们经常遇到各种【替代对象】的叫法,mock,stub,fake,dummy,很多时候区分不开到底哪个是哪个,今天这篇文章,我们就来看看这些概念,以及 Test Double 如何区分。
Test Double
- SUT:System Under Test,也就是待测的系统。
注意这个是看站在谁的角度,比如 A 系统需要和 B 系统交互完成一件事,当我们把 B 系统给替换了一个假实现,就是测试 A 的话,这里 A 就是 SUT。
- Test Double
Test doubles are objects that stand in for other objects during a test.
我们通常所说的 mock 其实是一系列统称,事实上这一类【用来代替 SUT 中某个不需要测试的对象】的对象都可以被称为 Test Double。比如我们上面的例子,我此次要测试 A 系统,就这一件事情上看,B 系统具体如何我是不关心的,只要符合规范来返回数据即可,同时为了避免直接依赖,污染数据,所以我需要一个【替代对象】,来作为一个【假的B系统】供 A 系统调用测试,这就是 Test Double。
并且因为有一个我们可以控制的 Test Double,很多场景我们都可以构造出来,这就是所谓的:
Control the behavior of our dependencies.
这一切的根本依据在于:此时此刻,我们需要验证的是,A 系统在遇到 B 系统各种 “符合交互规范” 的响应之后,它该怎么处理的问题。而不是 B 系统是否正常运作。
Test Double 包含了 dummy, fake, mock, stub, spy 五种不同的类型,这里我们引用 Martin Fowler 的经典论述:
- Dummy objects are passed around but never actually used. Usually they are just used to fill parameter lists.
- Fake objects actually have working implementations, but usually take some shortcut which makes them not suitable for production (an in memory database is a good example).
- Stubs provide canned answers to calls made during the test, usually not responding at all to anything outside what's programmed in for the test.
- Spies are stubs that also record some information based on how they were called. One form of this might be an email service that records how many messages it was sent.
- Mocks are what we are talking about here: objects pre-programmed with expectations which form a specification of the calls they are expected to receive.
简单总结一下:
-
Dummy:只是个 placeholder,放在那儿没有实际作用,不会真的包含任何逻辑;
-
Fake:对一些系统进行裁剪之后形成的可运行的一套实现,跟源系统相比有一些(甚至很大)区别,不能上生产环境,但是作为测试使用非常适合,能提前暴露很多问题,例如一个内存版的数据库;
-
Stub:提前预设好一些响应返回,不会跟其他系统有交互;
-
Spy:相当于 Stub 加强版,不仅仅有预先定义的数据,还会记录一些被调用时候的信息(比如每次调用都把数据加到一个队列里)。比如我们针对一个发邮件服务做 spy,调用结束后,需要看下调用了几次,这时候就用到了这份信息;
-
Mock:重点在于 expectation!也就是说,我们对于这次【调用】在遇到各种情况时应该怎么处理,提前指定好规范)。
其中,Dummy 和 Fake 好理解,一个是没啥用,只是占位符,另一个是基本上啥都能干,比真实的系统差点意思,但基本上能覆盖大部分场景。而对于 spy,通常我们不太区分它和 stub,可以一起理解。
那么问题来了,Mock 说的是你要明确你对每次调用的 expectation,需要写代码来指明什么情况下要怎么做,而 Fake 好像也是这个意思,区别在于这个代码可能不用你写(因为开源社区有一些现成可用的)。那么它们根本区别是什么呢?
把握住这三点即可:
-
Fake => working implementations
-
Mock => predefined behavior
-
Stub => predefined values
In state verification you have the object under testing perform a certain operation, after being supplied with all necessary collaborators. When it ends, you examine the state of the object and/or the collaborators, and verify it is the expected one.
In behaviour verification, on the other hand, you specify exactly which methods are to be invoked on the collaboratos by the SUT, thus verifying not that the ending state is correct, but that the sequence of steps performed was correct.
Mock 和 Stub 的区别在于,前者是行为,后者是状态。基于 Mock 做的是 behavior-based verification, 基于 Stub 做的是 State-based verification,这跟验证的方法有关。而 Mock 和 Fake 的区别在于,你在写单测的时候,需不需要构建出一个 working implementation,还是说只要预设一些行为的响应即可。
Fake
好了,有了基础的认知,我们就可以进入正题了,我们常用的 MySQL 和 redis 的内存 mock,本质上就是上面说的 fake,也就是说,一个直接可用的平替。ok,有的同学又要问了,那为啥针对这种存储,我要用 Fake 呢?我直接把整个 Repository 给 Mock 了不行么?
替代对象的意义
行,当然可以。但问题回来了,我们做 Mock 的原因是什么?
-
我们不希望依赖环境,我们希望单测是稳定的。否则一旦你的某个环境,那个真实的存储因为网络原因或磁盘等原因暂时不能服务了,你连单测都跑不了。
-
我们希望单测是快速的,如果你每次跑完一个项目的单测都需要十几分钟,验证一个基础的点都要花很长时间,那么会大大降低人们通过单测来发现问题的动力,所以,起 docker 来跑真实存储也 ok,但最好放到 CI,集成测试的流程,而不是单测。
而这一切的基础是什么呢?是我用替代对象,不影响测试的准确性。因为能被我 mock 的,一定不是这次要测试,要验证的组件。你不可能明明要测试某个下游服务,却还将其 mock 掉,换成一个假实现。
所以,Test Double 存在的意义在于:对于【并非此次要测试的系统】,用一个【替代对象】来简化处理,让我们可以低成本,快速地验证我们需要验证的部分。
Why Fake?
我用 Mock 能不能达到一样的效果?能,但注意,替代对象我们可以控制,但也需要遵循一定的规范,这个规范就是上下游的协议。
举个例子,我希望下游 X 服务给我一个班里学生的数量,接口名字叫做 GetStudentCount,传入一个班号即可,函数签名类似这样: func GetStudentCount(classNo int) int
。
针对这个函数来 mock 一点都不复杂,为什么?根本原因在哪儿?
在于这个入参出参极其简单!如果连这个参数都传错,那就是你开发者自己的低级问题了,单测救不了你。
被我们隐含的复杂性在于:出参入参的协议。这一点在很多场景下都比较简单,所以大家忽略了,但针对 MySQL,Redis 这一类定义了自己的一套 DML 的语言,你不一定真的能按照想法来写出对的语句!
这个时候,谁能来确认你的用法,写法对不对呢?Fake 就可以来帮忙!
Fake 可以提供一套,从交互,协议层面完全一致(或至少90%一致)的 working implementation,你直接用即可,仅限测试,这样能最大程度减少因为用法不对导致的问题。
下一篇文章,我们将会一起来看一看,MySQL 和 Redis 有哪些 Fake 可以用,敬请期待。