I'm trying to write terse tests for an API controller, but I'm having trouble with the "one-liner" syntax offered by RSpec.
I'm overriding the subject explictly to refer to the action of posting rather than the controller:
let (:params) { some_valid_params_here }
subject { post :create, params }
When I use the one-liner syntax to test http_status, it works fine:
it { is_expected.to have_http_status(:created) }
# pass!
But when I try to use it for a different expectation, it blows up:
it { is_expected.to change{SomeActiveRecordModel.count}.by(1) }
# fail! "expected result to have changed by 1, but was not given a block"
Notably, when I run this second expectation in a longer form, calling on subject explictly, it works:
it "creates a model" do
expect{ subject }.to change{SomeActiveRecordModel.count}.by(1)
end
# pass
Is this just a weakness of the one-liner syntax, that it can't handle this more complicated expression? Or have I misunderstood something about how subject is inferred into these tests?
(NB: I know that setting the subject to an action has some detractors, and I'm happy to hear opinions, but that isn't the aim of this question).
You can do it like this
subject { -> { post :create, params } }
and then
it { is_expected.to change(SomeActiveRecordModel, :count).by(1) }
Here you have very nice discussion about this
github_topic
As it was said before there's a simple solution involving subject ; it does not require a lambda tho ; I applied it this way for my CategoryController#show :
describe '#show' do
subject { get :show, id: category }
context "as guest" do
it { is_expected.to render_template('show') }
end
end
Related
I have just upgraded to Rails 5. In my specs I have the following
expect(model).to receive(:update).with(foo: 'bar')
But, since params no longer extends Hash but is now ActionController::Parameters the specs are failing because with() is expecting a hash but it is actually ActionController::Parameters
Is there a better way of doing the same thing in Rspec such as a different method with_hash?
I can get around the issue using
expect(model).to receive(:update).with(hash_including(foo: 'bar'))
But that is just checking if the params includes that hash, not checking for an exact match.
You could do:
params = ActionController::Parameters.new(foo: 'bar')
expect(model).to receive(:update).with(params)
However it still smells - you should be testing the behaviour of the application - not how it does its job.
expect {
patch model_path(model), params: { foo: 'bar' }
model.reload
}.to change(model, :foo).to('bar')
This is how I would test the integration of a controller:
require 'rails_helper'
RSpec.describe "Things", type: :request do
describe "PATCH /things/:id" do
let!(:thing) { create(:thing) }
let(:action) do
patch things_path(thing), params: { thing: attributes }
end
context "with invalid params" do
let(:attributes) { { name: '' } }
it "does not alter the thing" do
expect do
action
thing.reload
end.to_not change(thing, :name)
expect(response).to have_status :bad_entity
end
end
context "with valid params" do
let(:attributes) { { name: 'Foo' } }
it "updates the thing" do
expect do
action
thing.reload
end.to change(thing, :name).to('Foo')
expect(response).to be_successful
end
end
end
end
Is touching the database in a spec inheritenly bad?
No. When you are testing something like a controller the most accurate way to test it is by driving the full stack. If we in this case had stubbed out #thing.update we could have missed for example that the database driver threw an error because we where using the wrong SQL syntax.
If you are for example testing scopes on a model then a spec that stubs out the DB will give you little to no value.
Stubbing may give you a fast test suite that is extremely brittle due to tight coupling and that lets plenty of bugs slip through the cracks.
I handled this by creating in spec/rails_helper.rb
def strong_params(wimpy_params)
ActionController::Parameters.new(wimpy_params).permit!
end
and then in a specific test, you can say:
expect(model).to receive(:update).with(strong_params foo: 'bar')
It's not much different from what you're already doing, but it makes the awkward necessity of that extra call a little more semantically meaningful.
#max had good suggestions about how to avoid this altogether, and I agree they switched away from a hash to discourage using them with hashes interchangeably.
However, if you still want to use them, as a simple hack for more complex situations (for instance if you expect using a a_hash_including), you can try using something like this:
.with( an_object_satisfying { |o|
o.slice(some_params) == ActionController::Parameters.new(some_params)
})
There is the following spec:
require 'spec_helper'
describe Place do
let(:place) { FactoryGirl.create(:place) }
subject { place }
it { expect be_valid }
describe 'when content is not present' do
before { place.content = nil }
it { expect be_valid }
end
end
Also there is validation for presence content in Place model. But this spec doesn't throw any exception, even if the last instruction sets 'content' as nil. What's the trouble? Thanks.
You are just setting an expectation but never calling anything.
Replace your expect be_valid calls with
it { expect(subject).to be_valid }
...
it { expect(place).to be_valid }
I guess you are coming from the old should syntax. There you could have written it the way you did:
it { should be_valid }
But the newer expect syntax behaves slightly different
You have let(:place) { FactoryGirl.create(:place) }, so when you reference place for the first time then it creates an instance (with valid content). You are then changing content, but validation is not performed on it.
There are a few ways for doing what you need:
Use the factory that you already have, but instead of create use build
Define an invalid factory at FactoryGirl:
factory :place do
content 'valid value'
factory :invalid_place do
content nil
end
end
3) Or you can use attributes_for
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).
I'm refactoring my model rspecs as to be "as DRY" as possible, leading to something like
require 'spec_helper'
describe Model do
subject { build(:model) }
it { should be_valid }
it { should validate_presence_of(:description) }
it { should ensure_length_of(:description).is_at_least(3).is_at_most(255) }
it { should validate_presence_of(:position) }
it { should validate_numericality_of(:position).is_greater_than_or_equal_to(1) }
end
Now, every file starts with
subject { build(:model) }
it { should be_valid }
so, you guess it, I would like to get rid of these two lines as well...
Any suggestions?
The it { should be_valid } test seems to be testing only your factory. It's not really important to the function of the Model. Consider moving these tests to a single factories_spec test if you'd like to test them. See: https://github.com/thoughtbot/suspenders/blob/master/templates/factories_spec.rb
The matchers you are using in your example don't really require a model built with FactoryGirl. They will work fine with the implicit, default subject (Model.new). When that's not the case, I'd suggest defining as much of the state of your test as possible inside the test -- that is, inside the it blocks. If that results in some duplication, so be it. Particularly costly duplication can be extracted to method calls, which are preferable to subject, let and before because there's no magic to them. As a developer coming back to the project in 6 months, looking at spec on line 75, you'll know exactly what the setup is.
See: http://robots.thoughtbot.com/lets-not
You can use rspec shared examples:
shared_examples "a model" do
subject { build described_class }
it { should be_valid }
end
describe Foo do
it_behaves_like "a model"
end
describe Bar do
it_behaves_like "a model"
end
I'm working hard trying to keep my spec files as clean as possible. Using 'shoulda' gem and writing customized matchers that follow the same pattern.
My question is about creating a custom matcher that would wrap expect{ post :create ... }.to change(Model, :count).by(1) and could be used in the same example groups with other 'shoulda' matchers. Details bellow:
Custom matcher (simplified)
RSpec::Matchers.define :create_a_new do |model|
match do |dummy|
::RSpec::Expectations::ExpectationTarget.new(subject).to change(model, :count).by(1)
end
end
Working example
describe 'POST create:' do
describe '(valid params)' do
subject { -> { post :create, model: agency_attributes } }
it { should create_a_new(Agency) }
end
end
This work OK as long as I use a subject lambda and my matcher is the only one in the example group.
Failing examples
Failing example 1
Adding more examples in the same group makes the other matcher fail because subject is now a lambda instead of an instance of the Controller.
describe 'POST create:' do
describe '(valid params)' do
subject { -> { post :create, model: agency_attributes } }
it { should create_a_new(Agency) }
it { should redirect_to(Agency.last) }
end
end
Failing example 2
The 'shoulda' matcher expect me to define a before block, but this become incompatible with my custom matcher
describe 'POST create:' do
describe '(valid params)' do
before { post :create, agency: agency_attributes }
it { should create_a_new(Agency) }
it { should redirect_to(Agency.last) }
end
end
Expected result
I am looking for a way to write my custom matcher that would fit in the same example group as other matchers, meaning my custom matcher should use the before block to execute the controller action, the "failing example #2" above is the way I would like to write my specs. Is it possible?
Thanks for reading
I do not think there is a way you can get your failing examples passing.
This is because change really needs a lambda, since it needs to perform your count twice (once before, and once after calling it). That's the reason I tend not to use it (or use it in context isolation).
What I usually do, instead of using the count matcher, is checking three things:
The record is persisted. If I assign the model to #model, then I use expect(assigns(:model)).to be_persisted
The record is an instance of the expected model (though might not seem useful, it is
quite descriptive when using an STI). expect(assigns(:model)).to be_a(Model).
Check the last record in DB is the same as the one I just create `expect(assigns(:model)).to eq(Model.last)``
And that's the way I usually test the change matcher without using it. Of course, you can now create your own matcher
RSpec::Matchers.define :create_a_new do |model|
match do |actual|
actual.persisted? &&
actual.instance_of?(Participant) &&
(Participant.last == actual)
end
end