changed 2 years ago
Published Linked with GitHub

U dk Stub & Mock meh??
Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

Stub v.s. Mock

2022-10-25 Ting

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

slide: https://hackmd.io/@tingtinghsu/S1s64axEj#/


Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
Outline

    1. Test Double
    1. Stub
    1. Mock
    1. Stub & Mock Conbination
    1. Usage in codebase
    1. Discussion - When mock can be replaced by stub
  • Challenges for new stars

    Image Not Showing Possible Reasons
    • The image file may be corrupted
    • The server hosting the image is unavailable
    • The image path is incorrect
    • The image format is not supported
    Learn More →
    to read code
    Image Not Showing Possible Reasons
    • The image file may be corrupted
    • The server hosting the image is unavailable
    • The image path is incorrect
    • The image format is not supported
    Learn More →


1. Test Double
Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

  • 測試替身 / 測試副本

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

Ref: 測試中常見的名詞:Stub, Dummy, Mock..等


Why we need Test Doubles?
Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →


Why we need Test Doubles?
Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

Fixtures and Fatories

Test Double

  • Sometimes we may not want to place data in db
  • Sometimes the test object is hard to create

Benefits of Test Doubles
Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

  • 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

    Image Not Showing Possible Reasons
    • The image file may be corrupted
    • The server hosting the image is unavailable
    • The image path is incorrect
    • The image format is not supported
    Learn More →


2. Stub Objects
Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

test/helpers/model_helper.rb

  • 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.


Stub: returns canned value

  • SUT(System Under Test. 某個待測試的Class)

Let's read Stub in codebase
Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →


codebase usage: stub (1)

  • stub site code

test/models/master_plan_test.rb#L25

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

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
    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

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

    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

  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

  • 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", [丟入的參數])
@mock.expect(:do_something_with, true, [some_obj, true])
@mock.do_something_with(some_obj, true) #=> true

Mock usage (3) do block

後接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


Mock usage (4) +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
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
@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


Let's read Mock in codebase
Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →


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
  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
  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
  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
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
      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

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

# 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
Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

Slack Discussion in Feb. 2019

  • 使用Mock 時會強調驗證傳入的參數

Wrap up
Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

  • 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
      Image Not Showing Possible Reasons
      • The image file may be corrupted
      • The server hosting the image is unavailable
      • The image path is incorrect
      • The image format is not supported
      Learn More →
      or else..
      Image Not Showing Possible Reasons
      • The image file may be corrupted
      • The server hosting the image is unavailable
      • The image path is incorrect
      • The image format is not supported
      Learn More →


給新星

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
的小建議

  • 多寫測試

    Image Not Showing Possible Reasons
    • The image file may be corrupted
    • The server hosting the image is unavailable
    • The image path is incorrect
    • The image format is not supported
    Learn More →

  • 查GitHub文件

    Image Not Showing Possible Reasons
    • The image file may be corrupted
    • The server hosting the image is unavailable
    • The image path is incorrect
    • The image format is not supported
    Learn More →

  • slack討論串挖寶

    Image Not Showing Possible Reasons
    • The image file may be corrupted
    • The server hosting the image is unavailable
    • The image path is incorrect
    • The image format is not supported
    Learn More →

  • 比較codebase 各種 stub/ Mock用法,想想有沒有其他更好的方式

    Image Not Showing Possible Reasons
    • The image file may be corrupted
    • The server hosting the image is unavailable
    • The image path is incorrect
    • The image format is not supported
    Learn More →
    並與大大們討論

  • 多Google,多讀幾遍 ⬇️ reference


Thank you!
Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

Ref:

Select a repo