该文章同步发布於:我的部落格
在我一开始学习写 Rails 测试时,会有很常见的问题,就是到底什麽是 mocks & stubs,然後我要怎麽使用它们?
如果你对 mocks & stubs 也觉得很困惑,其实真的很正常,所以我自己也看了蛮多不同文章对於对於 mocks & stubs 的见解,希望今天可以厘清这个问题,特别是在 Rails/RSpec 的背景下。
昨天的文章也有提到,测试术语可能有一百万种,但对於含义却没有一个完全的共识。像是昨天提到的 end-to-end 以及 验收测试是同一回事吗?有些人是,有些人不是。而且也没有一个中央权威机构说什麽是对的,什麽是错的,这就是测试术语上会遇到的问题。
尤其在 mock & stub 上,有些人说 mock 就是 stub,反之。
这样的结果就是,关於 mock & stub 的解释都变得非常的混乱!
我在网路上看过 Gerard Meszaros 的 xUnit Test Patterns: Refactoring Test Code
这本书时提过,mocks & stubs 都是 double 的特殊类型时,我好像懂了些什麽。
这种理解对我来说就比较不容易被动摇,因为理解过 double
能够让我做模式比对,所以我知道这不太可能再以後被我看到的新知识给推翻,感觉就像是经验值真正的提升了一点!
所以什麽是 double ? 在之前我只提过她它在 RSpec 的用法,在书中,作者把 double 比喻成好莱坞的特技替身。
当电影想要拍摄一些对主角来说有风险的事情时,他们会雇用一个 特技替身
来代替演员在场景中的动作。特技替身是一个受过严格训练的人,能够满足画面的需求,他们可能不会演戏,但他们知道怎麽从高处坠落、撞车等等。特技替身通常要在身材和某些角度和演员很相似!
等等的文章会用第三方支付当作例子。当我们在测试第三方支付这件事情时,不会真的希望它每测试一次就收一次钱吧?我们希望用替身来代替真正的支付流程。
我在网路上有看到 这篇文章 是一个叫做 Michal Lipski 的人写的。
根据他的文章,我也有了自己的解释方式。
Test stub 是一个假的对象,用来代替一个真正的对象,目的是让程序码可以按照我们需要的方式来进行测试。Stub 有很大一部分工作是在我们原本的程序码被呼叫时直接调用。
你也可以看看 Gerard Meszaros 的 xUnit Test Patterns: Refactoring Test Code
关於 Test stub 的讲法,可以获得更详细和精准的解释,我可能没有办法讲得很好,因为我的英文没有好到可以翻译的很准确。
我没办法告诉你怎麽样可以去看到这本书,但如果是我可能会先上网 google 之後在买书。
至於什麽时候要用 Stub,等等就会看到了!
Mock 也是一个假的对象,用来代替一个真实的对象,目的是为了监听这个 Mock 上面被呼叫的方法。Mock 对象的主要工作是确保正确的方法被调用到他身上。
这和 Stub 明显有一点点不一样,但如果想更理解的话,去看书吧!
等等也会使用到 Mock!
根据我的理解,用一个广泛的说法,Stub 帮忙输入, Mock 帮忙输出。
Stub 是你做一个假的东西,直接插在你的程序码里面,骗他这是真的。
Mock 是你做一个假的东西,放在旁边,在你程序码不能直接测试的地方监视并且替代。
希望这样简单的说法能够让人比较快理解,但我相信这不是准确的答案,还是看看例子比较实际!
下面是一个 Ruby 的例子,让这个例子能够满足 Mock & Stub。
我知道这例子可能漏洞百出,但我是为了示范 Mock & Stub 而写的~
以下会有三个类别:
class Payment
attr_accessor :total_amount
def initialize(ecpay_key, logger)
@ecpay_key = ecpay_key
@logger = logger
end
def save
response = @ecpay_key.charge(total_amount)
@logger.record_payment(response[:payment_id])
end
end
class ECpayKey
def charge(total_amount)
p "这会触发真正的付款"
{ payment_id: rand(1000) }
end
end
class Logger
def record_payment(payment_id)
p "Payment id: #{payment_id}"
end
end
RSpec.describe Payment do
it 'records the payment' do
ecpay_key = ECpayKey.new
logger = Logger.new
payment = Payment.new(ecpay_key, logger)
payment.total_amount = 1800
payment.save
end
end
我们的最下方的测试有两个问题,他改变了资料( 这边没有真的写出来,你可以假装 这会触发真正的付款
那段是真的向某人收了钱 )
另一个问题是,这个测试没有验证付款记录有没有被记录下来,就算我把 @logger.record_payment(response[:payment_id])
给注解掉,测试还是会通过。
这会触发真正的付款
Payment id: 334
.
Finished in 0.00255 seconds (files took 0.09594 seconds to load)
1 example, 0 failures
首先我们先用 Stub 来取代 ECpayKey 这件事情。
下面我把 ecpay_key = ECpayKey.new
改成 ecpay_key = double()
。
而且我告诉他,他会期望收到一个 charge
的方法,当它收到时,回传一个 3333
的 ID
RSpec.describe Payment do
it 'records the payment' do
ecpay_key = double()
allow(ecpay_key).to receive(:charge).and_return(payment_id: 3333)
logger = Logger.new
payment = Payment.new(payment_gateway, logger)
payment.total_cents = 1800
payment.save
end
end
这时候我们在 Run 一次测试,就不会再看到 这会触发真正的付款
这样的讯息,这是因为我们的测试没有去 call ECpayKey 的 charge
方法,而是 call 了 Stub 物件身上的方法。
这时候我们的 ecpay_key
也不再是 ECpayKey 的实体,而是 RSpec::Mocks::Double
的一个实体了!
Payment id: 3333
.
Finished in 0.00877 seconds (files took 0.09877 seconds to load)
1 example, 0 failures
现在让我补上 纪录支付资讯 的测试,我们把 logger = Logger.new
换成 logger = double()
。
RSpec 本身没有区分 mocks 以及 stubs 的分别。他们都是 Test Doubles,所以我们要怎麽区分,RSpec 让我们自己决定~
我们还告诉了 Mock 物件,他必须接受一个值为 3333
的 record_payment 的方法!
RSpec.describe Payment do
it 'records the payment' do
ecpay_key = double()
allow(ecpay_key).to receive(:charge).and_return(payment_id: 3333)
logger = double()
expect(logger).to receive(:record_payment).with(3333)
payment = Payment.new(payment_gateway, logger)
payment.total_cents = 1800
payment.save
end
end
为了验证 Mock 物件有没有在监视这段程序码,我们把 @logger.record_payment(response[:payment_id])
给注解掉,就会看到以下的错误讯息!
Failures:
1) Payment records the payment
Failure/Error: expect(logger).to receive(:record_payment).with(3333)
(Double (anonymous)).record_payment(3333)
expected: 1 time with arguments: (3333)
received: 0 times
事实证明没有错,他确实有去监视是不是真的有执行这行程序码,和 Stub 直接插入不一样的做法!
但讲了这麽多,我其实也真的没有在测试中写过几次的 Mock & Stub。
主要原因是因为也不是常常遇到只需要 Mock & Stub 就能解决的问题。在 Rails 中我们并没有做到真正的 Unit Test,因为我们没有真正的把一个物件和其他物件完全隔离开来进行测试,至少我现在还没遇过,取而代之的是,我们写的 Model spec 会去触发资料库,并且能够访问到应用程序里面的每一个物件,这样做是好是坏我不清楚,但这样的方式基本上消除了对於 Mock & Stub 的需求。
另一个原因是,我自己觉得 Mock & Stub 的测试基本上就是重述了被测试的物件。像刚刚的程序码 @logger.record_payment
,测试写 expect(logger).to receive(:record_payment)
。
看起来真的有种我到底写了什麽的感觉,这很像是在测试期待,而不是测试结果,但可以的话,我宁愿只测试结果,而且在 Rails 里面,大部分都可以测试结果,而不是测试期待~
<<: Day27 [实作] 一对一视讯通话(7): 使用 Docker 封装
先说明:今天加班到现在,所以先写一点点,後续再补上。 前天的文章有一位读者提问: 如果将团队在组织中...
What is the HTML DOM? "The HTML DOM is a stan...
filter 眼尖的小光在昨日的内容中看到了一个有趣的东西,就是MiddlewareFilter,所...
那天跟朋友聊天,我跟他说我的同学是打程序的所以薪水很高。他很不解,什麽叫做打程序的?我就说就资料探勘...
前言 本文说明如何进行订阅盘中交易作业。 程序实作 选择订阅标的 # 取得长荣股票报价,长荣代号:2...