## 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: - 測試替身 / 測試副本 ![](https://i.imgur.com/nfHS04C.png) [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 ![](https://i.imgur.com/p64ll2m.png) - 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 ![](https://i.imgur.com/8w76GOh.png) ##### 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` ![](https://i.imgur.com/nCv1bV8.png) --- ### 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 ``` ![](https://i.imgur.com/VT40n4P.png) ``` @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://i.imgur.com/mNl7d5Q.png) ##### 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) ![](https://i.imgur.com/VYnPKlN.png) --- ### 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) ![](https://i.imgur.com/df8WrpQ.png) - 使用`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: ![](https://i.imgur.com/NuwjvRz.png) --- 給新星 :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)
{"metaMigratedAt":"2023-06-17T12:24:20.024Z","metaMigratedFrom":"YAML","title":"Stub vs Mock","breaks":true,"description":"View the slide with \"Slide Mode\".","contributors":"[{\"id\":\"55148774-a793-4ea6-a8ad-d47b88b811b2\",\"add\":22221,\"del\":10234}]"}
    386 views