Ruby 各位在写测试的时候会常用 mock 吗?

mizuhashi · 2022年10月21日 · 最后由 lehug 回复于 2022年10月23日 · 599 次阅读

有位同事特别喜欢用 mock,测试一个 service 的时候会把所有外部依赖都用 mock 写,以此隔离测试代码。他的理由有,service A 依赖 service B,在 A 的单测中如果直接调用 B 来准备测试数据,那么 B 在后面的时候可能会不断增长,然后引入很多和 A 无关的东西,也可能 break A 的测试。而如果用 mock 的话,A 的测试是不需要改的,写测试的时候关注点也只会在 A 内部。但是我认为如果这么干,会需要一些额外的代码来测 A 对 B 的依赖,不然不能保证 B 的运行结果和最初的 mock 数据是一样的,这实际上增加了测试的代码量,而且测试本来应该是为了发现问题,而不是作为“不会有问题”的代码,如果用维护业务代码的方式维护测试,似乎就失去了测试的意义。

我想了一种折中方案,就是通过实际调用 service 来准备数据,然后把这些数据 dump 一份,保存来用作 mock,这样 service 改变这个 dump 也不会变,可以很好地满足他想要隔离测试的想法。然后可以监视一个 service 生成的新的 dump 和以往的 dump 是不是有不同,如果有不同,就更新一下 dump 的版本看看单元测试还能不能过。这样如果 dump 更新得勤,就能实现我想要的发现问题。这样好像是不错的,可以实际观测到 service 改动所产生的效应,不过可能需要造一些轮子。

我想了一种折中方案,就是通过实际调用 service 来准备数据,然后把这些数据 dump 一份,保存来用作 mock,这样 service 改变这个 dump 也不会变,可以很好地满足他想要隔离测试的想法。

https://github.com/vcr/vcr

piecehealth 回复

看起来不错,这个应该可以参考,不过我需要的更多是关于数据库的数据,例如一些测试用例的数据需要一串 service class 调用才能生成(用户做了什么操作),和 service 的耦合是在这里引入的。我需要把这些操作的数据库结果集存起来(相当于 factory),然后在跑测试之前重放,如何进行 diff 可能也是个问题

ruby 不需要,直接 hack 实例的方法

你同事那种,叫单元测试,只保证自身的稳定性。外部依赖输入变了,那是外部的问题。用外部实际吐的数据 mock,也一样算单元测试。 你那种连 B 一起测试的方法,叫集成测试,保证的是 A,B 集成没有问题。 两种测试方法覆盖的目的是不一样的。单元测试要保证自身逻辑没有问题,所以覆盖面会很细,测试也需要很稳定,A 的功能没有变测试就应该能通过。集成测试则是全面的保证实际对外功能的正确性,受环境影响的随机因素比较大。比如 B 的输出数据变了,对应的测试 case 就可能挂,但其实都是正常的行为。因此集成测试的粒度就要粗不少,只测一些稳定的,关键的行为。

我挺喜欢用 mock 的,因为感觉 mock 后,关注点可以集中在想要测试的某个类里面,不需要关注这个类和其他类的交互

感觉一个更关注 单元测试; 另一个更关注 集成测试;

我之前听 郑晔 在极客时间的某个专栏文章讲到 "行业里的最佳实践是 测试金字塔"(我不知道是从哪里来的信息), 我是按照这个来作参考的

测试应该怎么配比

测试本来应该是为了发现问题,而不是作为“不会有问题”的代码

赞同这个,这个的解法他也有提到,就是尽量把测试写得简单 (简单到能很容易看出问题), 除此外好像没有别的好办法

@316786359 @lijunwei 谢谢,我大概能理解这两种测试的区别,不过一个问题是集成测试应该怎么写,如果单元测试已经覆盖了对内的所有东西,那么集成测试只应该测试不同单元之间的调用,也就是如果 A 单元调用了 B,只要确保 A 发出了调用的信息,并且调用信息和 B 单测里期望的调用结构是一致的,别的什么都不需要关心。这样的话单元测试和集成测试覆盖的东西才没有交集,如果能做到这样应该是很好的。

我的案例应该不适用单元测试和集成测试“覆盖面”的差异,举个例子,一个测试是写关于顾客购买了的商品,那么在准备测试数据的时候,自然的想法是PurchaseService.call(client, product),然后数据库里就有这个用户买完东西的状态了。但是我同事会说,这里不应该调用 PurchaseService 因为它创建的东西不完全是测试需要的,应该用 factory 自己构造一个购买后的状态。那么在这里用 service 还是用 factory 构造测试数据其实和覆盖面是没关系的,用两种方式该覆盖的都不会少。我认为如果以测试全部跑通为基准,显然用 service 能发现比用 factory 更多的问题,因为如果 factory 都会挂那么 service 也一定会挂,但是反之不然,用 service 挂了可能是 service 的问题,用 factory 是不会挂的。如果以测试跑通的比例为基准,那么用 factory 是更好的,因为有相当一部分测试可能不会挂了,但是这是以维护多一套 factory 为代价的,而且实际上挂的事实没有改变。因此我觉得“隔离测试以保护其不挂/不变”意义不大,可能对于足够大的项目,测试跑通的比例对于他们的规划/定位问题是有意义的,但是对于我们的小项目就没意义了。这里可能有一种容错和速错的理念区别

通过网络调用外部服务的时候会用 mock。 程序内部调用不会 mock。

单侧就是要屏蔽所有的外部依赖。 集成测试其实也可以分好几层,看集成的程度。比如集成所有的内部可控组建,但是一般外部服务还是集成不了,必须 mock 的。 单侧有必要,TDD/BDD 就是靠的单侧。 集成测试也有必要,因为是更完整的产品质量的保证。 但是团队咋选择?领导说了算。

单侧和集侧的价值都是发现问题,保证质量。单侧更多是在开发阶段,各种 case 都能验证。你偏向的集侧更能保证交付质量。但是集侧你把所有 case 都覆盖,那难度就更大了。你的立场很明确了,你是以最终交付质量来评判的测试代码的作用,那么你的结果却是是要集成测试。

需要 登录 后方可回复, 如果你还没有账号请 注册新账号