owned this note
owned this note
Published
Linked with GitHub
---
title: Stub vs Mock
type: slide
tags: Templates, Talk
description: View the slide with "Slide Mode".
---
## U dk Stub & Mock meh?? :face_with_raised_eyebrow:
`Stub` v.s. `Mock`
<!-- Put the link to this slide here so people can follow -->
2022-10-25 Ting :cat:
slide: https://hackmd.io/@tingtinghsu/S1s64axEj#/
---
## :cat: Outline
- 1. Test Double
- 2. Stub
- 3. Mock
- 4. Stub & Mock Conbination
- 5. Usage in codebase
- 6. Discussion - When `mock` can be replaced by `stub`
- Challenges for new stars :star: to read code :computer:
---
## 1. Test Double :dancers:
- 測試替身 / 測試副本

[Ref: 測試中常見的名詞:Stub, Dummy, Mock..等](https://jchu.cc/2018/08/16-test.html)
---
## Why we need Test Doubles? :thinking_face:
---
## Why we need Test Doubles? :thinking_face:
`Fixtures and Fatories`
- [Fixtures](https://gitlab.abagile.com/metis/nerv/-/tree/master/test/fixtures) and [factory](https://github.com/thoughtbot/factory_bot) allow us to get test data into db for testing
`Test Double`
- Sometimes we may not want to place data in db
- Sometimes the test object is hard to create
---
## Benefits of Test Doubles :moneybag:
- Test doubles allow tests to proceed by `faking the method calls` that produce the data (instead of faking data)
- Use `cheaper` or `more focussed replacement` for a more expensive real object :money_with_wings:
---
## 2. Stub Objects :canned_food:
#### test/helpers/model_helper.rb
- stubs - a common helper to testing environments([Martin Fowler - Mocks Aren't Stubs](https://martinfowler.com/articles/mocksArentStubs.html#TheDifferenceBetweenMocksAndStubs))
- To provide `canned answer to calls made` during the test. ([wiki](https://en.wikipedia.org/wiki/Test_stub))
- A fake object that returns a canned value for a method call `without calling the actual method on an actual object.`
---
## Stub: returns canned value

- SUT(System Under Test. 某個待測試的Class)
---
### Let's read Stub in codebase :muscle:
---
## codebase usage: stub (1)
- stub site code
`test/models/master_plan_test.rb#L25`
```ruby
it "should add internal_movement_item in SG" do
Setting.stub(:site_code, 'sg') do
...
end
end
```
---
## codebase usage: stub (2)
- Setting.stub_has?
#### `test/controllers/web/b2b/applications_controller_test.rb#L41`
```ruby
it "should not redirects to new_applications index for JP producers if inquiry_token exist" do
sign_in jp.users.first, scope: :web_producer
Setting.stub_has?({enable_jp_inquiry: true}) do
get :new, params: { inquiry_token: 'morty_c137' }
assert_response :success
end
end
```
##### test/helpers/model_helper.rb#L162 def self.stub_has?(hash)
---
## codebase usage: stub (3-1)
- stub object's association /methods
##### `test/models/ability_web_client_test.rb#L180`
```ruby
it 'should check ability to access jpy cca function' do
refute ability.can?(:web_credit_card_jpy_airwallex, bp), 'Expect not permitted if no global b2c airwallex setting'
Setting.stub_has?({enable_airwallex: true}) do
assert ability.can?(:web_credit_card_jpy_airwallex, bp)
# bp state
bp.stub(:state, 'confirmed') do
assert ability.can?(:web_credit_card_jpy_airwallex, bp), 'Expect confirmed plan can also be seen airwallex'
end
# pay state
bp.stub(:pay_state, 'paying') do
assert ability.can?(:web_credit_card_jpy_airwallex, bp), 'Expect paying plan can also be seen airwallex'
end
end
end
```
---
## codebase usage: stub (3-2)
- stub methods
`test/models/ability_web_client_test.rb#L19`
```ruby
describe "on access Allocation / Switching" do
it 'should allow to read allocation / switching' do
# 還有一個type,是PlanInfo::ADVISORY_PORTFOLIO
[PlanInfo::ADVISORY_NONE, PlanInfo::ADVISORY_BASIS_ONLY].each do |advisory_service_type|
bp.plan_info.stub(:advisory_service_type, advisory_service_type) do
assert ability.can?(:read_allocation, bp)
assert ability.can?(:read_switching, bp)
end
end
end
end
```
---
## codebase usage: stub (3-3)
- stub_any_instance
#### `test/forms/service_record_item/reduce_contribution_form_test.rb#L33`
```ruby
it 'should validate amount' do
stub_any_instance(BasicPlan, :all_icp_contribution_paid?, true) do
assert form.valid?
form.reduced_contribution = amount_great_than_original
refute form.valid?
end
end
```
##### test/helpers/model_helper.rb#L73 def stub_any_instance
---
## codebase usage: stub (4)
- begin_stub_class_method
#### `test/forms/service_record_item/add_single_topup_form_test.rb#L56`
```ruby
describe "Process" do
let(:fixture) { File.join(Rails.root, "test/fixtures/testing.pdf") }
before { begin_stub_class_method(CertifiedDocumentValidator, :validate, true) }
after { restore_stub_class_method(CertifiedDocumentValidator, :validate) }
it 'must add single topup' do
bp.update_columns(no_of_applicants: 4)
required_docs.each do |type|
sr.scan_docs.new(doc_type: type, doc: File.open(fixture))
end
assert_difference('bp.contribution_records.count') do
form.validate(params)
form.save!
end
topup = bp.reload.top_ups.last
assert_equal 'confirmed', topup.state
...
end
end
```
##### test/helpers/model_helper.rb#L169 def self.begin_stub_has?(hash)
---
## 3. Mock Objects
- `return the fake value` & set a testable expection that `the replaced method will actually be called` in the test

##### DOC (Depended-On Component) :想抽換掉的相依性元件
---
## stub method in [Minitest](https://github.com/minitest/minitest/blob/master/lib/minitest/mock.rb)
- All mock objects are an instance of Mock
```
let(:mock) { MiniTest::Mock.new }
```
- it `overrides` a single method for the duration of the block
- we want to `make sure we called the replaced method`, but we don’t care to test if it works or not (because it’s already been tested)
- If the replaced method is not called, the mock object `triggers a test failure`
---
### Mock usage (1) - name & retval
##### https://github.com/minitest/minitest/blob/master/lib/minitest/mock.rbL56
- `mock.expect(:call, "expect return value when the method_name is called")`
```
@mock.expect(:meaning_of_life, "make every day count")
@mock.meaning_of_life #=> "make every day count"
```
---
### Mock usage (2) - [args]
- `mock.expect(:call, "expect return value when the method_name is called", [丟入的參數])`
```ruby
@mock.expect(:do_something_with, true, [some_obj, true])
@mock.do_something_with(some_obj, true) #=> true
```
---
### Mock usage (3) `do block`
##### 後接block的方式指定參數
```ruby
@mock.expect(:do_something_else, true) do |a1, a2|
a1 == "parrots" && a2 == "dogs"
end
```
##### `https://github.com/minitest/minitest/blob/master/lib/minitest/mock.rb#L91`

---
### Mock usage (4) `+args+`
##### `+args+` is compared to the expected args using case quality (`===`), allowing for less specific expectation.
```ruby
@mock.expect(:uses_any_string, true, [String])
@mock.uses_any_string("foo") # => true
@mock.verify # => true
@mock.uses_any_string("bar") # => true
@mock.verify # => true
```

```
@mock.expect(:uses_one_string, true, ["foo"])
@mock.uses_one_string("bar") # => raises MockExpectationError
```
##### https://github.com/minitest/minitest/blob/master/lib/minitest/mock.rb#L199
---
### Mock usage (5) `when a method is called multiple times`
##### Specify a new expect for each one. They will be used in the order you define them
```ruby
@mock.expect(:ordinal_increment, 'first')
@mock.expect(:ordinal_increment, 'second')
@mock.ordinal_increment # => 'first'
@mock.ordinal_increment # => 'second'
@mock.ordinal_increment # => raises MockExpectationError
"No more expects available for :ordinal_increment"
```

##### https://github.com/minitest/minitest/blob/master/lib/minitest/mock.rb#L163
---
## 4. Mock & Stub combination
[Explanation from John dada](https://abagile.slack.com/archives/C0RDD9J8L/p1594720011276800)

---
### Let's read Mock in codebase :muscle:
---
### Mock in codebase (1)
- `mock.expect(:call, "expect return value when the method_name is called")`
##### test/forms/service_record_item/cancel_contribution_holiday_form_test.rb#L45 vs. 實作L15
```ruby
describe 'on save' do
it "must turn status of selected crs back to unsettled and pay_state back to paying" do
cancel_month_list = [1, 2, 3]
mock.expect(:call, true)
stub_any_instance(BasicPlan, :generate_contribution_records!, mock) do
form.assign_attributes(cancel_month_list: cancel_month_list).save!
end
assert mock.verify
canceled_crs = bp.reload.contribution_records.where(contribute_month: cancel_month_list)
assert canceled_crs.all?(&:unsettled?)
assert canceled_crs.all{ |cr| cr.sub_status.nil? }
assert bp.paying?
end
```
---
#### Mock usage (2) - [args]
#### `mock.expect(:call, "expect return value when the method_name is called", [丟入的參數])`
##### test/forms/service_record_item/pending_payment_info_form/cc_aw_test.rb#L24 vs. 實作L33
```ruby
describe 'On Save' do
it 'should create a pending payment info' do
...
mock.expect(:call, 'CC-AW', [{is_airwallex: true, card_option: nil, submit_on: nil}])
PaymentInfo::PayProfileCode.stub(:cc_init_val, mock) do
assert form.save
end
bp.reload.pending_payment_info.reload.tap do |ppi|
assert_equal contribution, ppi.pending_contribution
assert_equal 'CC-AW', ppi.pay_profile_code
end
mock.verify
end
end
```
---
#### Mock usage (3) - 後接block 指定參數
##### test/forms/service_record_item/adjust_override_due_on_form_test.rb#L69 vs. 實作L32
```ruby
describe 'save' do
describe ' - add new override contribute due on case' do
it 'updates selected contribution records override contribute due on date' do
form = klass.new(bp)
form.assign_attributes(content_set_override)
mock.expect(:call, true) do |arg|
arg == {
bp_id: bp.id,
from_month: 2,
to_month: 6,
date: content_set_override[:amendment_date]
}
end
Interactor::AdjustOverrideDueOn.stub(:call, mock) do
assert form.save!
end
assert mock.verify
# check after_image (for sr content equals to form.attributes)
form.attributes.tap do |image|
assert_equal 2, image[:from_contribute_month]
assert_equal 6, image[:to_contribute_month]
assert_equal Time.zone.today.advance(months: 1), image[:amendment_date]
assert_equal false, image[:is_cancel]
end
end
end
```
---
### Mock usage (4) - `+args+`
##### test/forms/service_record_item/personal_data_change_form_test.rb#L453 v.s. 實作 app/repositories/internal/client_repo.rb#L120 vs. test/repositories/internal/client_repo_test.rb#L141
```ruby!
it 'validates if id_no is used on existing client' do
...
mock.expect(:call, true, [Hash])
Internal::ClientRepo.stub(:duplicated_client?, mock) do
form = klass.new(new_bp)
form.assign_attributes(params)
refute form.valid?
assert_equal ["client with this id_no already exist"], form.plan_owners[0].errors[:id_no]
end
mock.verify
end
```
---
### Mock usage (5) `mock called multiple times`
##### test/services/change_initial_contribution_service_test.rb#L212 vs. 實作L90
```ruby!
it 'must fail if have additional_prepay but cannot prepay' do
initial_contribution_record_count = 1
2.times { mock.expect(:call, false) }
stub_any_instance(BasicPlan, :initial_contribution_record_count, initial_contribution_record_count) do
stub_any_instance(BasicPlan, :is_prepaid_contribution, mock) do
stub_any_instance(BasicPlan, :pay_all_icp_with_prepay?, mock) do
bp.assign_attributes(params)
ex = assert_raises(RuntimeError) { klass.regenerate_initial_contribution_record!(2) }
assert_equal "Valid regenerate contribution record count is #{initial_contribution_record_count}", ex.message
end
end
end
mock.verify
end
```
##### 如果沒有call正確的次數, 會raises MockExpectationError
---
### discussion time:
When `mock` can be replaced by `stub` :thinking_face:
```ruby!
# send message(exists?) 給BasicPlan(receiver)
BasicPlan.exists?
# 把BasicPlan(receiver)吃到message的反應stub成true
BasicPlan.stub(:exists?, true)
do_something
end
# 把BasicPlan(receiver)吃到message的反應換成mock
#mock也是在改receiver(exists?)吃到:call時的回應,mock成 true
mock.expect(:call, true)
BasicPlan.stub(:exists?, mock)
do_something
end
```
---
### When `mock` is better than `stub` :thinking_face:
[Slack Discussion in Feb. 2019](https://abagile.slack.com/archives/C0RDD9J8L/p1550731758092300)

- 使用`Mock` 時會強調`驗證傳入的參數`
---
### Wrap up :100:
- `Stub` on a method is `passive`
- Ignore the real implementation of a method and return canned value
- `Mock` on a mthod is `aggressive`
- This method will return this value. You'd better call the method :smirk: or else.. :skull:

---
給新星 :star: 的小建議
- 多寫測試 :keyboard:
- 查GitHub文件 :page_with_curl:
- slack討論串挖寶 :file_cabinet:
- 比較codebase 各種 stub/ Mock用法,想想有沒有其他更好的方式 :thinking_face: 並與大大們討論
- 多Google,多讀幾遍 ⬇️ reference
---
### Thank you! :sheep:
Ref:
- [Minitest GitHub](https://github.com/minitest/minitest/blob/master/lib/minitest/mock.rb)
- [Martin Fowler -Mocks Aren't Stubs](https://martinfowler.com/articles/mocksArentStubs.html)
- [mocking in ruby with minitest](https://semaphoreci.com/community/tutorials/mocking-in-ruby-with-minitest#h-stubbing)
- [測試中常見的名詞:Stub, Dummy, Mock..等](https://jchu.cc/2018/08/16-test.html)