Stub
v.s. Mock
2022-10-25 Ting
mock
can be replaced by stub
Challenges for new stars to read code
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
stubs - a common helper to testing environments(Martin Fowler - Mocks Aren't Stubs)
To provide canned answer to calls made
during the test. (wiki)
A fake object that returns a canned value for a method call without calling the actual method on an actual object.
test/models/master_plan_test.rb#L25
it "should add internal_movement_item in SG" do
Setting.stub(:site_code, 'sg') do
...
end
end
test/controllers/web/b2b/applications_controller_test.rb#L41
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/models/ability_web_client_test.rb#L180
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
test/models/ability_web_client_test.rb#L19
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
test/forms/service_record_item/reduce_contribution_form_test.rb#L33
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/forms/service_record_item/add_single_topup_form_test.rb#L56
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
return the fake value
& set a testable expection that the replaced method will actually be called
in the testlet(: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.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.expect(:call, "expect return value when the method_name is called", [丟入的參數])
@mock.expect(:do_something_with, true, [some_obj, true])
@mock.do_something_with(some_obj, true) #=> true
do block
@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
+args+
+args+
is compared to the expected args using case quality (===
), allowing for less specific expectation.@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
when a method is called multiple times
@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"
mock.expect(:call, "expect return value when the method_name is called")
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.expect(:call, "expect return value when the method_name is called", [丟入的參數])
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
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
+args+
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 called multiple times
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
When mock
can be replaced by stub
# 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
Stub
on a method is passive
Mock
on a mthod is aggressive
給新星 的小建議
多寫測試
查GitHub文件
slack討論串挖寶
比較codebase 各種 stub/ Mock用法,想想有沒有其他更好的方式 並與大大們討論
多Google,多讀幾遍 ⬇️ reference