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
paramshash 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_keysmethod on theaccount_attributeshash 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.