I'm working on learning TDD while writing some small ruby programs. I have the following class:
class MyDirectory
def check(dir_name)
unless File.directory?(dir_name) then
raise RuntimeError, "#{dir_name} is not a directory"
end
end
end
and I'm trying to test it with this rspec test.
describe MyDirectory do
it "should error if doesn't exist" do
one = MyDirectory.new
one.check("donee").should raise_exception(RuntimeError, "donee is not a directory")
end
end
It never works, and I don't understand what is wrong from the rspec output.
Failures:
1) MyDirectory should error if doesn't exist
Failure/Error: one.check("donee").should raise_error(RuntimeError, "donee is not a directory")
RuntimeError:
donee is not a directory
# ./lib/directory.rb:4:in `check'
# ./spec/directory_spec.rb:9:in `block (2 levels) in <top (required)>'
I'm hoping this is something simple that I'm missing, but I'm just not seeing it.
If you are checking for an exception, you have to separate that from your test with lambda or the exception will bubble up.
lambda {one.check("donee")}.should raise_error(RuntimeError, "donee is not a directory")
Edit: Since people still use this answer, here is what to do in Rspec 3:
expect{one.check("donee")}.to raise_error(RuntimeError, "donee is not a directory")
The lambda is no longer necessary because the expect syntax takes an optional block.
should is deprecated now so instead of this
-> { described_class.new('InValidParam') }
.should raise_error(RuntimeError)
# or
lambda { described_class.new('InValidParam') }
.should raise_error(RuntimeError)
use this:
expect { described_class.new('InValidParam') }.to raise_error(RuntimeError)
https://relishapp.com/rspec/rspec-expectations/docs/built-in-matchers/raise-error-matcher
Related
I'm trying to write specs for an extension to rspec.
This is the gist of what I'm trying to test:
require 'rspec-let-and-after-extension'
RSpec.describe "let(...).and_after" do
it 'is called if the `let` is invoked even if the example fails' do
call_order = []
RSpec.describe do
let(:foo) { }.and_after { call_order << :and_after }
it { foo; call_order << :example; raise 'failed!' }
end.run
expect(call_order).to eq [:example, :and_after]
end
end
One of the important behaviours is that if running the example fails, the cleanup code still runs. So I test this by recording the order of the calls and raising an exception from the example.
Problem is, when I run it, it sees this block as a second example, which then fails with errors:
.F
Failures:
1)
Got 0 failures and 2 other errors:
1.1) Failure/Error: it { foo; call_order << :example; raise 'failed!' }
RuntimeError:
failed!
# ./spec/spec.rb:43:in `block (4 levels) in <top (required)>'
# ./spec/spec.rb:44:in `block (2 levels) in <top (required)>'
1.2) Failure/Error: it { foo; call_order << :example; raise 'failed!' }
RuntimeError:
failed!
# ./spec/spec.rb:43:in `block (4 levels) in <top (required)>'
Finished in 0.00167 seconds (files took 0.08011 seconds to load)
2 examples, 1 failure
Failed examples:
rspec ./spec/spec.rb:43 #
As you can see, the output did have one dot, so the actual example passed. But then there is an F, because it has seen the internal example, run that, and unsurprisingly that one failed.
How do I make rspec not see this nested example as one of the examples it's supposed to run, so that this example completes with a single dot?
(If you're wondering about what the rspec devs themselves do about their tests, it looks like they use cucumber. Do they use cucumber because they couldn't figure this out either? :))
You can use the new sandboxing API (available in 3.2+).
RSpec.configure do |rspec|
rspec.around do |ex|
RSpec::Core::Sandbox.sandboxed do |config|
# re-configure any configuration defined by your extension here
# before allowing the example to run. The sandbox runs with a fresh
# config instance, which means any configuration you have set in
# `rspec-let-and-after-extension` will not apply while the example
# is running.
# config.extend MyExtensionModule
ex.run
end
end
end
I had this written, and it passed.
it 'raises a GitConfigNotFound error when YAML config file cannot be found' do
allow(YAML).to receive(:load_file)
.with(Rails.root.join('config', 'git_config.yml'))
.and_raise(Errno::ENOENT)
expect { described_class::config }.to raise_error GitConfigNotFound
end
Then I tried to put it within a context to match my other tests and it failed. I formatted as shown below. Does anybody have any insight as to why this is happening?
context 'will raise a GitConfigNotFound exception if git config file is missing' do
before do
allow(YAML).to receive(:load_file)
.with(Rails.root.join('config', 'git_config.yml'))
.and_raise(Errno::ENOENT)
end
it { expect(described_class::config).to raise_error GitConfigNotFound }
end
It is giving me this output, which seems to be what I want but for some reason doesn't catch it.:
1) GitConfigsLoader will raise a GitConfigNotFound exception if git config file is missing
Failure/Error: it { expect(described_class::config).to raise_error }
GitConfigNotFound:
Error: git_config.yml not found.
# ./lib/git_configs_loader.rb:9:in `rescue in config'
# ./lib/git_configs_loader.rb:7:in `config'
# ./spec/lib/git_configs_loader_spec.rb:37:in `block (3 levels) in <top (required)>'
Perhaps this is what #PeterAlfvin meant, but I finally foud the answer per another one of his answers! I was using expect(...) rather than expect{...}. The parens executes immediately and blows up instantly and isn't caught by the .to raise_exception. Using the braces allows for the raise_error to execute the except block and catch the error.
context 'when no git_config.yml file is proivded' do
before do
allow(YAML).to receive(:load_file).and_raise(Errno::ENOENT)
end
it { expect{ described_class::config }.to raise_exception GitConfigNotFound }
end
This is something that I've seen before when using RSpec Rails and I believe that I know what is happening, I just don't know how I can get around it.
To me, it appears that the following test should pass. It expects an error, and an error is raised although I assume that the source of the error is what it is tripping up on.
csv_file_spec.rb
require 'spec_helper'
RSpec.describe Cleaner::CSVFile do
context 'when CSV file does not exist' do
let(:file) { Cleaner::CSVFile.new('tmp/file-does-not-exist.csv') }
it 'raises error' do
expect(file).to raise_error
end
end
end
csv_file.rb
module Cleaner
# A CSVFile is a CSV file loaded into memory. It exposes the clean method.
class CSVFile
attr_accessor :raw
def initialize(file)
#raw = File.open(file)
end
end
end
Output
1) Cleaner::CSVFile is not valid
Failure/Error: expect(Cleaner::CSVFile.new('tmp/file-does-not-exist.csv')).to raise_error
Errno::ENOENT:
No such file or directory # rb_sysopen - tmp/file-does-not-exist.csv
# ./lib/cleaner/csv_file.rb:8:in `initialize'
# ./lib/cleaner/csv_file.rb:8:in `open'
# ./lib/cleaner/csv_file.rb:8:in `initialize'
# ./spec/csv_file_spec.rb:7:in `new'
# ./spec/csv_file_spec.rb:7:in `block (2 levels) in <top (required)>'
I can see that the CSVFile object is not able to be initialized because the file does not exist and that'll be why RSpesc can't continue the test but what can I do to get around this?
I get the feeling that there is something fundamentally wrong with my approach to testing that I'm not seeing. I'd rather delegate the error to the standard File class, and not raise my own error messages as the error is verbose enough and I'd only be duplicating effort - should I be implementing my own instead?
Thanks!
For exceptions you should use block or lambda in expect syntax:
it 'raises error' do
expect{ Cleaner::CSVFile.new('tmp/file-not-exist.csv') }.to raise_error
end
You could use stubbing also :
require 'spec_helper'
RSpec.describe Cleaner::CSVFile do
context 'when CSV file does not exist' do
it 'raises error' do
allow(described_class).to receive(:new).and_raise("File not exist")
expect { described_class.new }.to raise_error("File not exist")
end
end
end
Read match message with a string.
I have an rspec test on a pure Ruby model:
require 'spec_helper'
require 'organization'
describe Organization do
context '#is_root?' do
it "creates a root organization" do
org = Organization.new
expect { org.is_root?.to eq true }
end
end
end
My organization model looks like this:
class Organization
attr_accessor :parent
def initialize(parent = nil)
self.parent = parent
end
end
The output when running the tests:
bundle exec rspec spec/organization_spec.rb:6
Run options: include {:locations=>{"./spec/organization_spec.rb"=>[6]}}
.
Finished in 0.00051 seconds
1 example, 0 failures
When I run the test, it passes, despite the fact that the method is_root? doesn't exist on the model. I usually work in Rails, not pure Ruby, and I've never seen this happen. What is going on?
Thanks!
It should be:
expect(org.is_root?).to eq true
When you pass block to expect it is being wrapped in ExpectationTarget class (strictly speaking BlockExpectationTarget < ExpectationTarget). Since you didn't specify what you expect from this object, the block is never executed, hence no error is raised.
You are passing a block to expect, which is never being called. You can see this by setting an expectation on that block
expect { org.is_root?.to eq true }.to_not raise_error
1) Organization#is_root? creates a root organization
Failure/Error: expect { puts "HI";org.is_root?.to eq true }.to_not raise_error
expected no Exception, got #<NoMethodError: undefined method `is_root?' for #<Organization:0x007ffa798c2ed8 #parent=nil>> with backtrace:
# ./test_spec.rb:15:in `block (4 levels) in <top (required)>'
# ./test_spec.rb:15:in `block (3 levels) in <top (required)>'
# ./test_spec.rb:15:in `block (3 levels) in <top (required)>'
Or by just putting a plain raise or puts inside the block, neither of which will be called:
expect { puts "HI"; raise; org.is_root?.to eq true }
The block form is used for expecting that a piece of code raises an exception or not. The correct syntax for checking values is:
expect(org.is_root?).to eq(true)
I have a problem with mocking. I have class DistanceMatrix and I would
like to indicate which method form_matrix was called in if/else
statement. I need to use mocha and RSpec. Any ideas?
class DistanceMatrix
def initialize(*args)
if args[0].class == String
form_matrix(get_data_from_yaml(args[0], args[1]))
elsif args[0].class == Array || args[0] == nil
form_matrix(get_data_from_db(args[0]))
end
end
def form_matrix(...)
...
end
end
it tried:
describe DistanceMatrix, "when mocking ..." do
it "should do call form_matrix" do
DistanceMatrix.any_instance.expects(:form_matrix).with([1]).once
DistanceMatrix.any_instance.expects(:get_data_from_yaml).with("file_name.yml").once.returns([1])
DistanceMatrix.new("file_name.yml")
end
end
but got error:
Failures:
1) DistanceMatrix when mocking ... should do call form_matrix
Failure/Error: DistanceMatrix.new("file_name.yml")
unexpected invocation: #<AnyInstance:DistanceMatrix>.get_data_from_yaml('file_name.yml', nil)
unsatisfied expectations:
- expected exactly once, not yet invoked: #<AnyInstance:DistanceMatrix>.get_data_from_yaml('file_name.yml')
- expected exactly once, not yet invoked: #<AnyInstance:DistanceMatrix>.form_matrix([1])
satisfied expectations:
- allowed any number of times, already invoked once: #<DistanceMatrix:0x9e48b40>.get_optimal_route(any_parameters)
- allowed any number of times, already invoked once: #<Database::Distances:0x9d59798>.load_distances(any_parameters)
# ./distance_matrix.rb:18:in `initialize'
# ./tsp_algorithm_spec.rb:253:in `new'
# ./tsp_algorithm_spec.rb:253:in `block (2 levels) in <top (required)>'
Finished in 0.25979 seconds
I found that in RSpec we should use not .expects() but .should_receive(), so I tried:
describe DistanceMatrix, "when mocking ..." do
it "should do call form_matrix" do
DistanceMatrix.any_instance.should_receive(:form_matrix).with([1])
DistanceMatrix.any_instance.should_receive(:get_data_from_yaml).with("file_name.yml").and_return([1])
DistanceMatrix.new("file_name.yml")
end
end
but got new failure:
Failures:
1) DistanceMatrix when mocking ... should do call form_matrix
Failure/Error: DistanceMatrix.any_instance.should_receive(:form_matrix).with([1])
(#<Mocha::ClassMethods::AnyInstance:0x96356b0>).form_matrix([1])
expected: 1 time
received: 0 times
# ./tsp_algorithm_spec.rb:251:in `block (2 levels) in <top (required)>'
Finished in 0.26741 seconds
I only have experience with using Mocha and not RSpec, but looking at the Mocha failure message, the key parts are these :-
unexpected invocation: #<AnyInstance:DistanceMatrix>.get_data_from_yaml('file_name.yml', nil)
unsatisfied expectations:
- expected exactly once, not yet invoked: #<AnyInstance:DistanceMatrix>.get_data_from_yaml('file_name.yml')
If you look at the ends of these lines, you will notice that get_data_from_yaml is not being called with the expected parameters. It is being called with ('filename.yml', nil) and not ('filename.yml') as expected.
This is happening because when you call DistanceMatrix.new("file_name.yml") in your test with only one argument and then inside DistanceMatrix#initialize DistanceMatrix#get_data_from_yaml is being called with (args[0], args[1]) and since args is a single element array, args[1] will be nil.
Maybe this isn't how you expected Ruby to work, but the following demonstrates this behaviour :-
def foo(*args)
puts "args[0]=#{args[0].inspect}; args[1]=#{args[1].inspect}"
end
foo("string") # => args[0]="string"; args[1]=nil
DistanceMatrix.any_instance.expects(:form_matrix).with("String") # => supply the correct string param
or
DistanceMatrix.any_instance.expects(:form_matrix).with([]) # => supply the correct array param
I'm not sure what your get_data_from_db and get_data_from_yaml methods are doing, but you should be able to control those inputs as well to verify the correct arguments are being supplied to form_matrix.
EDITED
You'll have to use DistanceMatrix.any_instance instead of mocking on an instance variable because you're trying to mock something in the initializer. Also, in case its unclear, you'll need to actually make the appropriate method call after you set up the mock in the lines above, e.g.
DistanceMatrix.new("SomeString")
EDITED
it "should do call #form_matrix with proper arguments" do
DistanceMatrix.any_instance.expects(:form_matrix).with([1])
DistanceMatrix.any_instance.expects(:get_data_from_yaml).with("foo").returns([1])
DistanceMatrix.new("foo")
end