Hi I am new to rspec (and unit testing in general) and want to test the following method:
class HelloController < ApplicationController
def hello_world
user = User.find(4)
#subscription = 10.00
render :text => "Done."
end
end
I am trying to use Rspec like so:
Describe HelloController, :type => :controller do
describe "get hello_world" do
it "should render the text 'done'" do
get :hello_world
expect(response.body).to include_text("Done.")
end
end
end
I would like to simply test that the method works properly and renders the test "done". I get the following error when I run the test:
Failure/Error: user = User.find(4)
ActiveRecord::RecordNotFound:
Couldn't find User with 'id'=4
But how do I properly create a user with that id before executing it? I have tried the following based on other tutorials and questions but it doesn't work:
describe "get hello_world" do
let(:user) {User.create(id: 4)}
it "should render the text 'done'" do
get :hello_world
expect(response.body).to include_text("Done.")
end
end
Thank you in advance.
Hey so really no action (e.g. def hello_world) should rely on a specific id. So a simple alternative could be to use user = User.last or to find the user by name user = User.find_by(name: "name"). Then in the test you would create any user if you using User.last in the action.
describe "get hello_world" do
let(:user) {User.create!}
it "should render the text 'done'" do
get :hello_world
expect(response.body).to include_text("Done.")
end
end
or if you are searching by name you can make a user with that name;
describe "get hello_world" do
let(:user) {User.create!(name: "name")}
it "should render the text 'done'" do
get :hello_world
expect(response.body).to include_text("Done.")
end
end
Hope this helps, questions welcome.
Do you really mean to use 'user = User.find(4)'? If you really meant to do that, you should stub the User's find method and return a user object.
it "should render the text 'done'" do
u = User.new #a new user, your test database is empty, so there's no user with id 4
User.stub(find: u) #stub the User's find method to return that new user
get :hello_world
expect(response.body).to include_text("Done.")
end
Another option is to send the user_id via params
it "should render the text 'done'" do
u = User.create(.... your user params)
get :hello_world, user_id: u.id
expect(response.body).to include_text("Done.")
end
and
def hello_world
user = User.find(params[:user_id])
#subscription = 10.00
render :text => "Done."
end
Anyway, I don't think you should be doing that, a hardcoded id is a bad sign. If you need to control users registrations and logins you can use something like Devise, and you may need to create an login a user before the spec.
Related
Hi I am implementing a method to delete a user account in my web application. My controller:
class UsersController < ApplicationController
before_filter :set_current_user
def user_params
params.require(:user).permit(:user_id, :first_name, :last_name, :email, :password, :password_confirmation)
end
def delete_account
#user = User.find_by_id(params[:id])
if #user.present?
#user.destroy
flash[:notice] = "User Account Deleted."
end
redirect_to root_path
end
def destroy
User.delete(:user_id)
redirect_to root_path
end
end
My rspec:
require 'spec_helper'
require 'rails_helper'
require'factory_girl'
describe UsersController do
describe "delete account" do
before :each do
#fake_results = FactoryGirl.create(:user)
end
it "should call the model method that find the user" do
expect(User).to receive(:find).with(params[:id]).and_return (#fake_results)
end
it "should destroy the user account from the database" do
expect{delete :destroy, id: #fake_results}.to change(User, :count).by(-1)
end
it "should redirect_to the home page" do
expect(response).to render_template(:home)
end
end
end
The first error is
Failure/Error: expect(User).to receive(:find).with(params[:id]).and_return (#fake_results)
NameError:undefined local variable or method `params' for #<RSpec::ExampleGroups::UsersController::DeleteAccount:0x00000007032e18>
I know what this error means but I don't know how to correct it. How can I pass the user id from the controller to rspec?
The second error is:
Failure/Error: expect(response).to render_template(:home)
expecting <"home"> but rendering with <[]>
I think there is something wrong with my controller method. It should redirect to the home page but it doesn't.
params is not available in your tests, it's available in your controller.
Looks like you create a test user in your test:
#fake_results = FactoryGirl.create(:user)
Then, you can use the id of this test user (#fake_results.id) instead of trying to use params[:id]:
expect(User).to receive(:find).with(#fake_results.id).and_return (#fake_results)
Although, you may want to change the name from #fake_results to something more meaningful e.g. test_user or so.
However, this should fix both of your problems as your second problem is there because of the first problem. As it's failing to delete the user in the first place, it's not being redirected to the root path and hence the home template is not rendering.
I've recently learned how to stub in rspec and found that some benefits of it are we can decouple the code (eg. controller and model), more efficient test execution (eg. stubbing database call).
However I figured that if we stub, the code can be tightly tied to a particular implementation which therefore sacrifice the way we refactor the code later.
Example:
UsersController
# /app/controllers/users_controller.rb
class UsersController < ApplicationController
def create
User.create(name: params[:name])
end
end
Controller spec
# /spec/controllers/users_controller_spec.rb
RSpec.describe UsersController, :type => :controller do
describe "POST 'create'" do
it 'saves new user' do
expect(User).to receive(:create)
post :create, :name => "abc"
end
end
end
By doing that didn't I just limit the implementation to only using User.create? So later if I change the code my test will fail even though the purpose of both code is the same which is to save the new user to database
# /app/controllers/users_controller.rb
class UsersController < ApplicationController
def create
#user = User.new
#user.name = params[:name]
#user.save!
end
end
Whereas if I test the controller without stubbing, I can create a real record and later check against the record in the database. As long as the controller is able to save the user Like so
RSpec.describe UsersController, :type => :controller do
describe "POST 'create'" do
it 'saves new user' do
post :create, :name => "abc"
user = User.first
expect(user.name).to eql("abc")
end
end
end
Really sorry if the codes don't look right or have errors, I didn't check the code but you get my point.
So my question is, can we mock/stub without having to be tied to a particular implementation? If so, would you please throw me an example in rspec
You should use mocking and stubbing to simulate services external to the code, which it uses, but you are not interested in them running in your test.
For example, say your code is using the twitter gem:
status = client.status(my_client)
In your test, you don't really want your code to go to twitter API and get your bogus client's status! Instead you stub that method:
expect(client).to receive(:status).with(my_client).and_return("this is my status!")
Now you can safely check your code, with deterministic, short running results!
This is one use case where stubs and mocks are useful, there are more. Of course, like any other tool, they may be abused, and cause pain later on.
Internally create calls save and new
def create(attributes = nil, options = {}, &block)
if attributes.is_a?(Array)
attributes.collect { |attr| create(attr, options, &block) }
else
object = new(attributes, options, &block)
object.save
object
end
end
So possibly your second test would cover both cases.
It is not straight forward to write tests which are implementation independent. That's why integration tests have a lot of value and are better suited than unit tests for testing the behavior of the application.
In the code you're presented, you're not exactly mocking or stubbing. Let's take a look at the first spec:
RSpec.describe UsersController, :type => :controller do
describe "POST 'create'" do
it 'saves new user' do
expect(User).to receive(:create)
post :create, :name => "abc"
end
end
end
Here, you're testing that User received the 'create' message. You're right that there's something wrong with this test because it's going to break if you change the implementation of the controllers 'create' action, which defeats the purpose of testing. Tests should be flexible to change and not a hinderance.
What you want to do is not test implementation, but side effects. What is the controller 'create' action supposed to do? It's supposed to create a user. Here's how I would test it
# /spec/controllers/users_controller_spec.rb
RSpec.describe UsersController, :type => :controller do
describe "POST 'create'" do
it 'saves new user' do
expect { post :create, name: 'abc' }.to change(User, :count).by(1)
end
end
end
As for mocking and stubbing, I try to stay away from too much stubbing. I think it's super useful when you're trying to test conditionals. Here's an example:
# /app/controllers/users_controller.rb
class UsersController < ApplicationController
def create
user = User.new(user_params)
if user.save
flash[:success] = 'User created'
redirect_to root_path
else
flash[:error] = 'Something went wrong'
render 'new'
end
end
# /spec/controllers/users_controller_spec.rb
RSpec.describe UsersController, :type => :controller do
describe "POST 'create'" do
it "renders new if didn't save" do
User.any_instance.stub(:save).and_return(false)
post :create, name: 'abc'
expect(response).to render_template('new')
end
end
end
Here I'm stubbing out 'save' and returning 'false' so I can test what's supposed to happen if the user fails to save.
Also, the other answers were correct in saying that you want to stub out external services so you don't call on their API every time you're running your test suite.
Having trouble testing variable values from a controller using RSpec.
Relevant controller code:
class ToysController < ApplicationController
def claim
toy = Toy.find(params[:toy_id])
current_user.toys << toy
toy.status = "claimed"
render :index
end
end
This definitely works -- I know because I puts toy.inspect after it happens, and it's fine. But I can't test it. Here's what my current test looks like, after a lot of messy attempts:
require 'spec_helper'
describe ToysController do
describe "GET 'claim'" do
let(:james) {create(:user)}
let(:toy) {create(:toy)}
before do
OmniAuth.config.mock_auth[:google] = {
uid: james.uid
}
session[:user_id] = james.id
end
it "can be claimed by a user" do
get :claim, toy_id: toy.id
assigns(:toy).user.should eq james.id
end
end
end
When I run the test, I get all sorts of errors on assigns(:toy).user.should indicating that toy is Nil. I've tried messing with the assigns syntax in lots of ways, because I was unable to find the docs for it.
What am I doing wrong? What's the right way to see what the controller does with the user and the toy passed to it?
Edit: Trying to phase over to instance variables, but it still doesn't do the trick. Here's my code again with instance variables (different var names, same results):
Ideas controller:
def claim
#idea = Idea.find(params[:idea_id])
current_user.ideas << #idea
#idea.status = "claimed"
render :index
end
Test:
describe "GET 'claim'" do
let(:james) {create(:user)}
let(:si_title) {create(:idea)}
before do
OmniAuth.config.mock_auth[:google] = {
uid: james.uid
}
session[:user_id] = james.id
end
it "can be claimed by a user" do
get :claim, idea_id: si_title.id
puts assigns(si_title).inspect
end
end
Output: nil.
Solved! Test now reads like this:
describe "GET #claim" do
let(:james) {create(:user)}
let(:si_title) {create(:idea)}
before do
OmniAuth.config.mock_auth[:google] = {
uid: james.uid
}
session[:user_id] = james.id
end
it "can be claimed by a user" do
get :claim, idea_id: si_title.id
assigns(:idea).user_id.should eq james.id
end
end
My mistakes:
Not using a colon to prefix the instance variable name in the assigns.
Using the incorrect variable name in the assigns.
Try replacing toy variable in Controller with #toy. assigns has access only to instance variables.
This following Controller test is failing, and I can't figure out why:
describe "GET 'index'" do
before(:each) do
#outings = FactoryGirl.create_list(:outing, 30)
#user = FactoryGirl.create(:user)
end
it "should be successful" do
get :index
response.should be_success
end
end
Rspec offers up the (rather unhelpful) error:
Failure/Error: response.should be_success
expected success? to return true, got false
Here's the code for the actual Controller, too:
def index
if #user
#outings = Outing.where(:user_id => #user.id)
#outing_invites = OutingGuest.where(:user_id => #user.id)
else
flash[:warning] = "You must log in to view your Outings!"
redirect_to root_path
end
end
Anyone have an idea what's causing my test to fail? I assume it may have something to do with the conditional in the Outing Controller, but I have no idea what a passing test would look like...
You're confusing instance variables between two separate classes - the controller is its own class and the specification is its own class. They don't share state. You could try this simple example to get a better understanding...
def index
// obvious bad code, but used to prove a point
#user = User.first
if #user
#outings = Outing.where(:user_id => #user.id)
#outing_invites = OutingGuest.where(:user_id => #user.id)
else
flash[:warning] = "You must log in to view your Outings!"
redirect_to root_path
end
end
I'll guess that FactoryGirl.create_list(:outing, 30) doesn't create an outing associating the first user with the outing since you're creating the user after you create the outing so your Outing.where will fail as well.
Its important to understand that when you are including the database in your test stack the database needs to contain the data in the way the test expects. So if your controller is querying for outings belonging to a specific user your spec needs to setup the environment such that the user the controller will retrieve (in this case, the terrible line with User.first from my example) will also have the outings associated with it that the specification is expecting.
I have a Rails 3 project in which I want to store the current company selected in a session variable.
I'm working with the staff controller spec and would like to stub out current_company for now as I'm isolating my spec example for the staff new controller action.
it "should call current_company" do
company = mock_model(Company, :id => "1")
controller.should_receive(:current_company).and_return(company)
get :new
end
Here is my new action for the staff controller
def new
#staff = Staff.new
#staff.company_id = current_company.id
end
I keep getting error
Failure/Error: get :new
NameError:
undefined local variable or method `current_company' for #<StaffsController:0x000000028d6ad8>
I've also tried just stubbing it out instead of using should_receive
controller.stub!(:current_company).and_return(company)
I get the same error.
Your code looks fine to me, it should work. There must be some other problem we are not seeing. I notice the controller name is "StaffsController" -- is that correct? Double-check the names of the controller and the corresponding spec -- they should be the same.
I think it was bombing out on the 'should be successful' example/test, so I've put my stubbing in a before block.
require 'spec_helper'
describe StaffsController do
describe "GET 'new'" do
let(:staff) { mock_model(Staff, :company_id= => nil)}
let(:company) { mock_model(Company, :id => 1)}
before do
Staff.stub!(:new).and_return(staff)
controller.stub!(:current_company).and_return(company)
end
it "should be successful" do
get :new
response.should be_success
end
it "should call current_company" do
controller.should_receive(:current_company).and_return(company)
get :new
end
end
end
This works for:
class StaffsController < ApplicationController
def new
#staff = Staff.new
current_company.id
end
end