How should I stub a method globally using RSpec?

2020-05-30 03:02发布

I am working on a Rails application. I am trying to stub a method globally.

What I am doing is to stub it inside the RSpec configuration, on a before(:suite) block as follows:

RSpec.configure do |config|
  config.before(:suite) do
    allow_any_instance_of(MyModel).to receive(:my_method).and_return(false)
  end
end

However, starting the test fails with the following error:

in `method_missing': undefined method `allow_any_instance_of' for #<RSpec::Core::ExampleGroup:0x00000008d6be08> (NoMethodError)

Any clue? How should I stub a method globally using RSpec?

P.

6条回答
干净又极端
2楼-- · 2020-05-30 03:16

I recently ran into a case where I needed to stub something in a before(:all) or before(:context) block, and found the solutions here to not work for my use case.

RSpec docs on before() & after() hooks says that it's not supported:

before and after hooks can be defined directly in the example groups they should run in, or in a global RSpec.configure block.

WARNING: Setting instance variables are not supported in before(:suite).

WARNING: Mocks are only supported in before(:example).

Note: the :example and :context scopes are also available as :each and :all, respectively. Use whichever you prefer.

Problem

I was making a gem for writing a binary file format which contained at unix epoch timestamp within it's binary header. I wanted to write RSpec tests to check the output file header for correctness, and compare it to a test fixture binary reference file. In order to create fast tests I needed to write the file out once before all the example group blocks would run. In order to check the timestamp against the reference file, I needed to force Time.now() to return a constant value. This led me down the path of trying to stub Time.now to return my target value.

However, since rspec/mocks did not support stubbing within a before(:all) or before(:context) block it didn't work. Writing the file before(:each) caused other strange problems.

Luckily, I stumbled across issue #240 of rspec-mocks which had the solution!

Solution

Since January 9th 2014 (rspec-mocks PR #519) RSpec now contains a method to work around this:

RSpec::Mocks.with_temporary_scope

Example

require 'spec_helper'
require 'rspec/mocks'

describe 'LZOP::File' do
  before(:all) {
    @expected_lzop_magic = [ 0x89, 0x4c, 0x5a, 0x4f, 0x00, 0x0d, 0x0a, 0x1a, 0x0a ]
    @uncompressed_file_data = "Hello World\n" * 100
    @filename = 'lzoptest.lzo'
    @test_fixture_path = File.join(File.dirname(__FILE__), '..', 'fixtures', @filename + '.3')
    @lzop_test_fixture_file_data = File.open( @test_fixture_path, 'rb').read
    @tmp_filename = File.basename(@filename)
    @tmp_file_path = File.join( '', 'tmp', @tmp_filename)

    # Stub calls to Time.now() with our fake mtime value so the mtime_low test against our test fixture works
    # This is the mtime for when the original uncompressed test fixture file was created
    @time_now = Time.at(0x544abd86)
  }

  context 'when given a filename, no options and writing uncompressed test data' do

    describe 'the output binary file' do
      before(:all) {
        RSpec::Mocks.with_temporary_scope do
          allow(Time).to receive(:now).and_return(@time_now)
          # puts "TIME IS: #{Time.now}"
          # puts "TIME IS: #{Time.now.to_i}"
          my_test_file = LZOP::File.new( @tmp_file_path )
          my_test_file.write( @uncompressed_file_data )
          @test_file_data = File.open( @tmp_file_path, 'rb').read
        end
      }

      it 'has the correct magic bits' do
        expect( @test_file_data[0..8].unpack('C*') ).to eq @expected_lzop_magic
      end

      ## [...SNIP...] (Other example blocks here)
      it 'has the original file mtime in LZO file header' do
        # puts "time_now= #{@time_now}"

        if @test_file_data[17..21].unpack('L>').first & LZOP::F_H_FILTER == 0
          mtime_low_start_byte=25
          mtime_low_end_byte=28
          mtime_high_start_byte=29
          mtime_high_end_byte=32
        else
          mtime_low_start_byte=29
          mtime_low_end_byte=32
          mtime_high_start_byte=33
          mtime_high_end_byte=36
        end
        # puts "start_byte: #{start_byte}"
        # puts "end_byte: #{end_byte}"
        # puts "mtime_low: #{@test_file_data[start_byte..end_byte].unpack('L>').first.to_s(16)}"
        # puts "test mtime: #{@lzop_test_fixture_file_data[start_byte..end_byte].unpack('L>').first.to_s(16)}"

        mtime_low = @test_file_data[mtime_low_start_byte..mtime_low_end_byte].unpack('L>').first
        mtime_high = @test_file_data[mtime_high_start_byte..mtime_high_end_byte].unpack('L>').first
        # The testing timestamp has no high bits, so this test should pass:
        expect(mtime_low).to eq @time_now.to_i
        expect(mtime_high).to eq 0

        expect(mtime_low).to eq @lzop_test_fixture_file_data[mtime_low_start_byte..mtime_low_end_byte].unpack('L>').first
        expect(mtime_high).to eq @lzop_test_fixture_file_data[mtime_high_start_byte..mtime_high_end_byte].unpack('L>').first

        mtime_fixed = ( mtime_high << 16 << 16 ) | mtime_low

        # puts "mtime_fixed: #{mtime_fixed}"
        # puts "mtime_fixed: #{mtime_fixed.to_s(16)}"

        expect(mtime_fixed).to eq @time_now.to_i

      end
    end
  end
end
查看更多
太酷不给撩
3楼-- · 2020-05-30 03:17

You may use the following to stub a method 'do_this' of class 'Xyz' :

allow_any_instance_of(Xyz).to receive(:do_this).and_return(:this_is_your_stubbed_output)

This stubs the output to - ':this_is_your_stubbed_output' from wherever this function is invoked.

You may use the above piece of code in before(:each) block to make this applicable for all your spec examples.

查看更多
劳资没心,怎么记你
4楼-- · 2020-05-30 03:27

Do not stub methods in before(:suite) because stubs are cleared after each example, as stated in the rspec-mocks README:

Use before(:each), not before(:all)

Stubs in before(:all) are not supported. The reason is that all stubs and mocks get cleared out after each example, so any stub that is set in before(:all) would work in the first example that happens to run in that group, but not for any others.

Instead of before(:all), use before(:each).

I think that's why allow_any_instance_of is not available in before(:suite) block, but is available in before(:each) block.

If the method is still missing, maybe you configured rspec-mocks to only allow :should syntax. allow_any_instance_of was introduced in RSpec 2.14 with all the new :expect syntax for message expectations.

Ensure that this syntax is enabled by inspecting the value of RSpec::Mocks.configuration.syntax. It is an array of the available syntaxes in rspec-mocks. The available syntaxes are :expect and :should.

RSpec.configure do |config|
  config.mock_with :rspec do |mocks|
    mocks.syntax = [:expect, :should]
  end
end

Once configured properly, you should be able to use allow_any_instance_of.

查看更多
仙女界的扛把子
5楼-- · 2020-05-30 03:27

If you want a particular method to behave a certain way for your entire test suite, there's no reason to even deal with RSpec's stubs. Instead, you can simply (re)define the method to behave how you want in your test environment:

class MyModel
  def my_method
    false
  end
end

This could go in spec/spec_helper.rb or a similar file.

查看更多
迷人小祖宗
6楼-- · 2020-05-30 03:28

It probably is a context / initialization issue. Doing it in config.before(:each) should solve your problem.

查看更多
混吃等死
7楼-- · 2020-05-30 03:29

What version of RSpec are you using? I believe allow_any_instance_of was introduced in RSpec 2.14. For earlier versions, you can use:

MyModel.any_instance.stub(:my_method).and_return(false)
查看更多
登录 后发表回答