新手问题 对 stub 和 mock 的理解

williamherry · 2013年05月14日 · 最后由 EvanYa 回复于 2018年05月16日 · 44967 次阅读

一直对 stub 和 mock 不是非常理解,前几天听了TerryaNdReW大侠的指点后感觉对它们的理解深入了许多,从不理解到理解这种恍然大悟的感觉非常爽,这里把自己粗浅的认识记录下来并分享给大家,希望对像我一样的新手有帮助

先从stub说起,什么是stub呢,CodeSchool给出这样的定义:

Stub: For replacing a method with code that returns a specified result

简单说就是你可以用stub去伪造 (fake) 一个方法,阻断对原来方法的调用,例如下面来自CodeSchool的例子

我们有一个叫zombiemodel

class Zombie < ActiveRecord::Base
  has_one :weapon

  def decapitate
    weapon.slice(self, :head)
    self.status = "dead again"
  end
end

我们要测试decapitate方法,它里面调用了weaponslice方法,下面是测试代码:

describe Zombie do
  let(:zombie) { Zombie.create }

  context "#decapitate" do
    it "sets status to dead again" do
      zombie.weapon.stub(:slice)
      zombie.decapitate
      zombie.status.should == "dead again"
    end
  end
end

上面代码的第 6 行就是用stub伪造了weaponslice方法,阻断了对原来方法的调用

你可能会问为什么我们要这样做,这是因为我们在做单元测试,weaponslice可能会非常复杂,里面又调用了其它的方法等等,这是集成测试应该做的工作。事实上这里我们是在测试decapitate方法会把zombie.status设置成"dead again"

接下来我们来说mock, CodeSchool上给的定义是这样的:

Mock:
A stub with an expectations that the method gets called.

简单来说mock就是stub + expectation, 说它是stub是因为它也可以像stub一样伪造方法,阻断对原来方法的调用,expectation是说它不仅伪造了这个方法,它还期望你 (必须) 调用这个方法,如果没有被调用到,这个testfail了,看下面的例子

describe Zombie do
  let(:zombie) { Zombie.create }

  context "#decapitate" do
    it "calls weapon.slice" do
      zombie.weapon.should_receive(:slice)
      zombie.decapitate
    end
  end
end

这里的第 6 行伪造了weaponslice方法,并期望这个方法在这个测试中被调用。

你可能会想为什么要这样写,这是因为我们仅仅是要测试decapitate这个方法确实调用了weapon.slice, 可以把decapitate想成下面的黑盒,我们蹲在图中的 A 点,等着看它会不会去调用weapon.slice

这个图是Sandi MetzRailsConf 2013上的演讲The Magic Tricks of Testing

视频

Slides

这里注意一下顺序,一般的测试先是执行一个动作,然后再去判断状态或其它东西,像前面stub的例子,先调用 decapitate 方法,再去判断status的变化,就好像我踢你一脚,看你会不会喊疼,而这里是先有期望再有动作,这就好比老板对你说这个下周前完成,不然就滚蛋一样

博客地址

期待Terry深入探讨 stub 和 mock 的文章

好像有些 markdown 格式方面的问题,重新格式化一遍吧。

@lgn21st 正在搞,你速度真快

#2 楼 @williamherry 这么快就搞好了,说明你速度也很快。

@lgn21st 我是自己的博客里写好粘过来的,有些 markdown 记法这边好像不支持,像

``` ruby path/to/file.rb sss

#4 楼 @williamherry 的确是这样,我粗略看过 RubyChina 论坛中 Markdown 处理这部分的源码,可能还有很多地方支持的不够周全,有兴趣的话帮助研究完善一下,永远欢迎各种 Pull Request.

Martin Fowler (搞 Java 的应该都知道他)有一篇不错的文章: Mocks Aren't Stubs

@lgn21st 我应该不说出来自己偷偷修说不定还有机会,现在怎么可能轮到我

@donnior 是的,这个 Terry 已经给过,但看起来实在太累了

a small typo 伪造(fade) -> fake.

一般来说,能够直接验证,就不要用 mock,原因很简单:mock 推迟了集成

最后的博客地址 localhost 了。:)

@fsword 我在 rspec 里都是直接 mock 的。不做 stub。使用 expect 的方式做验证。你说推迟了集成测试,可否多说一下。这方面不是很清楚。

@williamherry 楼主的博客地址是 localhost,超越了

#12 楼 @xds2000 mock 掉的那个东西,未来需要再进行集成测试或者说联调,不但增加了测试成本,而且还会给人系统没问题的错觉。 一般来说,所有的问题都是越早发现越好,包括那些推迟到联调阶段的问题,所以才要持续集成,也正因为如此,应该尽可能减少不必要的 mock

#14 楼 @fsword 集成测试本来就要写的啊,难道不写集成测试只在单元测试里面混作一团系统就没问题么……混作一团维护起来成本也不低额……

@reyesyang @xds2000 哈哈,我可以说我是故意这样写看有没人点击吗 😄

#14 楼 @fsword

mock 最大的价值不是在于验证,而是在于推迟协作者的固化,在测试的过程中来推动界面的形成。

#16 楼 @aptx4869

单元,功能和集成测试的取舍是和整个开发过程的方法相关联的;对于本身复杂,零件多的 app 可以考虑详尽的外部驱动集成测试搭配大胆的 mock 驱动的内部零件化。

#16 楼 @aptx4869 #18 楼 @knwang 集成情况比较复杂,webapp 和 js app 的情况只是其中之一,不过,即使是这种场景,也符合一般性规律——mock 推迟了集成。 @knwang所说的“推迟协作者固化”我不是完全理解,这不是协同规约要做的事么?大家约定一个接口,然后各自演化,和是否使用 mock 没有关系

#21 楼 @fsword 是的,简单的协同规约可以一眼看出来,而协调多方工作的好规约的出现往往没那么简单,甚至很多时候是放在这里也可以,那里也行的。用 mock 可以让你在开发的时候集中在你工作的零件上,and mock the collaborators. 在与一个 mocked collaborator 不断协作的过程中,你会在多方面对其加深了解,而用实际的用例来让其公共接口逐渐浮现。当其接口在从多方面被逐渐定性的过程中,你就可以实际写出这个 class 了。

stub 是伪造方法,mock 是伪造对象 https://github.com/rspec/rspec-mocks#mock-objects-and-test-stubs

There is a lot of overlapping nomenclature here, and there are many variations of these patterns (fakes, spies, etc). Keep in mind that most of the time we're talking about method-level concepts that are variations of method stubs and message expectations, and we're applying to them to one generic kind of object: a Test Double.

谁能告诉我这个要怎么解决??? 我已经把 sqlite3.dll 放到 C:\WINDOWS 和 Ruby 的 bin 目录底下了!

@dsmylv windows 的问题很少有人回的哦,你单独发帖试试

图片解释的非常棒,其实两者就是测试思想上的不同,我倾向于 mock,有种是骡子是马拉出来溜溜的感觉,但是感觉 mock 没 stub 好写…

#22 楼 @knwang 赞同!TDD 方式除了验证方法功能正确性以外,我认为更多的优点在于快速帮你设计了各种方法如何实现协同规约的最佳方式。有了 mock 和 stub,具体方法的实现细节不用马上完成都可以模拟了

mock 是用来替代真实对象的测试对象,也被称为测试替身 (test double)。mock 可以替代我们之 前使用预构件或纯 Ruby 创建的对象,但是不改动数据库中的数据,所以速度快一些。

stub 是对指定对象方法的重写,返回一个预设的值。也就是说,stub 虽是个虚假方法,但调用时 会返回一个真实的值供测试使用。stub 经常用来重写方法的默认功能,特别是在频繁操作数据库 或网络密集型交互中。


it "delegates name to the user who created it" do 
  user = double("user", name: "Fake User") #mock
  note = Note.new
  allow(note).to receive(:user).and_return(user) expect(note.user_name).to eq "Fake User" #stub
end

引用自 Everyday Rails Testing with RSpec

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