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