Just learning rspec syntax and I noticed that this code works:
context "given a bad list of players" do
let(:bad_players) { {} }
it "fails to create given a bad player list" do
expect{ Team.new("Random", bad_players) }.to raise_error
end
end
But this code doesn't:
context "given a bad list of players" do
let(:bad_players) { {} }
it "fails to create given a bad player list" do
expect( Team.new("Random", bad_players) ).to raise_error
end
end
It gives me this error:
Team given a bad list of players fails to create given a bad player list
Failure/Error: expect( Team.new("Random", bad_players) ).to raise_error
Exception:
Exception
# ./lib/team.rb:6:in `initialize'
# ./spec/team_spec.rb:23:in `new'
# ./spec/team_spec.rb:23:in `block (3 levels) in <top (required)>'
My question is:
Why does this happen?
What is the difference between the former and later example exactly in ruby?
I am also looking for rules on when to use one over the other
One more example of the same but inverse results, where this code works:
it "has a list of players" do
expect(Team.new("Random").players).to be_kind_of Array
end
But this code fails
it "has a list of players" do
expect{ Team.new("Random").players }.to be_kind_of Array
end
Error I get in this case is:
Failure/Error: expect{ Team.new("Random").players }.to be_kind_of Array
expected #<Proc:0x007fbbbab29580#/Users/amiterandole/Documents/current/ruby_sandbox/tdd-ruby/spec/team_spec.rb:9> to be a kind of Array
# ./spec/team_spec.rb:9:in `block (2 levels) in <top (required)>'
The class I am testing looks like this:
class Team
attr_reader :name, :players
def initialize(name, players = [])
raise Exception unless players.is_a? Array
#name = name
#players = players
end
end
As has been mentioned:
expect(4).to eq(4)
This is specifically testing the value that you've sent in as the parameter to the method. When you're trying to test for raised errors when you do the same thing:
expect(raise "fail!").to raise_error
Your argument is evaluated immediately and that exception will be thrown and your test will blow up right there.
However, when you use a block (and this is basic ruby), the block contents isn't executed immediately - it's execution is determined by the method you're calling (in this case, the expect method handles when to execute your block):
expect{raise "fail!"}.to raise_error
We can look at an example method that might handle this behavior:
def expect(val=nil)
if block_given?
begin
yield
rescue
puts "Your block raised an error!"
end
else
puts "The value under test is #{val}"
end
end
You can see here that it's the expect method that is manually rescuing your error so that it can test whether or not errors are raised, etc. yield is a ruby method's way of executing whatever block was passed to the method.
In the first case, when you pass a block to expect, the execution of the block doesn't occur until it's time to evaluate the result, at which point the RSpec code can catch any error that are raised and check it against the expectation.
In the second case, the error is raised when the argument to expect is evaluated, so the expect code has no chance to get involved.
As for rules, you pass a block or a Proc if you're trying to test behavior (e.g. raising errors, changing some value). Otherwise, you pass a "conventional" argument, in which case the value of that argument is what is tested.
Related
In app/models/car.rb the class method stock looks in part like this:
def self.stock
raise Exception, "Property amount not set" if Property.get(:amount).nil?
...
end
This Property is accessable by the user through all CRUD operations.
I now want to test that if that Property is truly deleted an standard expection should be thrown. Therefore i created in my rspec model the following example group
describe '.stock' do
describe 'if property is not set' do
before(:all) {Property.where(name: 'amount').map(&:destroy)}
it 'raises an exception' do
expect{Car.stock}.to raise_error (Exception)
end
after (:all) {Property.create! name: 'amount', value: 10}
end
describe 'if property is set' do
before (:all) do
create :car_listed
create :car_sold
end
it 'calculates the correct amount of sellable cars' do
amount = 10 - Car.where(state: "sold")
expect(Car.stock).to eq(amount)
end
end
end
I make sure all Properties with that name get deleted. In the it-block i then expect the exception to be trown. After the it-block i created the property again because other tests depend on it.
In the app are properties which are not going to change during tests. So database_cleaner does not truncate the properties table. It got set through a seeds file.
config.before(:suite) do
DatabaseCleaner.strategy = :truncation, {except: %w[properties]}
end
The test however fails with
Car
.stock
if property is set
calculates the correct amount of sellable cars
if property is not set
raises an exception (FAILED - 1)
Failures:
1) Car.stock if property is not set not set raises an exception
Failure/Error: expect{Car.stock}.to raise_error (Exception)
expected Exception but nothing was raised
# ./spec/models/car_spec.rb: `block (4 levels) in <top (required)>'
My question is now how do i have to delete this property properly ((:), so that my exception gets raised.
There are easier ways to test this that do not touch the database. Here's how to approach the problem using stubs:
describe '.stock' do
before do
allow(Property).to receive(:get).with(:amount).and_return(amount)
end
context 'when property amount is NOT set' do
let(:amount) { nil }
it 'raises an exception' do
expect { Car.stock }.to raise_error(Exception)
end
end
context 'when amount property is set' do
let(:amount) { 10 }
before do
create :car_listed
create :car_sold
end
it 'does NOT raise an exception' do
expect { Car.stock }.to_not raise_error(Exception)
end
it 'calculates the correct amount of sellable cars' do
expect(Car.stock).to eq(amount - 1)
end
end
NOTE: I'm unsure about that last test since you didn't include that code.
I would like to create a validation like the matcher have_http_status to the content_type.
I created the following matcher:
require 'rspec/expectations'
RSpec::Matchers.define :have_content_type do |expected|
match do |actual|
actual.content_type == expected
end
description do
"respond with content_type #{expected}"
end
end
However, I would like to check if the actual value is a response object. If not, I would like to give a message like the have_http_status, which would be:
Failure/Error: it { expect(legal_person).to have_http_status(200) }
expected a response object, but an instance of LegalPerson was received
# ./spec/controllers/legal_people_controller_spec.rb:105:in `block (5 levels) in <top (required)>'
So when I pass an object different from a request-response object, I would expect an error saying that a response object was expected.
It works without it but would be better if it shows an informative message saying what went wrong exactly.
Thanks in advance for the help.
You're trying to have 2 matchers under 1 name. I believe this is a bad practice, and there should be two matchers - one for content type, another one - for checking request/response type. So your tests should look like:
expect(response).to be_a(ActionDispatch::Response)
expect(response).to have_content_type('application/json')
Also, I don't see any problem of having just content type check. If the object passed does not have .content_type method, the matcher will throw corresponding error: Undefined method 'content_type' and you should be fine figuring out that you've passed wrong object.
But, if you are still sure you need to check two things in one matcher, check this:
RSpec::Matchers.define :have_content_type do |expected|
match do |actual|
request_or_response?(actual) && actual.content_type.to_s == expected
end
description do |actual|
if request_or_response?(actual)
"respond with content_type #{expected}"
else
"a response object, but an instance of #{actual.class} was received"
end
end
private
def request_or_response?(actual)
[Rack::Request, ActionDispatch::Response].any? do |klass|
actual.is_a?(klass)
end
end
end
I have a Rails controller which does a health check of the database, like this:
def health_check
begin
status = ActiveRecord::Base.connected? ? 'UP' : 'DOWN'
rescue
status = 'DOWN'
end
render text: status
end
I'm trying to create a RSpec controller spec for this, and the specs for positive and negative responses work, but when I try to test the rescue block, RSpec seems to be ignoring it:
RSpec.describe(HealthCheckController) do
context 'When the check raises an exception' do
before :each do
allow(ActiveRecord::Base).to receive(:connected?).and_raise(OCIException) # Using Oracle
end
it 'should render text DOWN' do
# First attempt
get :health_check
expect(response.body).to eq 'DOWN'
# Second attempt
expect { get :health_check }.to raise_error
expect(response.body).to eq 'DOWN'
end
end
end
I tried the spec with both of the code inside the it block above (separatedly).
For the first, RSpec failed with this:
Failure/Error: get :health_check
OCIException:
OCIException
For the second, it also failed, with this more "familiar" message instead:
Failure/Error: expect(response.body).to eq 'DOWN'
expected: "DOWN"
got: ""
(compared using ==)
I also checked the HTTP code being returned by response, and it's 200, so the response itself is fine, no 500 error.
It's as if RSpec is simply bypassing the rescue block and not running it. What may be causing this? I'm not using the bypass_rescue RSpec method anywhere, this is also a new project.
Using:
Rails 4.2.6
Rake 10.5.0
RSpec-core 3.3.2
RSpec-rails 3.3.3
Actually, the problem has nothing to do with the rescue block in your controller. Rather, it is caused by the fact that you have overriden ActiveRecord::Base#connected? by stubbing it in your before block.
Calling render in the controller initiates a connection to the database. Somewhere in the process ActiveRecord::Base#connected? gets called (twice, actually), but instead of returning what it is supposed to return, it raises an exception that you have defined in your setup.
In your first example, the exception is raised before your expectation, thus the explicit exception name in the failure message.
In your second example, the exception is suppressed by your raise_error expectation, so RSpec is able to proceed to the next one. Where it fails because the render call in the controller never runs (due to the error), and as a result, the response body never gets a chance to be populated.
As a confirmation that ActiveRecord::Base#connected? does get called when you call render, try running the following spec. You will see that it's green, meaning that the said method has been called twice in the process. You can also try replacing render with head :ok and will see that in this case ActiveRecord::Base#connected? gets called only once.
RSpec.describe ApplicationController do
controller do
def index
render json: ''
end
end
specify 'test' do
expect(ActiveRecord::Base).to receive(:connected?).twice
get :index
end
end
Sometimes new (very DRY) rspec syntax makes me crazy...
Rspec v 2.14.1
describe "POST create" do
subject { post :create, contractor: valid_params }
context "by user" do
before { sign_in #legal.user }
it "contractor successful created" do
expect { subject }.to redirect_to(contractor_path(assigns(:contractor).id))
I have error & question here:
NoMethodError: # :contractor variable not defined
undefined method `id' for nil:NilClass
It seems that expect take an operator before controller method post executes, because I try to raise this method.
My code:
def create
#contractor = Contractor.restrict!(current_accreditation).new(permitted_params) # TODO move to the IR::Base
if #contractor.save
current_accreditation = #contractor.create_legal!(user: current_user) # TODO legal create
redirect_to(#contractor)
else
render(:new)
end
end
Secondly, why I have an error when try
expect(subject).to ...
Why {} works, but () no? In relish docs this method work great: https://www.relishapp.com/rspec/rspec-rails/docs/matchers/redirect-to-matcher
Kinda unrelated but I've found the following helpful:
Use expect {} when you want to test before/after of whatever's in the block. eg. expect { subject } to change(User, :count) - you want to check the count before, then after, to work out the difference and see if it actually changed.
Use expect () to verify the outcome of a block, eg. that a redirect occurred, or something got assigned, etc.
Your test with the assigns(:contractor) doesn't work because you're using the {} notation - so it's trying to work out the assigns(:contractor).id both before and after evaluating the subject (and of course, before the subject, it doesn't exist).
What's the proper way to force an RSpec test to fail?
I'm considering 1.should == 2 however there's probably something better.
fail/raise will do the trick (they are aliases of each other).
Example
specify "this test fails" do
raise "this is my failure message"
end
Fails with:
1) failing this test fails
Failure/Error: raise "this is my failure message"
RuntimeError:
this is my failure message
Alternatives
If you are thinking of using raise/fail in a spec, you should consider that there are probably more explicit ways of writing your expectation.
Additionally, raise/fail doesn't play well with aggregate_failures because the exception short-circuits the block and won't run any following matchers.
Mark a test as pending
If you need to mark a test as pending to make sure you get back to it, you could use the fail/raise, but you can also use pending.
# 🚫 Instead of this:
it "should do something" do
# ...
raise "this needs to be implemented"
end
# ✅ Try this:
it "should do something" do
pending "this needs to be implemented"
end
Assert that a block is not called
If you need to ensure a block is not being executed, consider using the yield matchers. For example:
describe "Enumerable#any?" do
# 🚫 Instead of this:
it "doesn't yield to the block if the collection is empty" do
[].any? { raise "it should not call this block" }
end
# ✅ Try this:
it "doesn't yield to the block if the collection is empty" do
expect { |b| [].any?(&b) }.not_to yield_control
end
end
I know that this was asked and answered many years ago, but RSpec::ExampleGroups has a flunk method. I prefer this flunk method to using fail in the context of testing. Using fail has an implied code failure (you can see more on that here: https://stackoverflow.com/a/43424847/550454).
So, you can use:
it 'is expected to fail the test' do
flunk 'explicitly flunking the test'
end
If you want to simulate an RSpec expectation failure rather than an exception, the method you're looking for is RSpec::Expectations.fail_with:
describe 'something' do
it "doesn't work" do
RSpec::Expectations.fail_with('oops')
end
end
# => oops
#
# 0) something doesn't work
# Failure/Error: RSpec::Expectations.fail_with('oops')
# oops
Note that despite the documentation, fail_with doesn't actually raise an ExpectationNotMetError directly, but rather passes it to the private method RSpec::Support.notify_failure. This is handy when using aggregate_failures, which (under the hood) works via a custom failure notifier.
describe 'some things' do
it "sometimes work" do
aggregate_failures('things') do
(0..3).each do |i|
RSpec::Expectations.fail_with("#{i} is odd") if i.odd?
end
end
end
end
# => some things
#
# Got 2 failures from failure aggregation block "things":
#
# 1) 1 is odd
#
# 2) 3 is odd
#
# 0) some things sometimes work
# Got 2 failures from failure aggregation block "things".
#
# 0.1) Failure/Error: RSpec::Expectations.fail_with("#{i} is odd") if i.odd?
# 1 is odd
#
# 0.2) Failure/Error: RSpec::Expectations.fail_with("#{i} is odd") if i.odd?
# 3 is odd
# sometimes work (FAILED - 1)