Day 19 魁儡的 double object

该文章同步发布於:我的部落格

昨天结束了 Matcher 的介绍,今天开始进入 mock 的篇章。

还记得一开始提到的 unit test,我们希望着重在小的功能上进行测试,但一个 App 常常牵扯到各式各样不同的关联。

这时候的 unit test 就会变得很难写,因为你需要顾虑到许多不同的方法所回传的值,因为他们都会影响到你测试的结果!

这时候 mock 系列的功能就可以很有效地帮助到你,因为他就是为了假装、为了模仿而生!

Double method

从概念上来说,double 是一个制造假物件的方法,而这个假物件进而能够接受方法,设定好回传值。

我们先看看范例,再来解释:

RSpec.describe "double method" do
  it "can defined method to be invoked" do
    basketball_player = double("Lebron James", dunk: "Ah!!!!", shoot: "Goal!!!!")
    expect(basketball_player.dunk).to eq("Ah!!!!")
  end
end

我们 double 出了一个篮球员,可以灌篮然後发出 Ah!!! 的声音~

上面这段测试是成功通过的!那到底是在做什麽呢?

double 可以让我们预期的方法和回传的值,变成key-value 的组合,然後存放在这个 double 物件中。

接着他一样可以使用他身上的方法,然後得到预设好的回传值~

我们也可以用 allow 的写法,看起来会更直观一些,虽然比较长。

RSpec.describe "double method" do
  it "can defined method to be invoked" do
    basketball_player = double("Lebron James")
    allow(basketball_player).to receive(:dunk).and_return("Ah!!!!")
    expect(basketball_player.dunk).to eq("Ah!!!!")
  end
end

我们允许 basketball_player 物件接收一个 dunk方法,然後回传 Ah!!!!!

所以这个测试也是理所当然的会成功通过!

但这样一次写一个会不会太麻烦了?而且我又不想要写在初始化里面很乱...

还有另一个叫做 receive_messages 的方法喔!

RSpec.describe "double method" do
  it "can defined method to be invoked" do
    basketball_player = double("Lebron James")
    allow(basketball_player).to receive_messages(dunk: "Ah!!!!", shoot: "Goal!!!!")
    expect(basketball_player.dunk).to eq("Ah!!!!")
    expect(basketball_player.shoot).to eq("Goal!!!!")
  end
end

其实效果等同你在初始化的时候,直接写在後面是一样的道理,但我自己比较喜欢看 allow 的方法,会让人比较理解的感觉~

但其实上面的三种方式都可以,都只是建立 double 物件的手段~

而或许你还是觉得,所以这能干嘛?反正怎麽测都可以过,有意义吗?

当然不是叫你针对你要测试的物件做 double,这样钱也太好赚了吧 ?

而是我们要被不同的类别所牵扯,导致我们需要 double 的帮忙。

接下来就会用范例来看看,什麽叫做被别的类别给影响,导致你需要使用 double 的情形~

class Cowboy
  def initialize(name)
    @name = name
  end
  
  def fighting?
    true
  end
  
  def draw_the_gun
    "Bang!!!"
  end
  
  def be_shot
    "Help me..."
  end
  
  def continue?
    false
  end
end


class Bar
  attr_reader :cow_boy
  
  def initialize(cow_boy)
    @cow_boy = cow_boy
  end
  
  def start_fighting
    if cow_boy.fighting?
      cow_boy.draw_the_gun
      cow_boy.be_shot
      cow_boy.continue?
    end
  end
end

从上面这个简单的西部牛仔剧情片编织的类别,可以看到酒吧以及牛仔之间的关系!

但当我们今天要写 Bar 这个类别的测试时怎麽办呢?

我们要测试 start_fighting 这个方法时,可以看到被 cow_boy 给影响到了!

但其实我们根本可以不用关心 cow_boy 是怎麽作业的?他的方法回传什麽那都不重要,我们要专注在现在这个方法应该要怎麽通过才对。

他的流程是不是正确的?这才是我们在乎的地方~

所以这就是前面提到,被影响到的时候,我们应该要用 double 来取代这边的 cow_boy 然後使我们的测试可以通过!

当然这是很搞笑的介绍,但事实上的专案中,就是会有许多不同的关系的存在,也都会影响到整个方法的进行。

接着我们来用 double 测试这个 Bar 类别的实体方法吧!

RSpec.describe Bar do
  let(:cow_boy) { double("Gene Autry", fighting?: true, draw_the_gun: "Bang!!!", be_shot: "Help me...", continue?: true) }
  subject { described_class.new(cow_boy) }
  
  describe "#start_fighting method" do
    it "expect cow_boy start_fight" do
      expect(cow_boy).to receive(:fighting?)
      expect(cow_boy).to receive(:draw_the_gun)
      expect(cow_boy).to receive(:be_shot)
      expect(cow_boy).to receive(:continue?)
      subject.start_fighting
    end
  end
end

这是会成功通过的测试,因为我比较懒一点直接写在初始化里面 请见谅~

然後我们在测试中期待这个物件会接收到这些方法~

但如果我们不给予这些方法的话,这个方法是会没办法成功运作的喔!

因为我们把 double 物件放进去 Bar 类别生成实体,进而执行他的 start_fighting 这个方法,其中有使用到的方法都会先去参考 double 物件有没有才继续向下执行!

可以看看如果我们只写了 double 物件的话:

RSpec.describe Bar do
  let(:cow_boy) { double("Gene Autry") }
  subject { described_class.new(cow_boy) }
  
  describe "#start_fighting method" do
    it "expect cow_boy start_fight" do
      subject.start_fighting
    end
  end
end

再看看执行的 Output !

程序的执行在 if cow_boy.fighting? 中断了,内容也有提到我们的 double 物件没有接受到这个方法~

所以我们可以用最上面介绍的三种写法来填入方法,使我们可以专注在测试 Bar 的方法,而不受到 cow_boy 干扰!

而如果想要指定接收次数的话,也可以加入计数的方法喔!

毕竟某些时候你会想要限制有些方法只执行一次,或是最多两次等等的条件~

describe "#start_fighting method" do
  it "expect cow_boy start_fight" do
    expect(cow_boy).to receive(:fighting?)
    expect(cow_boy).to receive(:draw_the_gun)
    expect(cow_boy).to receive(:be_shot).twice
   #expect(cow_boy).to receive(:be_shot).exactly(1).times
   #expect(cow_boy).to receive(:be_shot).at_most(1).times
    expect(cow_boy).to receive(:continue?)
    subject.start_fighting
  end
end

上面有注解的部分,都是可以用这样的写法!

现在我们加上了 twice 就是希望这个方法能被执行两次,那我们先来看看 Output

这边的意思就是,我们期待会收到两次的执行,但其实程序码只有一次!

那我们先改写要测的的方法,让牛仔中枪两次吧

def start_fighting
  if cow_boy.fighting?
    cow_boy.draw_the_gun
    cow_boy.be_shot
    cow_boy.be_shot
    cow_boy.continue?
  end
end

这样就能成功通过测试啦~但其实你不写任何次数的话也是会通过啦!

但既然是写测试,方法会执行几次,或是接收什麽样的参数,都是需要考量和注意的!

小结

今天介绍了使用 double 的情境,以及为什麽要使用它~

就是希望可以让我们的工作更顺利,不要在一些奇怪的事情上烦恼,有时候被其他的物件干扰导致测试写不出来,真的是超级痛苦的!

明天会稍微介绍一下今天有提到的 allow method 然後进入 instance double 的世界!

这是一个更真实更好用的东西!


<<:  DAY18 专案进度按钮功能实现-2

>>:  Day 19 BeautifulSoup模组一

Day23 Android - RxJava+Post

那麽今天主要要用RxJava来结合retrofit做Post的部分,与上次用Retrofit的cal...

Day17-D3 的 Scale( ) 比例尺

本篇大纲:Domain & Range 输入域与输出域、Interpolate 插补值、c...

Day17 Combine 04 - Operators 主要类型

Operators 操作符是Combine 中非常重要的一部分,通过各式各样的操作符,可以将原来各自...

Day 17. 常见模板 Template DB MySQL by Zabbix agent 介绍

Hi 大家今天要跟大家介绍 DB 样板,针对 MySQL 服务。 我们主要的服务都是基本上都是 LA...

[Day12] 策略最佳化模组改造(2)

现在要来处理上一篇文章的红框部分,输入N个np.arange让他跑for loop。今天在网路上看了...