Testing nested resources with RSpec results in a strange failure - ruby-on-rails

I wrote this to test my controller's create action which uses a nested resources. I have an Account model with a has_many :users association. Upon sign up, an Account with a single user gets created.
describe "POST #create", focus: true do
let(:account) { mock_model(Account).as_null_object }
before do
Account.stub(:new).and_return(account)
end
it "creates a new account object" do
account_attributes = FactoryGirl.attributes_for(:account)
user_attributes = FactoryGirl.attributes_for(:user)
account_attributes[:users] = user_attributes
Account.should_receive(:new).with(account_attributes).and_return(account)
post :create, account: account_attributes
end
end
This is the failure output I'm getting; notice the difference between expected and got: it expected a symbol while it got a string.
1) AccountsController POST #create creates a new account object
Failure/Error: Account.should_receive(:new).with(account_attributes).and_return(account)
<Account(id: integer, title: string, subdomain: string, created_at: datetime, updated_at: datetime) (class)> received :new with unexpected arguments
# notice that expected has symbols while the other users strings...
expected: ({:title=>"ACME Corp", :subdomain=>"acme1", :users=>{ ... }})
got: ({"title"=>"ACME Corp", "subdomain"=>"acme1", "users"=>{ ... }})
# ./spec/controllers/accounts_controller_spec.rb:34:in `block (3 levels) in <top (required)>'
I can't help but notice that this code also smells a little. I don't know if I am going about this right. I'm new to RSpec, so bonus points if you can provide some feedback on my effort.

The params hash generally contains keys that are strings instead of symbols. While we do access them using symbols, it is due to the fact that it is a Hash with indifferent access, which doesn't care whether it is accessed using strings or symbols.
For your test to pass, you can use the stringify_keys method on the account_attributes hash when setting the expectation. Then, when Rspec compares the hashes both will be string-keyed.
Now, about the review you asked: instantiating an account is really an expectation that you have upon your controller? Your tests will be less brittle if you place your assertions/expectations upon more concrete, externally-visible behaviors, instead of upon each method that your object should use.
Rails controllers are generally brittle to test, because there are many equivalent ways to manipulate ActiveRecord models... I generally try to make my controllers as dumb as possible, and them I don't unit test them, leaving their behavior to be covered by higher-level integration tests.

Related

RSpec not seeing changes to ActiveRecord object with `change(object, :message)` syntax

I have three models:
User
Company
Commitment
Commitment is a HABTM join table for Users and Companies (i.e., when a User joins a Company, it creates a new Commitment). It also has a few extra columns/attributes:
admin (does the user have admin privileges for this company?)
confirmed_by_admin (has a company admin confirmed this user's request to join the company?)
confirmed_by_member (has the user himself confirmed an invitation to join the company?)
I've also defined a convenience method to quickly determine if a commitment is fully confirmed:
class Commitment < ApplicationRecord
def confirmed?
confirmed_by_admin? && confirmed_by_member?
end
end
Now I'm writing a request spec, but for some reason, the change matcher only works with one of its two syntaxes:
let :carol { FactoryGirl.create(:user) }
let :company { FactoryGirl.create(:company) }
it 'confirms invitation to join company' do
# Initialize unconfirmed commitment
FactoryGirl.create(:commitment, user: carol,
company: company,
confirmed_by_admin: true)
expect do
patch commitment_path(carol.commitments.first),
params: { commitment: { confirmed_by_member: true } }
# for the following syntaxes, ------------------------------------------------
# this works:
end.to change { carol.commitments.first.confirmed?) }.from(false).to(true)
# and this fails:
end.to change(carol.commitments.first, :confirmed?).from(false).to(true)
# ----------------------------------------------------------------------------
end
It appears that carol.commitments.first isn't being reloaded when RSpec tests for the change — I get the following test output:
Failure/Error:
expect do
patch commitment_path(carol.commitments.first),
params: { commitment: { confirmed_by_member: true } }
end.to change(Commitment.find_by(user: carol, company: company), :confirmed?).from(false).to(true)
expected #confirmed? to have changed from false to true, but did not change
# ./spec/requests/commitments_spec.rb:69:in `block (3 levels) in <top (required)>'
What gives? Clearly I can just stick to the curly-brace / block syntax, but I'd like to understand why one works and not the other.
Upon inspecting docs and trying out myself a new rails project replicating your scenarios, and also failing, I believe that the reason why it was failing is because
the "block" form of .change is ran twice ("before" and "after" the expect block), whatever is inside of that block:
.change{ carol.commitments.first.confirmed? }
while the "method" form of .change is ran once for the first argument: carol.commitments.first, but ran twice for the second argument :confirmed?. However, the problem with this is that the the carol.commitments.first at this point inside the spec file does not share the same memory space as that object that has been actually updated in your commitments_controller#update (most likely that object is named #commitment). Although they are the same Commitment record, they are separate instances, and the attribute-values of the other do not automatically changes when the another one changed.
Consider the following which demonstrates a scenario in which this "method" form works:
it 'sometest' do
commitment = FactoryGirl.create(:commitment, user: carol,
company: company,
confirmed_by_admin: true)
expect do
# this commitment object is exactly the same object passed in the `change` below
commitment.confirmed_by_member = true
end.to change(commitment, :confirmed?).from(false).to(true)
end
Disclaimer: This is unverified, but because it was too complex for me write as a comment (with all the sample test code), I wrote it here as an answer. Should anyone know any better, please do let me know.

How do I check for an existing user in Rspec on a Post#Create?

I am trying to write a test for my InvitationsController#Create.
This is a POST http action.
Basically what should happen is, once the post#create is first executed, the first thing that needs to do is we need to check to see if an User exists in the system for the email passed in via params[:email] on the Post request.
I am having a hard time wrapping my head around how I do this.
I will refactor later, but first I want to get the test functionality working.
This is what I have:
describe 'POST #create' do
context 'when invited user IS an existing user' do
before :each do
#users = [
attributes_for(:user),
attributes_for(:user),
attributes_for(:user)
]
end
it 'correctly finds User record of invited user' do
expect {
post :create, invitation: attributes_for(:member, email: #users.first.email)
}.to include(#users.first[:email])
end
end
end
This is the error I get:
1) Users::InvitationsController POST #create when invited user IS an existing user correctly finds User record of invited user
Failure/Error: expect {
You must pass an argument rather than a block to use the provided matcher (include "valentin#parisian.org"), or the matcher must implement `supports_block_expectations?`.
# ./spec/controllers/users/invitations_controller_spec.rb:17:in `block (4 levels) in <top (required)>'
I am not surprised by the error, because the Test doesn't feel right to me. I just can't quite figure out how to test for this without writing code in my controller#action.
I am using FactoryGirl and it works perfectly, in the sense that it returns valid data for all the data-types. The issue here is how do I get RSpec to actually test for the functionality I need.
The error you are getting is a syntax error, nothing related to whatever your action is supposed to do.
The code you have there it is being interpreted as you are passing a block ({}) to the expect method.
I'd change it to something like
it 'correctly finds User record of invited user' do
post :create, { email: #users.first[:email] }
expect(response).to include(#users.first[:email])
end
Assuming that the response of the create action returns the email as plain text, which seems weird to me.
Also note that I have email directly passed to the post since you mentioned you were expecting it in params[:email] but by the test you wrote seems like you were expecting it in params[:invitation][:email].
Change that part if that is the case.

RSpec: Expect to change multiple

I want to check for many changes in a model when submitting a form in a feature spec. For example, I want to make sure that the user name was changed from X to Y, and that the encrypted password was changed by any value.
I know there are some questions about that already, but I didn't find a fitting answer for me. The most accurate answer seems like the ChangeMultiple matcher by Michael Johnston here: Is it possible for RSpec to expect change in two tables?. Its downside is that one only check for explicit changes from known values to known values.
I created some pseudo code on how I think a better matcher could look like:
expect {
click_button 'Save'
}.to change_multiple { #user.reload }.with_expectations(
name: {from: 'donald', to: 'gustav'},
updated_at: {by: 4},
great_field: {by_at_leaset: 23},
encrypted_password: true, # Must change
created_at: false, # Must not change
some_other_field: nil # Doesn't matter, but want to denote here that this field exists
)
I have also created the basic skeleton of the ChangeMultiple matcher like this:
module RSpec
module Matchers
def change_multiple(receiver=nil, message=nil, &block)
BuiltIn::ChangeMultiple.new(receiver, message, &block)
end
module BuiltIn
class ChangeMultiple < Change
def with_expectations(expectations)
# What to do here? How do I add the expectations passed as argument?
end
end
end
end
end
But now I'm already getting this error:
Failure/Error: expect {
You must pass an argument rather than a block to use the provided matcher (nil), or the matcher must implement `supports_block_expectations?`.
# ./spec/features/user/registration/edit_spec.rb:20:in `block (2 levels) in <top (required)>'
# /Users/josh/.rvm/gems/ruby-2.1.0#base/gems/activesupport-4.2.0/lib/active_support/dependencies.rb:268:in `load'
# /Users/josh/.rvm/gems/ruby-2.1.0#base/gems/activesupport-4.2.0/lib/active_support/dependencies.rb:268:in `block in load'
Any help in creating this custom matcher is highly appreciated.
In RSpec 3 you can setup multiple conditions at once (so the single expectation rule is not broken). It would look sth like:
expect {
click_button 'Save'
#user.reload
}.to change { #user.name }.from('donald').to('gustav')
.and change { #user.updated_at }.by(4)
.and change { #user.great_field }.by_at_least(23}
.and change { #user.encrypted_password }
It is not a complete solution though - as far as my research went there is no easy way to do and_not yet. I am also unsure about your last check (if it doesn't matter, why test it?). Naturally you should be able to wrap it within your custom matcher.
If you want to test that multiple records were not changed, you can invert a matcher using RSpec::Matchers.define_negated_matcher. So, add
RSpec::Matchers.define_negated_matcher :not_change, :change
to the top of your file (or to your rails_helper.rb) and then you can chain using and:
expect{described_class.reorder}.to not_change{ruleset.reload.position}.
and not_change{simple_ruleset.reload.position}
BroiSatse's answer is the best, but if you are using RSpec 2 (or have more complex matchers like .should_not), this method also works:
lambda {
lambda {
lambda {
lambda {
click_button 'Save'
#user.reload
}.should change {#user.name}.from('donald').to('gustav')
}.should change {#user.updated_at}.by(4)
}.should change {#user.great_field}.by_at_least(23)
}.should change {#user.encrypted_password}
The accepted answer is not 100% correct since the full compound matcher support for change {} has been added in RSpec version 3.1.0. If you try to run the code given in accepted answer with the RSpec version 3.0, you would get an error.
In order to use compound matchers with change {}, there are two ways;
First one is, you have to have at least RSpec version 3.1.0.
Second one is, you have to add def supports_block_expectations?; true; end into the RSpec::Matchers::BuiltIn::Compound class, either by monkey patching it or directly editing the local copy of the gem. An important note: this way is not completely equivalent to the first one, the expect {} block runs multiple times in this way!
The pull request which added the full support of compound matchers functionality can be found here.

Why is Rspec using the wrong object for a controller test?

We have an API which we returns some structured JSON data. Sites have_many :controllers, and Controllers belong_to :site
For the test, we have to create a mock site and controller, which is achieved in all our other feature test files exactly like I have it listed below in the before(:each) do block.
Test:
describe Api::V2::SitesController, :type => :controller do
render_views
before(:each) do
basic_auth_and_skip_hmac
#site = FactoryGirl.create(:site)
#user_site = FactoryGirl.create(:user_site, user: #user, site: #site)
#controller = FactoryGirl.create(:controller, site: #site)
end
it 'List all sites' do
get :index, format: :json
puts response.body
expect(response.body).to include("Site 1")
expect(response.body).to include("Controller 1")
end
end
But the response for this controller test is unexpected:
Api::V2::SitesController
List all sites (FAILED - 1)
Failures:
1) Api::V2::SitesController List all sites
Failure/Error: get :index, format: :json
NoMethodError:
undefined method `response_body=' for #<Controller:0x0000010db0c2d8>
Why do you even care about response_body for the Controller object Rspec? It clearly states at the top that we're describing the SitesController!
Removing the creation of the controller object and the matching expectation at the bottom of the file makes the test pass as expected:
Finished in 0.60435 seconds (files took 5.38 seconds to load)
1 example, 0 failures
But I'm not really testing everything I set out to test because my JSON includes:
"controllers":[]
Which technically cannot happen in our application. The controller is the most important unit to measure for us, so returning a JSON response with valid site information but no controllers would be pointless.
As shown in the discussion above with Mike - it turns out that "#controller" is special to Ruby.
And I happen to work in probably the only industry where this becomes a naming conflict. We manage a service for irrigation controllers, so the word is always messing with my head - am I talking about MVC controller or the actual device?
It's been a burden that probably no one else will ever encounter as it's just not a variable you would ever think to use.
In summary - don't ever call #controller, pretty much anywhere.

undefined local variable or method `request'

I am new to ruby on rails. I am getting an undefined method error when I run rspec on comment_spec.rb
1) after_save calls 'Post#update_rank' after save
Failure/Error: request.env["HTTP_REFERER"] = '/'
NameError:
undefined local variable or method `request' for #<RSpec::ExampleGroups::AfterSave:0x007fa866ead8d0>
# ./spec/models/vote_spec.rb:45:in `block (2 levels) in <top (required)>'
This is my spec:
require 'rails_helper'
describe Vote do
....
describe 'after_save' do
it "calls 'Post#update_rank' after save" do
request.env["HTTP_REFERER"] = '/'
#user = create(:user)
#post = create(:post, user: #user)
sign_in #user
vote = Vote.new(value:1, post: post)
expect(post). to receive(:update_rank)
vote.save
end
end
Any help that you would have would be greatly appreciated...
I was following the apirails book tutorial chapter 3 here
http://apionrails.icalialabs.com/book/chapter_three
I was receiving the same error and DrPositron's solution worked for me, all green again. Just needed to add ":type => :controller" on my block like so:
describe Api::V1::UsersController, :type => :controller do
end
Hope this helps someone
OK here's the deal.
Vote is a model, i suppose.
You are writing a test for that model.
There's a difference between model tests ("the domain logic is doing what its supposed to") and feature/integration tests ("the application is behaving the way its supposed to").
The request variable is associated with feature or controller tests.
So what's wrong?
You are not logging in users in model tests, just check if the update_rank method is being called on save, thats it.
No user-interaction jazz in model tests.
Hope that helps!
Cheers
Jan
So Louis, just to expand on Jan's response:
You appear to be writing a model spec. The purpose of a model spec is simply to test how your model classes work, and that behavior is testable without having to pay any attention to the application logic around signing in, making "requests" to particular controllers, or visiting particular pages.
You're essentially just testing a couple related Ruby classes. For this, we don't need to think about the whole app -- just the classes we're testing.
As a consequence, RSpec doesn't make certain methods available in the spec/models directory -- you're not supposed to think about requests or authentication in these tests.
It looks like your test is simply designed to make sure that when you create a vote for a post, it updates that post's rank (or, specifically, call's that post's update_rank method). To do that, you don't need to create a user, or sign a user in, or pay any attention to the request (what request would we be referring to? We're just testing this as if in Rails console, with no HTTP request involved).
So you could basically remove the first four lines of your test -- apart from the line creating your post, and the post's user if it's necessary (if the post model validates the presence of a user). Don't sign a user in -- we're just testing a Ruby class. There's no concept of a website to sign into in this test.
Then, as a last thing to take care of to get your spec to pass, make sure to refer to the post you create by the right name. Right now, you're creating a post and assigning it to the #post variable, but then you're referring to just post later on. post doesn't exist; just #post. You'll have to pick one variable name and stick with it.
Also, if you are using RSpec 3, file type inference is now disabled by default and must be opted in as described here. If you're new to RSpec, a quick overview of the canonical directory structure is here.
For example, for a controller spec for RelationshipsController, insert , :type => :controller as such:
describe RelationshipsController, :type => :controller do
#spec
end

Resources