I have a block like this:
begin
response = Facebook.make_profile_request(params[:token])
rescue => e
Airbrake.notify(
:error_class => "Facebook Processing",
:error_message => "Error: #{e.message}"
)
flash[:notice] = "Uh oh...something went wrong. Please try again."
redirect_to root_path
end
This is what I have so far:
it "should notify Airbrake if call to FB fails" do
Facebook.stub(:make_profile_request).with(fb_token).and_raise(Exception)
Airbrake.should_receive(:notify)
get :facebook_process, token: fb_token
end
I get error:
1) UsersController GET facebook_process should notify Airbrake if call to FB fails
Failure/Error: get :facebook_process, token: fb_token
Exception:
Exception
# ./app/controllers/users_controller.rb:9:in `facebook_process'
# ./spec/controllers/users_controller_spec.rb:41:in `block (3 levels) in <top (required)>'
How should I properly test rescue?
You have to specify a specific exception class, otherwise rspec will bail as soon as it detects the exception; but, here is how you can do it without rescuing from Exception (as pointed out in Nick's comment).
class MyCustomError < StandardError; end
begin
response = Facebook.make_profile_request(params[:token])
rescue MyCustomError => e
...
end
And in your spec, you should make the stub return the custom error class. Something like this:
Facebook.stub(:make_profile_request).with(fb_token).and_raise(MyCustomError)
I have faced the similar issue recently.
if you change your code
rescue => e
to
rescue Exception => e
your test case will pass.
Related
I have a custom validation that checks whether a param is valid JSON or not:
def is_valid_json
begin
!!JSON.parse(preferences)
rescue
errors.add(:preferences, "This is not valid JSON")
end
end
In my controller test, I want to make sure that when I send in a bad value, the status code of the response is 422. Here is the spec from my controller:
it 'should return a 422 when validations fail' do
put :update, {:user_preferences => { :email => #email, :preferences => 'badval' } }
expect(response.status).to eq(422)
res = JSON.parse(response.body)
expect(res['error']).to_not be_blank
end
The test fails due to an error:
Failure/Error: put :update, {:user_preferences => { :email => #email, :preferences => 'badval' } }
ActiveRecord::RecordInvalid:
Validation failed: Preferences This is not valid JSON
Controller code:
def update
#user_preference = UserPreference.where(email: params[:user_preferences][:email]).first
authorize! :update, #user_preference
#user_preference.update_attributes!(params[:user_preferences])
render_api_response(#user_preference)
end
When I make the request from the browser, I get a 422 return status code, so is there a reason that I can't get the same result from the test?
The way I see it, update_attributes raises an exception, and you need to catch that. Perhaps you are doing an XHR call with your browser and you code handles that exception code (422) in the front end. For tests to work you should rescue the exception and respond with the relevant status in your render
rescue ActiveRecord::RecordInvalid do
render json: {
error: "Invalid params",
status: 422
},
status: 422
end
I have a rails 3.2.16 app that has a model and controller to upload a csv file that contains a list of customer details. In the app itself this works fine, however I can't get the test to work.
I basically get an error that says
undefined method 'first_name,last_name,address_1,address_2,city .... etc.'
So it is trying to use the first line of the csv file as a method ... ?
The files I am using are shown below
spec (the commented out lines show things that I have tried along the way having seen other issues in SO)
it "upload a file with correct properties" do
#include Rack::Test::Methods
# #file = fixture_file_upload(Rails.root.join('spec/fixtures/files/cust-imp-good.csv'), 'text/csv')
#file = Rack::Test::UploadedFile.new(Rails.root.join('spec/fixtures/files/cust-imp-good.csv'), 'text/csv')
post :create, :customer_import => #file
response.should be_success
end
uploader model
class CustomerImport #< ActiveRecord::Base
extend ActiveModel::Naming
include ActiveModel::Conversion
include ActiveModel::Validations
attr_accessor :file
def initialize(attributes = {})
debugger
attributes.each { |name, value| send("#{name}=", value) }
end
def persisted?
false
end
def save
if imported_customers.map(&:valid?).all?
valid_ids = true
dive_shop_ids = DiveShop.ids_array
discount_level_ids = DiscountLevel.ids_array
imported_customers.each_with_index do |customer, index|
if !dive_shop_ids.include?(customer.dive_shop_id)
errors.add :base, "Row #{index+2}: dive_shop_id #{customer.dive_shop_id} is not valid"
valid_ids = false
end
if !discount_level_ids.include?(customer.discount_level_id)
errors.add :base, "Row #{index+2}: discount_level_id #{customer.discount_level_id} is not valid"
valid_ids = false
end
end
if valid_ids
imported_customers.each(&:save!)
return_val = imported_customers.count
else
false
end
else
imported_customers.each_with_index do |customer, index|
customer.errors.each do |message|
errors.add :base, "Row #{index+2}: #{message}"
end
end
false
end
end
def imported_customers
#imported_customers ||= ImportRecord.load_imported_records("Customer", file)
end
end
From the error shown below I can see that it is failing in the initializer. Although if I put a debugger in there the initializer looks to be OK.
Output from debugger inside initializer
rdb:1 attributes
Rack::Test::UploadedFile:0x0000000b089a98 #content_type="text/csv", #original_filename="cust-imp-good.csv", #tempfile=#<File:/tmp/cust-imp-good.csv20131212-26548-ynutnh>>
rdb:1
Output from rspec failure message
Failures:
1) CustomerImportsController POST 'create' upload a file with correct properties
Failure/Error: post :create, :customer_import => #file
NoMethodError:
undefined method `first_name,last_name,address1,address2,address3,city,state,country,postcode,telephone,email,dob,local_contact,emergency_name,emergency_number,dive_shop_id,discount_level_id
=' for #<CustomerImport:0x0000000a5f7580>
# ./app/models/customer_import.rb:10:in `block in initialize'
# ./app/models/customer_import.rb:10:in `initialize'
# ./app/controllers/customer_imports_controller.rb:14:in `new'
# ./app/controllers/customer_imports_controller.rb:14:in `create'
# ./spec/controllers/customer_imports_controller_spec.rb:20:in `block (3 levels) in <top (required)>'
any help would be much appreciated I tried the solution shown in Undefined Method 'NameOfField' for #<Model:0x000...> i.e rake: db:test:prepare and bundle exec rspec . but this didn't work either
EDIT to include controller code
class CustomerImportsController < ApplicationController
before_filter do
#menu_group = "diveshop"
end
def new
#customer_import = CustomerImport.new
end
def create
if params[:customer_import] != nil
#customer_import = CustomerImport.new(params[:customer_import])
return_value = #customer_import.save # need to add #customer_import.file here
if return_value != false
addauditlog("A bulk import of customers was carried out")
redirect_to customers_url, notice: "Imported #{return_value} customers successfully."
else
render :new
end
else
flash[:error] = "You have not selected a file"
redirect_to new_customer_import_url
end
end
end
In creating the new model instance, your controller seems to have passed a hash as a parameter with a key whose value is the first line of the csv file. You'll need to share the controller code and the first line of the file you've updated in order to be able to confirm that and provide more information.
I am trying to run specs for two custom validators:
spec/validators/email_validator_spec.rb
spec/validators/phone_validator_spec.rb
When I run bundle exec rspec spec/validators/ the phone_validator_spec.rb spec fails:
1) PhoneValidator with a valid phone number should be valid
Failure/Error: subject.should be_valid
expected valid? to return true, got false
# ./spec/validators/phone_validator_spec.rb:20:in `block (4 levels) in <top (required)>'
# ./spec/validators/phone_validator_spec.rb:18:in `each'
# ./spec/validators/phone_validator_spec.rb:18:in `block (3 levels) in <top (required)>'
However, when I run that spec individually using the command bundle exec rspec spec/validators/phone_validator_spec.rb, it passes.
When I remove the email_validator_spec.rb then phone_validator_spec.rb passes using the command bundle exec rspec spec/validators/.
I expect both specs to pass when I run bundle exec rspec spec/validators/. Can anyone explain to me what is happening?
Update:
Used zetetic's tip to print out the error hash:
1) PhoneValidator with a valid phone number should be valid
Failure/Error: subject.errors.should == {}
expected: {}
got: #<ActiveModel::Errors:0x37b2460 #base=#<Validatable:0x37b2700 #validation_context=nil, #errors=#<ActiveModel::Errors:0x37b2460 ...>, #phone_number="1112223333">, #messages={:email=>["is invalid"]}> (using ==)
Diff:
## -1 +1,8 ##
+#<ActiveModel::Errors:0x37b2460
+ #base=
+ #<Validatable:0x37b2700
+ #errors=#<ActiveModel::Errors:0x37b2460 ...>,
+ #phone_number="1112223333",
+ #validation_context=nil>,
+ #messages={:email=>["is invalid"]}>
# ./spec/validators/phone_validator_spec.rb:21:in `block (4 levels) in <top (required)>'
# ./spec/validators/phone_validator_spec.rb:18:in `each'
# ./spec/validators/phone_validator_spec.rb:18:in `block (3 levels) in <top (required)>'
It appears the Validatable class definitions are combined when both specs are run. Is this behavior expected? If I use distinct class names, both specs pass.
spec/validators/phone_validator_spec.rb
require 'active_model'
require 'rspec/rails/extensions'
require File.expand_path('app/validators/phone_validator')
class Validatable
include ActiveModel::Validations
attr_accessor :phone_number
validates :phone_number, phone: true
end
describe PhoneValidator do
subject { Validatable.new }
describe "with a valid phone number" do
it "should be valid" do
phone_numbers = ["1112223333", "123222ABCD"]
phone_numbers.each do |phone_number|
subject.phone_number = phone_number
subject.should be_valid
end
end
end
end
app/validators/phone_validator.rb
class PhoneValidator < ActiveModel::EachValidator
def validate_each(object, attribute, value)
return if value.blank?
unless value =~ /^[A-Za-z0-9]{10}$/
object.errors[attribute] << (options[:message] || "is not formatted properly")
end
end
end
spec/validators/email_validator_spec.rb
require 'active_model'
require 'rspec/rails/extensions'
require File.expand_path('app/validators/email_validator')
class Validatable
include ActiveModel::Validations
attr_accessor :email
validates :email, email: true
end
describe EmailValidator do
subject { Validatable.new }
describe "with a valid email address" do
it "should be valid" do
addresses = %w[user#foo.COM A_US-ER#f.b.org frst.lst#foo.jp a+b#baz.cn]
addresses.each do |valid_address|
subject.email = valid_address
subject.should be_valid
end
end
end
describe "with an invalid phone number" do
it "should be invalid" do
addresses = %w[user#foo,com user_at_foo.org example.user#foo]
addresses.each do |invalid_address|
subject.email = invalid_address
subject.should be_invalid
end
end
end
end
app/validators/email_validator.rb
require 'mail'
class EmailValidator < ActiveModel::EachValidator
def validate_each(object, attribute, value)
begin
m = Mail::Address.new(value)
# We must check that value contains a domain and that value is an email address
r = m.domain && m.address == value
t = m.__send__(:tree)
# We need to dig into treetop
# A valid domain must have dot_atom_text elements size > 1
# user#localhost is excluded
# treetop must respond to domain
# We exclude valid email values like <user#localhost.com>
# Hence we use m.__send__(tree).domain
r &&= (t.domain.dot_atom_text.elements.size > 1)
rescue => e
r = false
end
object.errors[attribute] << (options[:message] || "is invalid") unless r
end
end
Using rails 3.2.11, rspec-rails 2.11.0
Your model instance is invalid but you don't know why. Try changing
subject.should be_valid
to
subject.valid?
subject.errors.should == {}
Now the failure message will print out the error hash.
Another tip: Don't rescue Exception.
EDIT
It appears the Validatable class definitions are combined when both specs are run. Is this behavior expected?
Yes, that is normal for Ruby classes. When both spec files are required, each Validatable class body is executed, so you end up with a class that contains both validations.
You need to isolate the validations either by making the subjects pass the validation that is not under test, eg:
subject { Validatable.new(:email => "some value") }
or testing for the specific error message from the validation under test:
subject.valid?
subject.errors(:email).should include("is invalid")
PS. Seriously -- don't rescue Exception. Nothing good will come of that.
I run in to this problem myself, and yes you could rename the class but the solution I used is to create and teardown the Validatable class inside your spec.
Here's a code snippet:
describe "HttpUriValidator",
"Custom validator to ensure URL is a valid URI." do
# Create the dummy class once when the test is run.
before(:all) do
class Validatable
include ActiveModel::Validations
attr_accessor :url
validates :url, http_uri: true
end
end
# Must tearing down the class or it will taint other tests using its
# name.
after(:all) { Object.send(:remove_const, :Validatable) }
subject { Validatable.new }
EDIT::
Just a heads up when you are declaring a Module wrapping your tested class (to avoid namespacing other classes in the test) ie.
module Foo::Bar
describe Something do
after(:all) { Foo::Bar.send(:remove_const, :Testable) }
end
end
you will have to remove the constant from that namespace rather then Object.
I'm creating a series of objects on the fly using transaction and exception handling. Right now it handles the rollback and everything as expected but my rescue block doesn't attempt to render the action I tell it to.
Here's my code that handles the transaction
def post_validation
ActiveRecord::Base.transaction do
begin
params[:users].each do |user|
#process each user and save here
end
redirect_to root_path #success
rescue ActiveRecord::RecordInvalid
# something went wrong, roll back
raise ActiveRecord::Rollback
flash[:error] = "Please resolve any validation errors and re-submit"
render :action => "validation"
end
end
end
What's expected upon failure: Rolls back transaction and renders the action "validation".
What's happening upon failure: Rolls back transaction and attempts to render the view "post_validation" which doesn't exist.
Well it looks like there's a few things wrong with the code I provided. For starters you don't need to bother with the raise ActiveRecord::Rollback line, Rails does this behind the scenes when an exception is thrown inside of a transaction block. In addition the transaction block needed to be inside of the begin block. So the resulting code looked something like this:
def post_validation
begin
ActiveRecord::Base.transaction do
#process some new records here
redirect_to root_path
end
rescue ActiveRecord::RecordInvalid
# handle the exception here; the entire transaction gets rolled-back
flash[:error] = "Please resolve any validation errors and re-submit"
render :action => "validation"
end
end
I am trying to learn TDD and this is part of my homework I couldn't figure out how to do it.
I want to test create controller action, and here is my code for test:
require 'spec_helper'
describe MoviesController do
describe 'create' do
it 'should call the model method perform create!' do
Movie.should_receive(:create!).with({"title" => 'Milk', "rating" => 'R'})
post :create, :movie => {:title => 'Milk', :rating => 'R'}
end
end
end
But I got:
Failures:
1) MoviesController create should call the model method performe create!
Failure/Error: post :create, :movie => {:title => 'Milk', :rating => 'R'}
NoMethodError:
undefined method `title' for nil:NilClass
# ./app/controllers/movies_controller.rb:50:in `create'
# ./spec/controllers/movies_controller_spec.rb:7:in `block (3 levels) in <top (required)>'
Finished in 0.21714 seconds
And here is create action I am testing against. Yes, it is TDD, and yes, I am testing a
working code, and it is the testing doesn't working :D
def create
#movie = Movie.create!(params[:movie])
flash[:notice] = "#{#movie.title} was successfully created."
redirect_to movies_path
end
I don't even know why I got the undefined method error message? I have a few other test passed but I deleted in this code snippe for simplicity, so I don't think it is related db/model related config problem. But why it does not working and how to change it?
Cheers
When you do a should_receive like that, it will return nil so the next line (when setting the flash message) attempts to retrieve the title from nil. Change your should_receive to:
Movie.should_receive(:create!).with({"title" => 'Milk', "rating" => 'R'}).and_return(stub_model(Movie))