Getting rid of repetitive rspec tests across contexts - ruby-on-rails

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
...

Related

RSpec - testing instance variables within a controller

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.

Testing Controllers in Rails 4

I am looking for clarification and an understanding on how to effectively test my controllers with Rspec, I don't want to write tests that are not testing the potential issues at hand.
My scenario is as follows.
I am using Active Admin to create a Category, to do so you must obviously be logged into Active Admin.
What I want to ensure is that
1) A logged in user can create a Category
2) A Category cannot be created if you are not logged in
3) Attempts to create a Category outside of active admin are met with a 404 template
So what i have so far (and i really want to check i haven't gone over the top or performing unnecessary tests) is as follows.
spec/controllers/categories_controller_spec.rb
require 'rails_helper'
include Warden::Test::Helpers
# Ensure 404 pages are returned when requesting URLS
RSpec.describe CategoriesController, type: :request do
describe 'Routes' do
context 'All CRUD actions render 404' do
it '#create' do
post '/categories'
expect(response.status).to eq(404)
expect(response).to render_template(:file => "#{Rails.root}/public/404.html.erb")
end
# All other actions here
end
end
end
RSpec.describe Admin::CategoriesController, type: :request do
describe 'No Authorised Login' do
context 'All CRUD actions redirect correctly' do
it 'redirects when accessing #index' do
get '/admin/categories'
expect(response.status).to eq(302)
expect(response).to redirect_to(admin_root_path + '/login')
end
# All other actions here
end
end
end
# Ensure actions in admin can be carried out if logged in
RSpec.describe Admin::CategoriesController, type: :request do
before(:each) do
#user = FactoryGirl.create(:admin_user)
login_as #user
end
after(:each) do
#user.destroy
end
describe 'Authorised Login' do
context 'All CRUD actions perform as expected' do
it 'navigates to Categories #index' do
get '/my_admin_panel/categories'
expect(response.status).to eq(200)
expect(response).to render_template(:index)
end
# All other actions here
end
end
spec/routing/categories_routing.spec
RSpec.describe CategoriesController, type: :routing do
describe 'Routes' do
it 'does not get #index' do
expect(get: '/categories').to route_to(
controller: 'application',
action: 'raise_not_found',
unmatched_route: 'categories'
)
end
end
end
Should I be testing post /categories without supplying params, is that a wasted test? Am I over complicating what should be a simple set of tests ?
This is a judgement/style question and, as such, is not ideal for the StackOverflow format. That said, I don't think your testing is over the top. Some other thoughts:
Some people choose treat their controller tests as integration tests.
You can take advantage of RSpec's shared examples to DRY up your tests
In the default RSpec configuration, the database will be cleaned after each test, so you don't need to explicitly destroy the ActiveRecord objects you create if it's the database you're worried about
In general, I think testing behavior for programmatic actions you don't expect to happen (e.g. posting to undefined routes) is worthwhile unless you are trying to test specific error handling code

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

Dynamically test each method in a controller

I have an Ruby on Rails 3 admin_controller with the default set of CRUD, index and so on methods. I'd like to test each of these for certain assertions with rspec.
Like response.should render_template("layouts/some_layout") or tests that it should require login.
Copy-pasting that test into the group of tests for each method is a lot of duplication. IMO it makes little sense to have an
it 'should require login' do
Duplicated several times troughout that test.
Is there a simple way to run a test on a list of methods? Say defined_methods.each do |method| it 'should' .... of some sort?
Is this a good way in the first place? Or am I taking a wrong route in the first place?
Given that you really want all those assertions, have you considered shared example groups?
shared_examples_for "an action that requires authentication" do
it "should render successfuly" do
sign_in(user)
response.should be_success # or whatever
end
it "should deny access" do
# don't sign_in the user
# assert access was denied
end
end
shared_examples_for "another behaviour" do
# ...
end
let(:user) { create_user }
describe "#index" do
before(:each) { get :index }
it_behaves_like "an action that requires authentication"
it_behaves_like "another behaviour"
end
describe "#show" do
before(:each) { get :show }
it_behaves_like "an action that requires authentication"
end
# ...
Of course before writing large number of specs for a basic functionality you should always check if it isn't already tested by the library that is providing the functionality (e.g. checking for the rendered template, if it is handled by rails's implicit rendering, might be a bit overkill).
If you wanted to go down the route of iteratively testing each public method in the controller, you could do something like:
SomeController.public_instance_methods(false).each do |method|
it "should do something"
end
However, I think a shared example group (see about half way down this page: http://rspec.info/documentation/) would be prettier. If it were extracted so it could be used across all your controller specs, it'll be even nicer..
shared_examples_for "admin actions" do
it "should require login"
end
Then in each controller spec:
describe SomeController do
it_should_behave_like "admin actions"
end
Just add it to your test_helper.rb, something like:
def requires_login
...
end

Resources