I'm a Ruby on Rails developer and I was testing a fairly simple Rails application using RSpec. I was writing some Routing specs and then I faced this problem:
My routes are like this:
Rails.application.routes.draw do
root 'trip_plans#index'
resources :trip_plans
end
So I have a route like post /trip_plans for creating new plans and triggering the trip_plans#create action.
My Routing spec file spec/routing/trip_plans_spec.rb looks like this:
require 'rails_helper'
RSpec.describe 'trip_plans routes', type: :routing do
describe 'creating a new plan' do
it 'creates a new plan on post in /trip_plans' do
expect(post: '/trip_plans').to route_to(controller: 'trip_plans', action: 'create', title: 'New Plan', day: '3')
end
end
end
Now I need to somehow pass the params title: 'New Plan', day: '3' to my expect(post: '/trip_plans') so it seems like a real user is filling in the forms and hitting submit.
How do I pass params for POST requests to my RSpec Routing spec?
Thanks in advance!
Routing specs don't often add much value. In a routing spec you simply test that a certain route matches the correct controller. The controller is never actually called.
Instead what you can use are controller specs which are used to test how your application responds to user input:
# spec/controllers/trip_plans_controller_spec.rb
RSpec.describe TripPlansController, type: :controller do
let(:valid_params) do
{
title: 'New Plan',
day: '3'
}
end
let(:invalid_params) do
{
day: 'xxx'
}
end
describe 'POST #create' do
let(:action) { post :create, valid_params }
context 'with valid attributes' do
it 'creates a new post' do
expect { action }.to change(Post, :count).by(+1)
end
it 'has the correct attrributes' do
action
expect(assigns(:trip_plan).title).to eq 'New Plan'
expect(assigns(:trip_plan).day).to eq 3
end
end
context 'with invalid attributes' do
let(:action) { post :create, invalid_params }
it 'does not create a new post' do
expect { action }.to_not change(Post, :count)
end
it 'renders the new template' do
action
expect(response).to render_template :new
end
end
end
end
and feature specs which are end to end specs which test the actual user experience:
RSpec.feature 'Trip Plans' do
context 'as a User' do
scenario 'I should be able to create a trip plan' do
visit root_path
click_link 'Create a new trip plan'
fill_in 'Title', with: 'Go west'
fill_in 'Day', with: 5
click_button 'Create trip plan'
expect(page).to have_content 'Trip plan created.'
expect(page).to have_content 'Go west'
end
end
end
Controller specs are very useful for testing exactly how your controller responds to params and where you write actual expectations on the database state.
Feature specs are nice since they cover your views as well and well written specs also guarantee that your user paths are accessible. However they often do not catch errors which are not readily apparent from the front end and are slower, since you often need to render several pages to get to the actual meat of the test.
The stack trace or error message from feature specs is often less useful than lower level specs.
A good test suite is usually made of a combination of model specs, controller specs and feature specs which cover the most important paths through the application.
Related
I'm studying rails and rspec.
And I made rspec unit test (request test) on rails application.
But after searching on google, I'm wonder if my job is on right way.
Can my code be a "Unit test by function(not a method, web site's feature ex)create, show, delete..) of rails application" ?
this is my code with request test.
require 'rails_helper'
RSpec.describe 'Users', type: :request do
let!(:users) { create_list(:user, 10) }
let(:user_id) { users.first.id }
let(:user) { create(:user) }
def send_request_to_store_user(name, mailaddress)
post '/users', params: {
user: {
name: users.first.name,
mailaddress: users.first.mailaddress
}
}
end
def http_status_success_and_body_element_check(body_element)
expect(response).to have_http_status(:success)
expect(response.body).to include(body_element)
end
describe 'GET' do
context 'Get /users test' do
it 'test user list page' do
get '/users'
http_status_success_and_body_element_check('User List')
end
end
context 'Get /users/create test' do
it 'test user create page' do
get '/users/create'
http_status_success_and_body_element_check('create user')
end
end
context 'Get /users/:id/edit' do
it 'test user edit page' do
get "/users/#{user_id}"
http_status_success_and_body_element_check('edit user')
end
end
context 'Get /users/:id' do
it 'test user show page' do
get "/users/#{user_id}"
http_status_success_and_body_element_check('show user')
end
end
end
describe 'POST' do
context 'test store new user' do
it 'test create new user' do
send_request_to_store_user(user.name, user.mailaddress)
expect do
create(:user)
end.to change { User.count }.from(User.count).to(User.count + 1)
end
it 'test redirect after create' do
send_request_to_store_user(user.name, user.mailaddress)
expect(response).to have_http_status(302)
end
end
end
describe 'DELETE' do
it 'test delete user' do
expect do
delete "/users/#{user_id}"
end.to change { User.count }.from(User.count).to(User.count - 1)
expect(response).to have_http_status(302)
end
end
describe 'PUT' do
context 'user update' do
it 'test user information update' do
old_name = users.first.name
new_name = 'new_name'
expect do
put "/users/#{user_id}", params: {
user: {
name: new_name
}
}
end.to change { users.first.reload.name }.from(old_name).to(new_name)
expect(response).to have_http_status(:redirect)
end
end
end
end
this is my code with test on model
require 'rails_helper'
RSpec.describe User, type: :model do
it 'user must have name and mailaddress' do
user = create(:user)
expect(user).to be_valid
expect(user.name).not_to be_nil
expect(user.mailaddress).not_to be_nil
end
it 'mailaddress must include #' do
# user = FactoryBot.create(:user)
# If rails_helper.rb has config.include FactoryBot::Syntax::Methods,
# Can use shortcut. Don't have to FactoryBot.create
user = create(:user)
# Test pass if email match with regexp
expect(user.mailaddress).to match(/\A[\w+\-.]+#[a-z\d\-]+(\.[a-z]+)*\.[a-z]+\z/)
end
end
I don't think these tests are valuable (meaningful).
Here's my reasoning:
What are these tests telling you? That the Rails router is working? That the controller is responding with the right action? Neither of these are your responsibility to test. Rails has that covered.
If you want to know "does the index page render?" and "can I CRUD a user?" then write system tests with Capybara that simulate the whole flow. That way you are testing the real-world interaction with your whole system.
When using new_user_path for example, can I infer the rendered template name from this helper alone?
I am using rspec's shared examples, and looking for a way to assert that the rendered template is the correct one (rather than, say, a redirect) by only passing the route helper into the shared example.
i.e. what should ??? be if I only pass the path in?
shared_examples 'my spec' do
it 'will not redirect' do
get path
expect(response).to render_template(???)
end
end
Rspec.describe 'MySpec' do
it_behaves_like 'my spec' do
let(:path) { new_user_path }
end
end
Given that this is a controller shared example, you can use the following:
shared_examples 'my spec' do
it 'will not redirect' do
get path
expect(response).to render_template(request.params['action'])
end
end
Rspec.describe 'MySpec' do
it_behaves_like 'my spec' do
let(:path) { new_user_path }
end
end
In the context of your user controller, the test will be:
it 'will not redirect' do
get new_user_path # => get '/users/new'
expect(response).to render_template('new')
end
RSpec's render_template matcher only needs the action.
I'm new in testing and learning Rspec, and I can't git it working.
(I have read the book Effective testing with Rspec3, and many tutorials ...also pluralsight.com)
The situation is very simple. In a Companies controller I want to test de Create method, the company model belongs_to user, and is this the code:
I think the problem is when execute
in test: expect(Company).to receive(:new).with(company_params)
or in controller: #company.user=helpers.user
Controller:
class CompaniesController < SessionsController
def create
#company=Company.new(company_params)
#company.user=helpers.user
if #company.save()
redirect_to companies_path
else
render :edit
end
end
and Rspec:
RSpec.describe CompaniesController, type: :controller do
let(:user) { instance_double(User) }
before do
allow_any_instance_of(ApplicationHelper).to receive(:user){user}
allow(controller).to receive(:authorize){true}
end
describe 'Authenticated user with companies' do
let(:company_params) { {company:{name:"Albert",domain:"www.albert.com"}} }
let(:company) { instance_double(Company) }
before do
allow(Company).to receive(:new){company}
end
describe 'POST #create' do
context "with valid data" do
before { allow(company).to receive(:save){true} }
it "redirects to companies_path" do
expect(Company).to receive(:new).with(company_params)
expect(company).to receive(:user=).with(user)
post :create, params:{company: company_params}
expect(response).to redirect_to(companies_path)
end
My intention is very simple: Use instance_double to mock (or stub) #company, and Company.new, using instance double...to test the create action, and simulate the "save()" returning true...etc
I do not know if I explain myself very well, but given the create action of controlloer , how to test using mocks ans stubs, instance_double?
Thanks
First of all let me explain what we need to test here
def create
#company=Company.new(company_params)
#company.user=helpers.user
if #company.save()
redirect_to companies_path
else
render :edit
end
end
We are testing create action of a controller. First let us see what this action does? It's just takes comapany_params as input and create a company record in database.
Testing also goes like the same, we need to just pass the input that action required, and need to check whether it's creating record in database or not.
RSpec.describe CompaniesController, type: :controller do
let(:user) { instance_double(User) }
before do
# all your authentication stubing goes here
allow_any_instance_of(ApplicationHelper).to receive(:user){user}
allow(controller).to receive(:authorize){true}
end
describe 'POST#create' do
context 'with valid attributes' do
before do
post :create, { company:{ name:"Albert", domain:"www.albert.com"} }
end
it 'responds with success' do
expect(response.status).to eq(302)
end
it 'creates company' do
company = Company.find_by(name: "Albert")
expect(assigns(:company)).to eq(company)
expect(response).to redirect_to(companies_path())
end
end
context 'with invalid attributes' do
before do
post :create, { company:{ name:"", domain:""} }
end
it 'renders new template' do
expect(response).to render_template(:edit)
end
end
end
end
No need to sub anything here. As per my knowledge, Only when we use any lib classes / background jobs / third party libraries code inside action then we need to stub those code. Because for all those, we will write specs separately. So no need to test again here that's why we'll do stubing.
Thanks to Narsimha Reddy, I have better ideas about how to test.
Eventhough, if I want to stub
#company=Company.new(company_params)
#company.user=helpers.user
if #company.save()
For testing only de create's response , the solution was in a good use of parameters, and allowing allow(company).to receive(:user=) for the belongs_to association
let(:company_params) {{company:{name:"Albert",domain:"www.albert.com"}}}
let(:ac_company_params) {ActionController::Parameters.new(company_params).require(:company).permit!}
let(:company) { instance_double(Company) }
before do
allow(Company).to receive(:new){company}
allow(company).to receive(:user=)
allow(company).to receive(:save){true}
end
it "redirects to companies_path" do
expect(Company).to receive(:new).with(ac_company_params)
expect(company).to receive(:user=).with(user)
post :create, params: company_params
expect(response).to redirect_to(companies_path)
end
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
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?