I'm relatively new to testing and very new to Rails 4 and rSpec. I am trying to test a controller that uses Devise for authentication and I am stuck. All of the examples I can find are for Rails 3.
I'm using Rails 4.0.3, Devise 3.2.3, rSpec 2.14.1 and FactoryGirl 4.4.0.
class LessonPlansController < ApplicationController
before_action :authenticate_user!
# GET /lesson_plans
def index
#lesson_plans = current_user.lesson_plans.to_a
end
.
.
.
private
# Use callbacks to share common setup or constraints between actions.
def set_lesson_plan
#lesson_plan = LessonPlan.find(params[:id])
end
# Only allow a trusted parameter "white list" through.
def lesson_plan_params
params[:lesson_plan]
end
def lesson_plan_params
params.require(:lesson_plan).permit(:title, :synopsis)
end
end
Here are my factory definitions: (Maybe I don't need to define user_id in the lesson_plan factory?)
FactoryGirl.define do
factory :user do
sequence( :username ) { |n| "user#{n}" }
sequence( :email ) { |n| "foo#{n}#example.com" }
password 'foobarbaz'
password_confirmation 'foobarbaz'
created_at Time.now
updated_at Time.now
end
end
FactoryGirl.define do
factory :lesson_plan do
user_id 1
title "The French Revolution"
synopsis "Background and events leading up to the French Revolution"
end
end
And the test part is where I get stuck.
describe LessonPlansController do
let(:valid_attributes) { { } }
let(:valid_session) { {} }
# describe "GET index" do
it "assigns all lesson_plans as #lesson_plans" do
user=FactoryGirl.create(:user)
sign_in user
lesson_plan = LessonPlan.create! valid_attributes
get :index, {}, valid_session
assigns(:lesson_plans).should eq([lesson_plan])
end
end
I'm not sure what to put in valid_attributes and valid_session (or if I even need them). The test will get as far as signing in the user, but will fail on creation of the lesson_plan. Admittedly this is the default/generated test for rSpec, but I am not sure how to proceed.
Examples I have seen use a before block to set up the user. I haven't been able to find anything on the Devise wiki page covering how to write basic rSpec tests for a controller that requires the user to be logged in. Any pointers would be greatly appreciated!
"I'm not sure what to put in valid_attributes and valid_session (or if I even need them)."
Well that depends what you're testing for.. Say you're testing validations & want to ensure that a record not be created if x column is set to null... then you could try to specifically create a record with invalid attributes (e.g. column: nil) and expect the result to not return true; maybe you want to ensure that it IS created with valid attributes.
You can btw, use `attributes_for(:factory_name)`` since you're using FactoryGirl. And no you don't necessarily need to specify the user's id in your lesson plan factory; unless you always want it to reference user 1. You can simply reference user with no value. Check out http://everydayrails.com/2012/03/12/testing-series-intro.html and especially parts 3-5 for an introduction to testing with RSPec.. I found this a pretty easy to follow guide when I was getting started.
Related
I'm attempting to use Devise (2.2.4), which I'm new to, with the Rails 3.2.13/Ruby 2.0.0p195 app I'm building. I turned scoped_views on because I want to have my own separate users and admins views. And I created my own Users::RegistrationsController which seems to be doing what I want it to. I've just added my own Users::SessionsController, which is where I've hit problems.
I straight copied over a couple of action methods from the Devise::SessionsController source as a first step, planning to modify them once they were working (my controller code is at the bottom of this post). But my 'new' method is failing, when called, with a NameError because `sign_in_params' is apparently undefined.
Well, that seems pretty strange because I'm inheriting from Devise::SessionsController, and when I look at the source for that on GitHub, there's the sign_in_params defined in the protected section at the bottom. So I decided to investigate whether my controller is inheriting correctly from Devise::SessionsController - and it certainly seem to be. I can list out all the inherited methods, just not that one missing one. So I ended up running the following piece of code in the Rails Console:
(Devise::SessionsController.new.methods - DeviseController.new.methods).each {|m| puts m}
And it produces the following output:
_one_time_conditions_valid_68?
_one_time_conditions_valid_72?
_callback_before_75
_one_time_conditions_valid_76?
new
create
destroy
serialize_options
auth_options
If I ignore the underscored methods, the remainder are all those methods defined in the Devise::SessionsController source except sign_in_params. I can't see how anything I've written can be deleting that method, and I can't think what else to try. Google is silent on this problem, so I assume I'm doing something uniquely foolish, but I can't work out what. Any suggestions please? And might someone else try running that bit of Rails Console code to see what they get?
class Users::SessionsController < Devise::SessionsController
prepend_before_filter :require_no_authentication, :only => [ :new, :create ]
prepend_before_filter :allow_params_authentication!, :only => :create
prepend_before_filter { request.env["devise.skip_timeout"] = true }
# GET /resource/sign_in
def new
self.resource = resource_class.new(sign_in_params)
clean_up_passwords(resource)
respond_with(resource, serialize_options(resource))
end
# POST /resource/sign_in
def create
self.resource = warden.authenticate!(auth_options)
set_flash_message(:notice, :signed_in) if is_navigational_format?
sign_in(resource_name, resource)
respond_with resource, :location => after_sign_in_path_for(resource)
end
end
I think you are using code from a devise version compatible with Rails 4 on a rails 3 application.
sign_in_params is a method to be used with strong parameters. A gem used in rails 4.
If you check the controller on devise version 2.2. https://github.com/plataformatec/devise/blob/v2.2/app/controllers/devise/sessions_controller.rb
You will see that there is no sign_in_params method.
Check which version of devise you are using and copy the code based on that devise version in your controller, rather than the latest code from github.
I would like to know best practice on testing a REST API (in this case, using Sinatra and Rspec). The obvious problem is that, if you have a test that checks GET /users for a user list, you would like go through the stages of creating a user, running the test, then destroying the user. However, if the create/destroy steps are also API-dependent, you end up either breaking the rule of ordered-based testing, or testing multiple things in one test (eg. did it add a user?.. does GET /users return a user list?.. did it delete the user?).
You could use FactoryGirl. In your tests you can create Users via your API or create stubs with FG which you then delete, modify and so on. FG is a very flexible ORM testing helper that works great for this kind of stuff.
I also agree with #three - use FactoryGirl!
As an example (firstly, define a sample objects):
FactoryGirl.define do
sequence(:random_ranking) do |n|
#random_rankings ||= (1..10000).to_a.shuffle
#random_rankings[n]
end
factory :todo do
title { Faker::Lorem.sentence}
id { FactoryGirl.generate(:random_ranking) }
completed [true, false].sample
completed_at Time.new
created_at Time.new
updated_at Time.new
end
end
And in your spec test, describe your list action:
describe 'GET #index' do
before do
#todos = FactoryGirl.create_list(:todo, 10)
#todos.each do |todo|
todo.should be_valid
end
get :index, :format => :json
end
it 'response should be OK' do
response.status.should eq(200)
end
it 'response should return the same json objects list' do
response_result = JSON.parse(response.body)
# these should do the same
# response_result.should =~ JSON.parse(#todos.to_json)
response_result.should match_array(JSON.parse(#todos.to_json))
end
end
I am new to Rspec please tell me what would be the controller Spec for the following two methods In index method only login page is seen by entering the username control goes to login method and find the name of person. If person is find then control goes to people path otherwise it goes back to root path that is index page it self.
class HomeController < ApplicationController
def index
end
def login
#person = Person.find(:all, :conditions => ['people.name =?', params[:person][:name]] )
if #person.blank?
redirect_to root_path
else
redirect_to people_path
end
end
end
Please help me.
Thanks.
Your rspec controller tests could be like this:
describe HomeController do
render_views
it "Logs in Person with non-blank name" do
person = Factory(:Person, name: "non-blank name")
get :login
response.should redirect_to(people_path)
end
it "does not log in Person with blank name" do
person = Factory(:Person, name: "") # blank name
get :login
response.should redirect_to(root_path)
end
end
Refer to rails controller specs for details.
EDIT:
Factory: the code that creates objects (test objects in this case). This is a preferred method for creating test objects because you can customize your code to create objects with varying attributes with least duplication.
Fixtures: If you are not using factories, you can specify the attributes for each of the objects you are going to create. For more than 2-3 object, this data quickly becomes unmanageable to maintain (for example, when you add an attribute, you need to make changes for each of these objects).
Stubs: If you prefer not to create database records while creating model objects, you can stub the model code white testing controllers.
For more information, refer:
1. testing guide
2. asciicast (Note: this code refers to an older version of FactoryGirl gem. Refer below for up-to-date API of FactoryGirl)
3. FactoryGirl Readme
Suppose I have a model user, which has a uniqueness constraint on the email field
If I call Factory(:user) once all is well, but if I call it a second time it'll fail with an "entry already exists" error.
I'm currently using a simple helper to search for an existing entry in the DB before creating the factory...and calling any factory I make through that helper.
It works, but it's not entirely elegant, and considering how common I assume this problem must be, I'm guessing there's a better solution. So, is there an inbuilt way in factory girl to return_or_create a factory, instead of just charging ahead with create()? If not, how do most folk avoid duplicate entries with their factories?
Simple answer: use factory.sequence
If you have a field that needs to be unique you can add a sequence in factory_girl to ensure that it is never the same:
Factory.define :user do |user|
sequence(:email){|n| "user#{n}#factory.com" }
user.password{ "secret" }
end
This will increment n each time in order to produce a unique email address such as user52#factory.com. (See https://github.com/thoughtbot/factory_girl/wiki/Usage for more info)
However this isn't always great in Rails.env.development...
Over time I have found that this is not actually the most useful way to create unique email addresses. The reason is that while the factory is always unique for your test environment it's not always unique for your development environment and n resets itself as you start the environment up and down. In :test this isn't a problem because the database is wiped but in :development you tend to keep the same data for a while.
You then get collisions and find yourself having to manually override the email to something you know is unique which is annoying.
Often more useful: use a random number
Since I call u = Factory :user from the console on a regular basis I go instead with generating a random number. You're not guaranteed to avoid collisions but in practice it hardly ever happens:
Factory.define :user do |user|
user.email {"user_#{Random.rand(1000).to_s}#factory.com" }
user.password{ "secret" }
end
N.B. You have to use Random.rand rather than rand() because of a collision (bug?) in FactoryGirl [https://github.com/thoughtbot/factory_girl/issues/219](see here).
This frees you to create users at will from the command line regardless of whether there are already factory generated users in the database.
Optional extra for making email testing easier
When you get into email testing you often want to verify that an action by a particular user triggered an email to another user.
You log in as Robin Hood, send an email to Maid Marion and then go to your inbox to verify it. What you see in your inbox is something from user_842#factory.com. Who the hell is that?
You need to go back to your database to check whether the email was sent / received by whomever you expected it to be. Again this is a bit of a pain.
What I like to do instead is to generate the email using the name of the Factory user combined with a random number. This makes it far easier to check who things are coming from (and also makes collisions vanishingly unlikely). Using the Faker gem (http://faker.rubyforge.org/) to create the names we get:
Factory.define :user do |user|
user.first_name { Faker::Name::first_name }
user.last_name { Faker::Name::last_name }
user.email {|u| "#{u.first_name}_#{u.last_name}_#{Random.rand(1000).to_s}#factory.com" }
end
finally, since Faker sometimes generates names that aren't email-friendly (Mike O'Donnell) we need to whitelist acceptable characters: .gsub(/[^a-zA-Z1-10]/, '')
Factory.define :user do |user|
user.first_name { Faker::Name::first_name }
user.last_name { Faker::Name::last_name }
user.email {|u| "#{u.first_name.gsub(/[^a-zA-Z1-10]/, '')}_#{u.last_name.gsub(/[^a-zA-Z1-10]/, '')}_#{Random.rand(1000).to_s}#factory.com" }
end
This gives us personable but unique emails such as robin_hood_341#factory.com and maid_marion_10#factory.com
Here's what I do to force the 'n' in my factory girl sequence to be the same as that object's id, and thereby avoid collisions:
First, I define a method that finds what the next id should be in app/models/user.rb:
def self.next_id
self.last.nil? ? 1 : self.last.id + 1
end
Then I call User.next_id from spec/factories.rb to start the sequence:
factory :user do
association(:demo)
association(:location)
password "password"
sequence(:email, User.next_id) {|n| "darth_#{n}#sunni.ru" }
end
I found this a nice way to be sure the tests will always pass.
Otherwise you can not be sure the 100% of the times you will create a unique email.
FactoryGirl.define do
factory :user do
name { Faker::Company.name }
email { generate(:email) }
end
sequence(:email) do
gen = "user_#{rand(1000)}#factory.com"
while User.where(email: gen).exists?
gen = "user_#{rand(1000)}#factory.com"
end
gen
end
end
If you only need to generate a few values for attributes, you can also add a method to String, which keeps track of the prior strings used for an attribute. You could then do something like this:
factory :user do
fullname { Faker::Name.name.unique('user_fullname') }
end
I use this approach for seeding. I wanted to avoid sequence numbers, because they do not look realistic.
Here the String extension which makes this happen:
class String
# Makes sure that the current string instance is unique for the given id.
# If you call unique multiple times on equivalent strings, this method will suffix it with a upcounting number.
# Example:
# puts "abc".unique("some_attribute") #=> "abc"
# puts "abc".unique("some_attribute") #=> "abc-1"
# puts "abc".unique("some_attribute") #=> "abc-2"
# puts "abc".unique("other") #=> "abc"
#
# Internal:
# We keep a data structure of the following format:
# ##unique_values = {
# "some_for_id" => { "used_string_1" : 1, "used_string_2": 2 } # the numbers represent the counter to be used as suffix for the next item
# }
def unique(for_id)
##unique_values ||= {} # initialize structure in case this method was never called before
##unique_values[for_id] ||= {} # initialize structure in case we have not seen this id yet
counter = ##unique_values[for_id][self] || 0
result = (counter == 0) ? self : "#{self}-#{counter}"
counter += 1
##unique_values[for_id][self] = counter
return result
end
end
Caution: This should not be used for lots of attributes, since we track all prior strings (optimizations possible).
I am currently in the process of migration to rails 3 from rails 2 in a large application. In our functional specs, we have alot of stuff like this:
#model = Factory :model
#child = Factory :child
Model.stub!(:find).and_return(#model)
Child.stub!(:find).and_return(#child)
...
#child.should_receive(:method).twice
The main issue is that if I let it hit DB and get actual instance of child, real :method makes tests too complex (need two big factories) and slow.
In code we use various ways to get items: find, dynamic finders, etc
#model = Model.find(1)
#child = #model.children.find_by_name(name)
How would you advice to move this logic to rails 3? Any advice on another stubbing/mocking library maybe?
Normally you would mock the model inside controller specs:
Model.stub!(:find).and_return(mock_model('Model'))
Child.stub!(:find).and_return(mock_model('Child'))
However, when you've got gem "rspec-rails", "~> 2.0" in your rails 3 app's Gemfile, then the standard rails scaffold generator will use rspec to generate specs for you, so running rails generate scaffold MyResource will generate some example specs for you.
The following is a lightly annotated version of what rails/rspec will generate for controller specs, so I suppose this should be considered "The RSpec Way".
describe AccountsController do
# Helper method that returns a mocked version of the account model.
def mock_account(stubs={})
(#mock_account ||= mock_model(Account).as_null_object).tap do |account|
account.stub(stubs) unless stubs.empty?
end
end
describe "GET index" do
it "assigns all accounts as #accounts" do
# Pass a block to stub to specify the return value
Account.stub(:all) { [mock_account] }
get :index
# Assertions are also made against the mock
assigns(:accounts).should eq([mock_account])
end
end
describe "GET show" do
it "assigns the requested account as #account" do
Account.stub(:find).with("37") { mock_account }
get :show, :id => "37"
assigns(:account).should be(mock_account)
end
end
describe "GET new" do
it "assigns a new account as #account" do
Account.stub(:new) { mock_account }
get :new
assigns(:account).should be(mock_account)
end
end
end