Stubbing File.exists? in RSpec - ruby-on-rails

I want to test the else branch of the following ruby code:
SCHEMA_SOURCE = if File.exist? SCHEMA_FILENAME
SCHEMA_FILENAME
else
ContentfulApi::Http
end
I cannot find a way to mock the non-existence of the file SCHEMA_FILENAME. Here are the simplified versions of the test I have tried, and the error:
it 'fails to find file when it does not exist' do
allow(File).to receive(:exists?).and_return(false)
expect(File.exist?(ContentfulApi::SCHEMA_FILENAME)).to be_falsey
end
it 'fails to find file when it does not exist' do
allow(File).to receive(:exists?).with(ContentfulApi::SCHEMA_FILENAME)\
.and_return(false)
expect(File.exist?(ContentfulApi::SCHEMA_FILENAME)).to be_falsey
end
These both fail with the error
expected: falsey value
got: true
it 'fails to find file when it does not exist' do
File.stub(:exists?) { false }
expect(File.exist?(ContentfulApi::SCHEMA_FILENAME)).to be_falsey
end
it 'fails to find file when it does not exist' do
File.stub!(:exists?).with(ContentfulApi::SCHEMA_FILENAME)\
.and_return(false)
expect(File.exist?(ContentfulApi::SCHEMA_FILENAME)).to be_falsey
end
These both fail with the error
undefined method `stub' for File:Class
My first two attempts follow the example in the Method Stubs documentation, though there may be a distinction between classes and objects that is not explored in that documentation. (Though classes are objects in Ruby, right?)
The second two attempts follow older documentation.
How should I stub File.exist? SCHEMA_FILENAME so that it returns false even though the file does actually exist?
N.B. The approaches in these similar questions do not work:
rspec to stub several File.exists? calls
Rspec -- need to stub File.open that gets called in another file
rspec undefined method stub for model
how to reset expectations on a mocked class method?
How do I stub out a specific file with rspec?
Can't stub things with Rspec
rspec 3 - stub a class method

You're mocking File.exists? and calling File.exist?.
Note that if you have verify_partial_doubles set, and you should, RSpec won't allow you to mock a method which does not exist.
RSpec.configure do |config|
config.mock_with :rspec do |mocks|
mocks.verify_partial_doubles = true
end
end
RSpec.describe File do
it 'cannot mock a method whcih does not exist' do
allow(File).to receive(:dslkfjalk).and_return(true)
end
end
Failures:
1) File cannot mock a method whcih does not exist
Failure/Error: allow(File).to receive(:dslkfjalk).and_return(true)
File does not implement: dslkfjalk
But File does have both exist? and exists? methods. File.exists? is deprecated and removed from the documentation, but I guess it still... exists.

Related

How to configure Sorbet with rspec?

I have a simple test but the describe keyword is not working in Sorbet tests.
The error I'm receiving on these methods:
Method `describe` does not exist on `T.class_of(<root>)`7003
RSpec.describe(Model) do
describe 'my test' do
before(:each) do # .before error
user = FactoryBot.create(:user)
end
it 'can fill in all fields' do # .it errors
end
end
end
I think I need to tell Sorbet some how that this is called in the context of spec_helper.rbbut I'm not sure how to do that.
I've already installed this gem rspec-sorbet and ran
spec/spec_helper.rb
require 'rspec/sorbet'
To silence the errors, I ran this:
RSpec.describe(Model) do
T.bind(self, T.untyped)
# T.bind(self, RSpec) This does not work either
end

"Undefined method `build'" when using Rspec + FactoryBotRails

I'm trying this gem called FactoryBotRails. For some reason, when I try it on one of my models unit tests, the following error is thrown.
Failure/Error: my_model = build(:my_model)
NoMethodError:
undefined method `build' for #\<\RSpec::ExampleGroups::MyModel::ValidationTests:0x000055c553959958>
I don't know what I'm doing wrong, as long as I have followed several tutorials on the web, and did the same steps.
Added, in:
gemfile
gem 'factory_bot_rails', '~> 5.1.1'
app/spec/support/factory_bot.rb
RSpec.configure do |config|
config.include FactoryBot::Syntax::Methods
end
spec/rails_helper.rb
Dir[Rails.root.join('spec/support/**/*.rb')].each { |f| require f }
spec/factories/my_models.rb
FactoryBot.define do
factory :my_model do
name { 'some name' }
code { 'some code' }
end
end
And used it like:
my_model = build(:my_model)
What is wrong with my configuration?
The issue might not be what you're calling, but where you're calling it. my_model = build(:my_model) is not syntax you want to use while writing specs, and the error message looks maybe you're calling it from outside of a spec? Because if you're calling it from within a spec, the error should be something along the lines of ArgumentError: Factory not registered: my_model. The spec itself should look like this:
# spec/models/my_model_spec.rb
require 'rails_helper'
describe MyModel do
let(:my_model) { build :my_model }
it { expect(my_model).to be_valid }
end
I would also specify the model name in your factory declaration (i.e., factory :my_model, class: 'MyModel' do). If you want to play with your factories, you can start up a test console:
# start rails console in 'test' environment
rails console test
my_model = FactoryBot.build :my_model
Note that you will need to use FactoryBot.build instead of build in your test console.
If this doesn't resolve your issue, please update your post with the contents of the spec you're trying to run, how you're trying to run it, and expand your definition of your spec/rails_helper.rb file. Since you're new to RSpec, I also suggest checking out http://www.betterspecs.org/ for best practices.
Probably you're missing to setup shortcuts for FactoryGirl by including its methods in your rails_helper:
RSpec.configure do |config|
# ...
config.include FactoryGirl::Syntax::Methods
end
The syntax for creation of factorybot is:
FactoryBot.create :my_model
Pass arguments hash if you need something different:
FactoryBot.create :my_model, name: "John Doe"
For multiple (e.g. 10 my_models):
FactoryBot.create_list :my_model, 10

Upgrading to RSpec 3 and following deprecation notice results in "undefined method 'allow'"

Before upgrading RSpec, I had this block in my features/support/hooks.rb file:
After do
begin
Challenge.unstub(:current)
rescue RSpec::Mocks::MockExpectationError
end
end
After upgrading, I got this notice:
DEPRECATION: Using unstub from rspec-mocks' old :should syntax without explicitly enabling the syntax is deprecated. Use allow(...).to_receive(...).and_call_original or explicitly enable :should instead. Called from /Users/grant/xx/features/support/hooks.rb:37:in block in <top (required)>.
Ok, sounds pretty straightforward. I changed my code to this:
After do
begin
allow(Challenge).to receive(:current).and_call_original
rescue RSpec::Mocks::MockExpectationError
end
end
But now I'm getting:
undefined method allow for #<Cucumber::Rails::World:0x007facbed9f1d0> (NoMethodError)
Wat? C'mon RSpec, I did exactly what you told me to do!
Based on some Googling, I tried adding require 'rspec/expectations' to the top of this file. It didn't do anything.
Can anyone fill me in on what I'm missing?
Perhaps your RSpec configuration doesn't enable the allow syntax. In your RSpec configuration file, probably spec/spec_helper.rb or spec/rails_helper.rb, do you have something like the following?
RSpec.configure do |config|
config.mock_with :rspec do |c|
c.syntax = :should
end
end
If so, see whether changing it to
RSpec.configure do |config|
config.mock_with :rspec do |c|
c.syntax = [:should, :expect]
end
end
fixes your problem. And then replace all uses of should with uses of allow, and upgrade other uses of the :should syntax, perhaps with transpec, and disable the :should syntax, and you'll be up to the minute.
Source: https://relishapp.com/rspec/rspec-expectations/docs/syntax-configuration
It turns out that what I'm doing is completely unnecessary, and that's probably why allow is not defined for the After context.
Rspec-mock stubs are removed after every example (in Rspec) or scenario (in cucumber), so unstubbing them in the After block is redundant and useless.
The whole After block that I'm trying to correct should instead simply be deleted.
(Special thanks to this post by Myron Marston on the Rspec Google Group)

FakeFS and Rspec incosistency with Rspec and real filesystem

I'm trying to write some tests involving file operations. I want to use some fake file system (something like VCR for external services) and I have found fakeFS. Unfortunately, either I can't set it right or something is broken (which I doubt, it's quite basic function), I've prepared simple example which illustrates what I mean, let the code speak:
With real FS:
module MyModule
describe Something do
before(:all) do
File.open("#{Rails.root}/foo.txt", 'w+') { |f| f.write 'content'}
end
it 'should exist' do
expect(Pathname.new("#{Rails.root}/foo.txt").exist?).to be_true
end
it 'should still exist' do
expect(Pathname.new("#{Rails.root}/foo.txt").exist?).to be_true
end
end
end
Running that gives:
bash-4.2$ rspec
..
Finished in 0.00161 seconds
2 examples, 0 failures
Adding fakeFS in such way:
require 'fakefs/spec_helpers'
module MyModule
describe Something do
include FakeFS::SpecHelpers
FakeFS.activate!
FakeFS::FileSystem.clone(Rails.root)
before(:all) do
File.open("#{Rails.root}/foo.txt", 'w+') { |f| f.write 'content'}
end
it 'should exist' do
expect(Pathname.new("#{Rails.root}/foo.txt").exist?).to be_true
end
it 'should still exist' do
expect(Pathname.new("#{Rails.root}/foo.txt").exist?).to be_true
end
end
end
results in:
bash-4.2$ rspec
.F
Failures:
1) MyModule::Something should still exist
Failure/Error: expect(Pathname.new("#{Rails.root}/foo.txt").exist?).to be_true
expected: true value
got: false
# ./spec/models/something_spec.rb:23:in `block (2 levels) in <module:MyModule>'
Finished in 0.00354 seconds
2 examples, 1 failure
So it seems like file is not persisted through subsequent tests. Do I misunderstand how before(:all) works or do I do something wrong? If so then why that code works with real files?
If it is 'not a bug, just a feature' then is there any other fake filesystem gem which is consistent with real one? Or do I have to stay with real files to get tests that.. well, test?
I found the answer just after creating that question, duh ;) I've looked into source of that lib and found suspicious line.
Instead of FakeFS::SpecHelpers I've included FakeFS::SpecHelpers::All which is the same code except FakeFS::FileSystem is not being cleared after each call, now it behaves correctly.

How should I stub a method globally using RSpec?

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.
It probably is a context / initialization issue. Doing it in config.before(:each) should solve your problem.
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.
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
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.
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)
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.

Resources