I'm working the a Documents class, trying to test it. I've defined the following factory:
require 'factory_girl'
FactoryGirl.define do
factory :document do
user_id '6315'
name 'Test doc'
description 'W9'
filename 'test_doc.pdf'
filetype 'file'
filesize 500
end
factory :invalid_doc, parent: :document do
filesize 5242900
end
end
with the following helper method to access the right attributes in the test:
def build_attributes(*args)
attrs = FactoryGirl.build(*args).attributes
attrs.delete_if do |k, v|
["id", "created_at", "updated_at"].member?(k)
end
paramify_values(attrs)
end
Before each test I run:
before(:each) do
login_as_admin
#doc = #user.documents.create(FactoryGirl.attributes_for(:document))
end
where #user is set in the login_as_admin macro. Within my test, I'm running this:
describe 'POST #create' do
it "should create a new document" do
expect{
post :create, document: build_attributes(:document, user_id: #doc.user_id)
}.to change(Document,:count).by(1)
end
it "should find the right user" do
post :create, document: build_attributes(:document, user_id: #doc.user_id)
assigns(:user).should eq(#user)
end
# some other tests...
end
The former test was suggested on this article, the latter is just what I think should be happening. The controller action is assigning the instance with the following:
#user = User.find(document[:user_id])
so, pretty standard. However, both of these tests throw the same error,
Failure/Error: post :create, document: build_attributes(:document, user_id: #doc.user_id)
NoMethodError:
undefined method `original_filename' for nil:NilClass
but I never call that method explicitly, so is it something FactoryGirl is calling? The model is described as follows:
attr_accessible :description, :filename, :filesize, :filetype, :name, :user_id
where :filename is just a string. What could be going wrong here? I'm not using paperclip to upload the files, just a file_field in the view. I grab the path and save the file to the production server in the controller, but never call this method.
Edit:
I suppose an actual controller description might help haha
def create
uploaded_file = params[:document][:file]
document = params[:document]
document.delete(:file)
#user = User.find(document[:user_id])
filepath = Rails.root.join('documents', #user.company_id.to_s, #user.id.to_s, uploaded_file.original_filename)
%x[ mkdir #{Rails.root.join('documents', #user.company_id.to_s)} ]
%x[ mkdir #{Rails.root.join('documents', #user.company_id.to_s, #user.id.to_s)} ]
File.open(filepath, 'wb') do |file|
file.write(uploaded_file.read)
end
document[:filesize]= File.size(filepath)
document[:filetype]= File.ftype(filepath)
document[:filename] = uploaded_file.original_filename
d =Document.new(document)
d.save
redirect_to :action => 'show', :id => user.id
end
Please keep in mind I'm sure there are many things wrong with this method. I'm trying to refactor it and test as I go. For the moment, all I'm trying to do is get past this first hiccough, the original_filename method is being called somewhere, and I don't define it myself. Can anyone see why/where?
original_filename is a method on an uploaded file, see the rack documentation.
filepath = Rails.root.join('documents', #user.company_id.to_s, #user.id.to_s, uploaded_file.original_filename)
and
document[:filename] = uploaded_file.original_filename
In the controller are getting the original filename, since when a file gets uploaded it gets an ugly temp filename for storage you want to use the original filename to make it readable and accurate.
Consider using the fixture_file_upload helper in rspec. Here is an example spec:
expect {
post :create, document: attributes_for(:document, user_id: #doc.user_id, file: fixture_file_upload('spec/assets/documents/test_doc.pdf', 'appliation/pdf'))
}.to change(Document, :count).by(1)
And place a test pdf in spec/assets/documents/test_doc.pdf
You can use
Rack::Multipart::UploadedFile.new(path)
for your test.
Related
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.
I'm trying to figure out the how to test file attachments w/ the Fabrication and Rspec gems. The file upload works fine when testing the site manually, there's just no Rspec coverage. The problem seems to be that I don't know how to include an attachment in a PUT request.
How do I add a file attachment, preferably using a fabricator, to this test
Fabricator:
Fabricator(:application) do
email Faker::Internet.email
name Faker::Name.name
resume_url { File.open(
File.join(
Rails.root,
"spec",
"support",
"files",
"hey_look_a_pdf_pdf_lolz.pdf"
)
)
}
end
Controller:
class ApplicationsController < ApplicationController
def update
#application = Application.find_or_initialize_by(id: params[:id])
if #application.update(application_params)
flash[:success] = "Application has been saved :)"
redirect_to application_path(#application)
else
render :edit
end
end
private
def application_params
params[:application].permit(:email, :job_id, :name, :resume_url)
end
end
Controller test
require "spec_helper"
# this is a sample application attributes being passed into controller
# it should have a file attachment, but haven't figured out how to do that
#
# {
# "id"=>"fa446fdf-b82d-48c0-8979-cbbb2de4fb47",
# "email"=>"old#example.com",
# "name"=>"Dr. Rebeca Dach",
# "resume_url"=>nil,
# "job_id"=>"cf66dbcf-d110-42cc-889b-0b5ceeeed239",
# "created_at"=>nil,
# "updated_at"=>nil
# }
describe ApplicationsController do
context "PUT /applications/:id" do
it "creates an application" do
expect { put(
:update,
application: application.attributes,
id: application.id
)}.to change{Application.count}.from(0).to(1)
end
end
end
Update #1
The following Fabricator seems to work okay. I'm still not able to get file attachments to work in controller tests.
Fabricator(:application) do
email Faker::Internet.email
name Faker::Name.name
resume_url { ActionDispatch::Http::UploadedFile.new(
tempfile: File.new(Rails.root.join(
"./spec/support/files/hey_look_a_pdf_pdf_lolz.pdf"
)),
filename: File.basename(File.new(Rails.root.join(
"./spec/support/files/hey_look_a_pdf_pdf_lolz.pdf"
)))
)}
end
OK, I got it figured out. There were two pieces.
1) I had to fix the Fabricator, which I mentioned in "Update #1" to my question. Here's a simpler format for the Fabricator using Rack::Test::Upload.new
Fabricator(:application) do
email Faker::Internet.email
name Faker::Name.name
resume_url {
Rack::Test::UploadedFile.new(
"./spec/support/files/hey_look_a_pdf_pdf_lolz.pdf",
"application/pdf"
)
}
end
2) I was using Fabricator.build(:application).attributes, which wasn't compatible with a file attachment. Instead, I started using Fabricator.attributes_for(:application), and everything started working great. Here's the passing.
describe ApplicationsController do
context "PUT /applications/:id" do
let(:job) { Fabricate(:job) }
let(:application) do
Fabricate.attributes_for(
:application,
email: "old#example.com",
id: SecureRandom.uuid,
job_id: job.id
)
end
it "creates an application" do
expect { put(
:update,
application: application,
id: application["id"]
)}.to change{Application.count}.from(0).to(1)
end
end
end
I'm trying to get RSpec working for a simple scaffolded app, starting with the rspec scaffold tests.
Per the devise wiki, I have added the various devise config entries, a factory for a user and an admin, and the first things I do in my spec controller is login_admin.
Weirdest thing, though... all my specs fail UNLESS I add the following statement right after the it ... do line:
dummy=subject.current_user.inspect
(With the line, as shown below, the specs pass. Without that line, all tests fail with the assigns being nil instead of the expected value. I only happened to discover that when I was putting some puts statements to see if the current_user was being set correctly.)
So what it acts like is that dummy statement somehow 'forces' the current_user to be loaded or refreshed or recognized.
Can anyone explain what's going on, and what I should be doing differently so I don't need the dummy statement?
#specs/controllers/brokers_controller_spec.rb
describe BrokersController do
login_admin
def valid_attributes
{:name => "Bill", :email => "rspec_broker#example.com", :company => "Example Inc", :community_id => 1}
end
def valid_session
{}
end
describe "GET index" do
it "assigns all brokers as #brokers" do
dummy=subject.current_user.inspect # ALL SPECS FAIL WITHOUT THIS LINE!
broker = Broker.create! valid_attributes
get :index, {}, valid_session
assigns(:brokers).should eq([broker])
end
end
describe "GET show" do
it "assigns the requested broker as #broker" do
dummy=subject.current_user.inspect # ALL SPECS FAIL WITHOUT THIS LINE!
broker = Broker.create! valid_attributes
get :show, {:id => broker.to_param}, valid_session
assigns(:broker).should eq(broker)
end
end
and per the devise wiki here is how I login a :user or :admin
#spec/support/controller_macros.rb
module ControllerMacros
def login_admin
before(:each) do
#request.env["devise.mapping"] = Devise.mappings[:admin]
sign_in Factory.create(:admin) # Using factory girl as an example
end
end
def login_user
before(:each) do
#request.env["devise.mapping"] = Devise.mappings[:user]
user = Factory.create(:user)
user.confirm! # or set a confirmed_at inside the factory. Only necessary if you are using the confirmable module
sign_in user
end
end
end
What a struggle! Thank you Robin, I've been googling on this for hours and finally saw your post; now my controller tests are working :)
To add to your answer, I figured out how to get the devise session into the valid_session hash, which allows the controller tests to run properly as generated by rails.
def valid_session
{"warden.user.user.key" => session["warden.user.user.key"]}
end
In your tests, there is the following code:
def valid_session
{}
end
...
get :index, {}, valid_session
Because of this 'session' variable, the "log_in" that you did is essentially not being used during the 'get'.
The way that I solved it was to remove all of the "valid_session" arguments to the get, post, put, delete calls in that controller's spec. The example above becomes:
get :index, {}
I suspect that there's a way to add the devise's session to the "valid_session" hash, but I don't know what it is.
Thanks for this solution.
If you are using a different Devise model, the session id also changes.
For a model Administrator use the following:
def valid_session
{'warden.user.administrator.key' => session['warden.user.administrator.key']}
end
Rails 3.0.3....
I'm just starting out with Factory Girl, having had little success with the standard fixtures approach. I've commented out fixtures :all from the test/test_helper.rb file and have created a factories file.
My problem is that the sequence feature doesn't seem to work:
# test/factories.rb
Factory.sequence :clearer_name do |n|
"Clearer_#{n}"
end
Factory.define :clearer do |f|
f.name Factory.next(:clearer_name)
end
My (functional) test is only slightly different from standard:
require 'test_helper'
class ClearersControllerTest < ActionController::TestCase
setup do
#clearer = Factory.create(:clearer)
end
test "should get index" do
get :index
assert_response :success
assert_not_nil assigns(:clearers)
end
test "should get new" do
get :new
assert_response :success
end
test "should create clearer" do
assert_difference('Clearer.count') do
post :create, :clearer => #clearer.attributes
end
assert_redirected_to clearer_path(assigns(:clearer))
end
When I run rake test I get:
test_should_create_clearer(ClearersControllerTest):
ActiveRecord::RecordNotUnique: SQLite3::ConstraintException: column name is not unique: INSERT INTO "clearers" ("active", "updated_at", "name", "created_at") VALUES ('t', '2011-02-20 08:53:37.040200', 'Clearer_1', '2011-02-20 08:53:37.040200')
...as if it's not continuing the sequence.
Any tips?
Thanks,
UPDATE: heres my test file:
#clearers_controller_test.rb
require 'test_helper'
class ClearersControllerTest < ActionController::TestCase
setup do
#clearer = Factory.create(:clearer)
end
test "should create clearer" do
assert_difference('Clearer.count') do
# does not work without this:
Clearer.destroy_all
post :create, :clearer => #clearer.attributes
end
end
I can get this to work by putting Clearer.destroy_all at the top of the test method as shown, but that doesn't feel right.
I see - In your setup, you're creating a Clearer instance. The Factory.create method builds and saves a new record and returns it.
The problem is that you're then attempting to create another instance in your "should create clearer" test but you're re-using the existing instance's attributes.
If you want Factory to return fresh attributes (and the next name sequence), you need to ask it for new attributes:
test "should create clearer" do
assert_difference('Clearer.count') do
post :create, :clearer => Factory.attributes_for(:clearer)
end
end
You should only be using that existing #clearer instance in the context of an existing record, not where you want a new one.
You aren't starting with a fresh database is my guess. There's lots of reasons this could be happen, but you can verify that's the problem by adding a Clearer.destroy_all in your setup function, before creating one.
Sequences and other attribute values that are calculated at runtime need to be procs, not static values.
Change:
Factory.define :clearer do |f|
f.name Factory.next(:clearer_name)
end
to:
Factory.define :clearer do |f|
f.name {Factory.next(:clearer_name)}
end
I'm sorry, but this is beginning to feel like kicking myself in the head. I'm completely baffled by RSpec. Have watched video after video, read tutorial after tutorial, and still I'm just stuck on square one.
=== here is what I'm working with
http://github.com/fudgestudios/bort/tree/master
=== Errors
F
1)
NoMethodError in 'bidding on an item should work'
You have a nil object when you didn't expect it!
You might have expected an instance of ActiveRecord::Base.
The error occurred while evaluating nil.new_record?
spec/controllers/auction_controller_spec.rb:16:
spec/controllers/auction_controller_spec.rb:6:
Finished in 0.067139 seconds
1 example, 1 failure
=== here is my controller action
def bid
#bid = Bid.new(params[:bid])
#bid.save
end
=== here is my test
require File.dirname(__FILE__) + '/../spec_helper'
include ApplicationHelper
include UsersHelper
include AuthenticatedTestHelper
describe "bidding on an item" do
controller_name :items
before(:each) do
#user = mock_user
stub!(:current_user).and_return(#user)
end
it "should work" do
post 'bid', :bid => { :auction_id => 1, :user_id => #user.id, :point => 1 }
assigns[:bid].should be_new_record
end
end
=== spec_helper
http://github.com/fudgestudios/bort/tree/master/spec/spec_helper.rb
It's very disheartening to wake for work at 3 a.m. and accomplish nothing for the day. Please understand.
You've got a couple of things backwards in before(:each). Seeing as the example is specifying that the post should increase the count by 1, you're dealing with real records and there is no reason for stubbing anything at all. Also, at this point, since there is only one example, there is no reason to have a before block. I'd do it this way:
describe ItemsController, "bidding on an item" do
fixtures :users
it "should create a new Bid" do
login_as :quentin
lambda do
post 'bid', :bid => { :auction_id => 1, :user_id => #user.id, :point => 1 }
end.should change(Bid, :count).by(1)
end
end
One thing I'd recommend is creating these things VERY granularly for now until you understand them better. Start with the expectation (post should change bid count), run the spec and let the failure message guide you to add whatever else you need in the spec or in the code.
Jesse,
It'll still pass if you comment out the 2nd two lines of before(:each), which are having no impact on the "should create a new Bid" example.
The lambda keyword creates an arbitrary block of code that is not executed when you define it, but is actually an object you can assign to a variable and execute later:
the_post = lambda do
post 'bid', :bid => { :auction_id => 1, :user_id => #user.id, :point => 1 }
end
At this point that code is not executed, but we can refer to it with the 'the_post' variable. Now we can send it 'should', followed by 'change ...', like this:
the_post.should change(Bid, :count).by(1)
When this line is executed, a few things happen. The material to the right of 'should' is evaluated first, initializing an rspec matcher object with some instructions. That matcher is the argument to 'should' - the equivalent of this:
matcher = change(Bid, :count).by(1)
the_post.should(matcher)
The 'should' method is called on the_post, which is the code block (that still hasn't been executed). Under the hood, the 'should' method passes self (the_post) to the matcher, so the matcher now has everything it needs to evaluate the example.
The matcher calls Bid.count and records the value. Then it executes the block (the_post), and then calls Bid.count a second time and compares it to the value it recorded earlier. In this case, since we're looking for Bid.count to change by 1 (positive is implicit here - increase by 1), if that's what happens the matcher stays silent and the example passes.
If the values are the same, or differ by some value other than 1, the example will fail. You can see that work if you change the expectation to by(2) instead of by(1).
HTH,
David
EDIT: you shouldn't expect Bid.count to increment when using a mock object. Mantra I forgot: caffeine before code.
Just commenting out the lines, for now, so the original is still there.
require File.dirname(__FILE__) + '/../spec_helper'
include ApplicationHelper
include UsersHelper
include AuthenticatedTestHelper
describe "POST to bid_controller" do
controller_name :items
before(:each) do
##bid = mock_model(Bid) # create a new mock model so we can verify the appropriate things
#Bid.stub!(:new).and_return(#bid) # stub the new class method on Bid to return our mock rather than a new ActiveRecord object.
# this separates our controller spec entirely from the database.
end
it "should create a new Bid" do
lambda do
post 'bid', :bid => { :auction_id => 1, :user_id => #user.id, :point => 1 }
end.should change(Bid, :count).by(1)
end
# ... more specs
end
Try to write as small specs as possible, write your setences in such a way as to make it obvious what you should be verifying in that spec. For example, how I changed yours from it "should work" to it "should create a new Bid". If there's more to that controller, write a new spec
for each small piece of functionality.
If you do end up needing mock users, there are some helpers for restful_authentication that make it easier. First create a user fixture in
RAILS_ROOT/spec/fixtures/users.yml, like this:
quentin:
login: quentin
email: quentin#example.com
salt: 7e3041ebc2fc05a40c60028e2c4901a81035d3cd
crypted_password: 00742970dc9e6319f8019fd54864d3ea740f04b1 # test
created_at: <%= 5.days.ago.to_s :db %>
activation_code: 8f24789ae988411ccf33ab0c30fe9106fab32e9b
activated_at: <%= 5.days.ago.to_s :db %>
name: "Quentin"
Then in your spec you will be able to write the following and have your current_user method and all the other parts of restul_authentication
behave as you would expect them to at runtime.
login_as :quentin
# .... the rest of your spec
As an example of a few more specs I might add as a couple more examples:
def do_post
# extracting the method under test, so I don't repeat myself
post 'bid', :bid => { :auction_id => 1, :user_id => #user.id, :point => 1 }
end
it "should create a new Bid" do
lambda do
do_post
end.should change(Bid, :count).by(1)
end
it "should assign the Bid to the proper auction" do
#bid.should_receive(:auction_id=).with(1) # test this, I believe doing Bid.new(params[:bid]) sets the id directly not sets the model
do_post
end
it "should assign the Bid the proper points" do
#bid.should_receive(:point=).with(1)
do_post
end
While I don't quite understand what's going on. (with stubs and the lambda)....
for
def bid
#bid = Bid.new params[:bid]
#bid.save
end
The following passes !!
require File.dirname(__FILE__) + '/../spec_helper'
include ApplicationHelper
include UsersHelper
include AuthenticatedTestHelper
describe "bidding on an item" do
controller_name :items
fixtures :users
before(:each) do
#user = login_as :quentin
#bid = mock_model(Bid) # create a new mock model so we can verify the appropriate things
#bid.stub!(:new).and_return(#bid) # stub the new class method on Bid to return our mock rather than a new ActiveRecord object.
#Bid.stub!(:save).and_return(true)# this separates our controller spec entirely from the database.
end
it "should create a new Bid" do
lambda do
post 'bid', :bid => { :auction_id => 1, :user_id => #user.id, :point => 1 }
end.should change(Bid, :count).by(1)
end
end