RSpec - testing instance variables within a controller - ruby-on-rails

I have a new action which creates a circle and assigns the current parent as its administrator:
def new
return redirect_to(root_path) unless parent
#circle = Circle.new(administrator: parent)
end
I'm trying to test that the administrator ID is properly set, and have written out my test as such:
context 'with a parent signed in' do
before do
sign_in parent
allow(controller).to receive(:circle).and_return(circle)
allow(Circle).to receive(:new).and_return(circle)
end
it 'builds a new circle with the current parent as administrator' do
get :new
expect(#circle.administrator).to equal(parent)
end
end
This obviously throws an error as #circle is nil. How can I access the new object that hasn't yet been saved from my controller tests? I'm guessing it is some variety of allow / let but as I say all my searches have yielded nothing so far.

You're approaching the problem wrong. Test the behavior of the controller. Not its implementation.
If this is a legacy application you can use assigns to access the #circle instance variable of the controller:
context 'with a parent signed in' do
before do
sign_in parent
end
it 'builds a new circle with the current parent as administrator' do
get :new
expect(assigns(:circle).administrator).to equal(parent)
end
end
But Rails 5 removes assigns and using it is not encouraged in new projects.
Instead I would use a feature spec and actually test the steps of creating a circle:
require 'rails_helper'
RSpec.feature 'Circles' do
let(:parent) { create(:parent) }
context "a guest user" do
scenario "can not create circles" do
visit new_circle_path
expect(page).to have_content 'Please sign in'
end
end
context "when signed in" do
background do
login_as parent
end
scenario "can create circles" do
visit new_circle_path
fill_in 'name', with: 'Test Circle'
expect do
click_button 'Create circle'
end.to change(parent.circles, :count).by(+1)
expect(page).to have_content 'Test Circle'
end
end
end

You can use assigns:
expect(assigns(:circle).administrator).to eq parent
However, note that with rails 5, this gets deprecated. The rational being that checking the assigned instance variables in controllers is too fragile and implementation specific.
The recommended alternative is either to test side effects (for example if this actually got persisted to the db) or do full feature tests.

Related

What is the proper way to test that a controller appropriately handles a uniqueness validation?

Summary
I am building a Rails app which includes a user registration process. A username and password are necessary to create a user object in the database; the username must be unique. I am looking for the right way to test that the uniqueness validation prompts a particular action of a controller method, namely UsersController#create.
Context
The user model includes the relevant validation:
# app/models/user.rb
#
# username :string not null
# ...
class User < ApplicationRecord
validates :username, presence: true
# ... more validations, class methods, and instance methods
end
Moreover, the spec file for the User model tests this validation using shoulda-matchers:
# spec/models/user_spec.rb
RSpec.describe User, type: :model do
it { should validate_uniqueness_of(:username)}
# ... more model tests
end
The method UsersController#create is defined as follows:
# app/controllers/users_controller.rb
class UsersController < ApplicationController
def create
#user = User.new(user_params)
if #user.save
render :show
else
flash[:errors] = #user.errors.full_messages
redirect_to new_user_url
end
end
# ... more controller methods
end
Since the User spec for username uniqueness passes, I know that a POST request which contains a username already in the database will cause UsersController#create to enter the else portion of the conditional, and I want a test to verify this situation.
Currently, I test how UsersController#create handles the uniqueness validation on username in the following manner:
# spec/controllers/users_controller_spec.rb
require 'rails_helper'
RSpec.describe UsersController, type: :controller do
describe 'POST #create' do
context "username exists in db" do
before(:all) do
User.create!(username: 'jarmo', password: 'good_password')
end
before(:each) do
post :create, params: { user: { username: 'jarmo', password: 'better_password' }}
end
after(:all) do
User.last.destroy
end
it "redirects to new_user_url" do
expect(response).to redirect_to new_user_url
end
it "sets flash errors" do
should set_flash[:errors]
end
end
# ... more controller tests
end
Issue
My primary concern is the before and after hooks. Without User.last.destroy, this test will fail when run in the future: The new record can't be created, and thus the creation of a second record with the same username doesn't occur.
Question
Should I be testing this particular model validation in the controller spec? If so, are these hooks the right/best way to accomplish this goal?
I'll steer away from an opinion on the 'should I...' part, but there are a couple of aspects worth considering. First, although controller tests have not been formally deprecated, they have generally been discouraged by both the Rails and Rspec teams for a while now. From the RSpec 3.5 release notes:
The official recommendation of the Rails team and the RSpec core team
is to write request specs instead. Request specs allow you to focus on
a single controller action, but unlike controller tests involve the
router, the middleware stack, and both rack requests and responses.
This adds realism to the test that you are writing, and helps avoid
many of the issues that are common in controller specs.
Whether or not the scenario warrants a corresponding request spec is a judgement call, but if you want to unit test the validation at the model level, check out the shoulda matchers gem, which assists with model validation testing).
In terms of your question about hooks, before(:all) hooks run outside a database transaction, so even if you have use_transactional_fixtures set to true in your RSpec configuration, they won't be automatically rolled back. So, a matching after(:all) like you have is the way to go. Alternatives include:
Creating the user inside a before(:each) hook, which does run in a transaction and is rolled back. That's at the potential cost of some test performance.
Use a tool like the Database Cleaner gem, which gives you fine-grained control over the strategies for cleaning your test databases.
If you want to cover the controller together with the user feedback aspect of this I would suggest a feature spec:
RSpec.feature "User creation" do
context "with duplicate emails" do
let!(:user) { User.create!(username: 'jarmo', password: 'good_password') }
it "does not allow duplicate emails" do
visit new_user_path
fill_in 'Email', with: user.email
fill_in 'Password', with: 'p4ssw0rd'
fill_in 'Password Confirmation', with: 'p4ssw0rd'
expect do
click_button 'Sign up'
end.to_not change(User, :count)
expect(page).to have_content 'Email has already been taken'
end
end
end
Instead of poking inside the controller this drives the full stack from the user story and tests that the view actually has an output for the validation errors as well - it thus provides value where a controller spec provides very little value.
Use let/let! to setup givens for a particular example as it has the advantage that you can reference them in the example through the helper method it generates. before(:all) should generally be avoided apart from stuff like stubbing out API's. Each example should have its own setup/teardown.
But you also need to deal with the fact that the controller itself is broken. It should read:
class UsersController < ApplicationController
def create
#user = User.new(user_params)
if #user.save
redirect_to #user
else
render :new, status: :unprocessable_entity
end
end
end
When a record is invalid you should NOT redirect back. Render the form again as you're displaying the result of performing a POST request. Redirecting back will make for a horrible user experience since all the fields will be blanked out.
When creating a resource is successful you should redirect the user to the newly created resource so that the browser URL actually points to the new resource. If you don't reloading the page will load the index instead.
This also removes the need to stuff the error messages in the session. If you want to give useful feedback through the flash you would do it like so:
class UsersController < ApplicationController
def create
#user = User.new(user_params)
if #user.save
redirect_to #user
else
flash.now[:error] = "Signup failed."
render :new, status: :unprocessable_entity
end
end
end
And you can test it with:
expect(page).to have_content "Signup failed."

Getting rid of repetitive rspec tests across contexts

Let's say I have various RSpec context blocks to group tests with similar data scenarios.
feature "User Profile" do
context "user is active" do
before(:each) { (some setup) }
# Various tests
...
end
context "user is pending" do
before(:each) { (some setup) }
# Various tests
...
end
context "user is deactivated" do
before(:each) { (some setup) }
# Various tests
...
end
end
Now I'm adding a new feature and I'd like to add a simple scenario that verifies behavior when I click a certain link on the user's page
it "clicking help redirects to the user's help page" do
click_on foo_button
expect(response).to have('bar')
end
Ideally I'd love to add this test for all 3 contexts because I want to be sure that it performs correctly under different data scenarios. But the test itself doesn't change from context to context, so it seems repetitive to type it all out 3 times.
What are some alternatives to DRY up this test set? Can I stick the new test in some module or does RSpec have some built in functionality to let me define it once and call it from each context block?
Thanks!
You can use shared_examples ... define them in spec/support/shared_examples.rb
shared_examples "redirect_help" do
it "clicking help redirects to the user's help page" do
click_on foo_button
expect(response).to have('bar')
end
end
Then in each of your contexts just enter...
it_behaves_like "redirect_help"
You can even pass a block to it_behaves_like and then perform that block with the action method, the block being unique to each context.
Your shared_example might look like...
shared_examples "need_sign_in" do
it "redirects to the log in" do
session[:current_user_id] = nil
action
response.should render_template 'sessions/new'
end
end
And in your context you'd call it with the block...
describe "GET index" do
it_behaves_like "need_sign_in" do
let(:action) {get :index}
end
...

Optimize Rails RSpec Tests

I'm working on a test for my Rails 4 app and I'm pretty new to using RSpec. I have a controller named AppsController which has the standard index, new, show, create... methods and they all work the way Rails suggest Etc. "new" creates a new instance of the object and create actually saves it, show, shows it and index shows all of the object. Here are my current tests can anyone see any potential problems or things that i could improve?
FactoryGirl.define do
factory :developer do
email 'example#me.com'
password 'new_york'
password_confirmation 'new_york'
tos '1'
end
factory :app do
name 'New App'
tos '1'
end
factory :invalid_app, parent: :app do
name 'nil'
tos '0'
end
end
require 'spec_helper'
def create_valid!
post :create, app: app_attributes
end
def create_invalid!
post :create, app: app_invalid_attributes
end
def show!
get :show, id: app
end
def update_valid!
put :update, id: app, app: app_attributes
end
def update_invalid!
put :update, id: app, app: app_invalid_attributes
end
def delete!
delete :destroy, id: app
end
def http_success
expect(response).to be_success
end
def expect_template(view)
expect(response).to render_template(view)
end
describe AppsController do
render_views
before(:each) do
#developer = FactoryGirl.create(:developer)
#developer.confirm!
sign_in #developer
end
let(:app) { FactoryGirl.create(:app, developer: #developer) }
let(:app_attributes) { FactoryGirl.attributes_for(:app) }
let(:app_invalid_attributes) { FactoryGirl.attributes_for(:invalid_app) }
describe 'GET #index' do
it 'responds with an HTTP 200 status' do
get :index
http_success
end
it 'renders the :index view' do
get :index
expect_template(:index)
end
it 'populates #apps with the current_developers apps' do
app = FactoryGirl.create(:app, :developer => #developer)
get :index
expect(assigns(:app)).to eq([app])
end
end
describe 'POST #create' do
context 'with valid parameters' do
it 'creates a new app' do
expect { create_valid!
}.to change(App, :count).by(1)
end
it 'redirects to the new app keys' do
create_valid!
expect(response).to redirect_to keys_app_path(App.last)
end
end
context 'with invalid parameters' do
it 'does not create the new app' do
expect { create_invalid!
}.to_not change(App, :count)
end
it 'renders the :new view' do
create_invalid!
expect_template(:new)
end
end
end
describe 'GET #show' do
it 'responds with an HTTP 200 status' do
show!
http_success
end
it 'renders the :show view' do
show!
expect_template(:show)
end
it 'populates #app with the requested app' do
show!
expect(assigns(:app)).to eq(app)
end
end
describe 'PUT #update' do
context 'with valid parameters' do
it 'locates the requested app' do
update_valid!
expect(assigns(:app)).to eq(app)
end
it 'changes app attributes' do
update_valid!
expect(app.name).to eq('Updated App')
end
it 'redirects to the updated app' do
update_valid!
expect(response).to redirect_to app
end
end
context 'with invalid parameters' do
it 'locates the requested app' do
update_invalid!
expect(assigns(:app)).to eq(app)
end
it 'does not change app attributes' do
update_invalid!
expect(app.name).to_not eq('Updated App')
end
it 'renders the :edit view' do
update_invalid!
expect_template(:edit)
end
end
end
describe 'DELETE #destroy' do
it 'deletes the app' do
expect { delete!
}.to change(App, :count).by(-1)
end
it 'redirects to apps#index' do
delete!
expect(response).to redirect_to apps_url
end
end
end
count should have been changed by -1, but was changed by 0 - on DELETE #destroy
expecting <"new"> but rendering with <[]> - on POST #create
expected: "Updated App"
got: "New App" - on PUT #update
expecting <"edit"> but rendering with <[]> - on PUT #update
expected: [#<App id: nil, unique_id: "rOIc5p", developer_id: 18, name: "New App">]
got: nil - on GET #index
Small thing - your #http_success method is testing the exact same thing twice.
You could also factor out the references to app by putting a #let statement right after your #before block:
let(:app) { FactoryGirl.create(:app, developer: #developer) }
then in your specs, just
it 'renders the :show view' do
get :show, id: app
expect_template(:show)
end
Edit:
Then the order of operations will be 1) the #developer is created in the #before block, 2) the spec is entered, 3) at the first reference to app in the spec, the #let block will create an instance of an app.
That means you can't factor out the app creation in the #index spec, because in that case the spec will call the action before it creates the app.
A few things I thought of, reading your code:
You don't need to include parens on method calls taking no arguments. Just http_success will work.
You should get try to use the modern RSpec expectation syntax consistently. Instead of assigns(:app).should eq(app), use expect(assigns(:app)).to eq(app). (There's one exception to this, which is expectations on mocks (ie. should_receive(:message)), which will only take on the modern expect-to syntax as of RSpec 3.
For controller specs, I like to create little methods for each action that actually invokes the action. You'll notice you call get :show, id: app several times in the GET #show specs. To DRY up your specs a little more, you could instead write the following method within the describe block:
def show!
get :show, id: app
end
Try to use one Hash syntax consistently. What's more, Rails 4 can't be run with Ruby 1.8, so there's (nearly) no reason to use the hash-rocket Hash syntax.
If I'm getting really, really picky, I usually consider instance variables in a spec to be a smell. In almost all cases, instance variables should be refactored to a memoized let/given blocks.
If I'm getting really, really, really picky, I prefer to think of controller specs such as yours as strictly a unit test of the controller, not an integration test (that's what Capybara is for), and as such you shouldn't be exercising your model layer at all. You should only be testing that your controller is sending the right messages to the model layer. In other words, all the model layer stuff should be stubbed out. For example:
describe 'GET #show' do
let(:app) { stub(:app) }
before do
App.stub(:find).and_return(app)
end
it 'populates #app' do
get :show, id: app
assigns(:app).should eq(app)
end
end
I know this last is a personal preference, not a metaphysical truth or even necessarily a wide-spread standard convention, so you may choose to take it or leave it. I prefer it, because it keeps my specs very speedy, and gives me a very clear heuristic for when my controller actions are doing too much, and I might need to consider refactoring. It could be a good habit to get into.
First, I'm not certain but I suspect your invalid app factory may be wrong. Did you mean
factory :invalid_app, parent: :app do
name nil
tos '0'
end
nil as a ruby NilClass not "nil" as a string?
As for other comments about cleanup and stuff, here are a few of my thoughts.
You can avoid the need for some of your helper methods and duplication by using before blocks for each describe. Taking just your index tests you could have something more like
describe 'GET #index' do
before do
get :index
end
it 'responds with an HTTP 200 status' do
http_success
end
it 'renders the :index view' do
expect_template(:index)
end
it 'populates #apps with the current_developers apps' do
expect(assigns(:app)).to eq([app])
end
end
Notice also that you don't need to recreate app because your let is doing it as necessary.
On the failures, I suspect the delete count change could be failing because inside the expectation, the test framework is creating a new app (from the let) and then deleting it leading to a count change of 0. For that test, you need to make sure you're app is created outside of your expectation. Because you are using let, you could do that like this:
describe 'DELETE #destroy' do
it 'deletes the app' do
# ensure that app is already created
app
expect {
delete!
}.to change(App, :count).by(-1)
end
end
alternatively, change the let to a let! which will force the creation before the specs actually run.
As for other failures, thought #DanielWright suggested the helper methods, I find those complicate the debug. I can't see where you set the app name to "Updated App", for example. Perhaps a clearer test (for that particular one) would not use the helper methods but could be more explicit. Something like
describe 'PUT #update' do
let(:app_attributes) { FactoryGirl.attributes_for(:app, name: 'The New App Name') }
before do
put :update, id: app, app: app_attributes
end
context 'with valid parameters' do
it 'locates the requested app' do
expect(assigns(:app)).to eq(app)
end
it 'changes app attributes' do
# notice the reload which will make sure you refetch this from the db
expect(app.reload.name).to eq('The New App Name')
end
it 'redirects to the updated app' do
expect(response).to redirect_to app
end
end
end
For the other errors, you might want to start debugging your code. Are you certain it should work? Have you looked at output logs? Maybe the tests are doing there job and finding errors in your controller code. Have you done any step-through debugging?

Example Groups with the same expectation

A logged in user has access to a resource and can get there in different ways. I want to have an example group, that each test for the same expectation.
I put an page.should have_content("...") expectation in an after(:each) block, but that is not such a good solution: If I declare it pending, it fails anyway. And if it fails, the error appears (at first) white.
How should I write example groups that each have the same expectation?
It sounds like you want a shared example group:
describe 'foo' do
shared_examples "bar" do
it 'should ...' do
end
end
context "when viewing in the first way" do
before(:each) do
...
end
it_behaves_like 'bar'
end
context "when viewing in the second way" do
before(:each) do
...
end
it_behaves_like 'bar'
end
end
Within the before blocks you set things up so that the action is taken out in the correct way. Another way of doing this is to have your shared examples call a do_foo method and provide different implementations of do_foo in each context.
You can also have shared contexts if what you want to share is the setup stuff.

Rspec conditional assert: have_content A OR have_content B

I know this would be a really newbie question, but I had to ask it ...
How do I chain different conditions using logic ORs and ANDs and Rspec?
In my example, the method should return true if my page has any of those messages.
def should_see_warning
page.should have_content(_("You are not authorized to access this page."))
OR
page.should have_content(_("Only administrators or employees can do that"))
end
Thanks for help!
You wouldn't normally write a test that given the same inputs/setup produces different or implicit outputs/expectations.
It can be a bit tedious but it's best to separate your expected responses based on the state at the time of the request. Reading into your example; you seem to be testing if the user is logged in or authorized then showing a message. It would be better if you broke the different states into contexts and tested for each message type like:
# logged out (assuming this is the default state)
it "displays unauthorized message" do
get :your_page
response.should have_content(_("You are not authorized to access this page."))
end
context "Logged in" do
before
#user = users(:your_user) # load from factory or fixture
sign_in(#user) # however you do this in your env
end
it "displays a permissions error to non-employees" do
get :your_page
response.should have_content(_("Only administrators or employees can do that"))
end
context "As an employee" do
before { #user.promote_to_employee! } # or somesuch
it "works" do
get :your_page
response.should_not have_content(_("Only administrators or employees can do that"))
# ... etc
end
end
end

Resources