Testing Multiple Custom Validators with RSpec - ruby-on-rails-3

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.

Related

Rails API post in request spec should pass params but getting empty

To start, in my request spec sites.spec.rb I have this test:
describe "POST /v1/sites" do
let(:valid_attributes) { { url: "www.example.com", site_code: "123456" } }
context 'when the request is valid' do
before { post v1_sites_path, params: valid_attributes }
it 'creates a site' do
expect(json['url']).to eq("www.example.com")
expect(json['site_code']).to eq("123456")
end
it 'returns status code 201' do
expect(response).to have_http_status(201)
end
end
I then get a failing test for "creates a site"...
1) Sites POST /v1/sites when the request is valid creates a site
Failure/Error: expect(json['url']).to eq("www.example.com")
expected: "www.example.com"
got: ["can't be blank"]
(compared using ==)
# ./spec/requests/sites_spec.rb:61:in `block (4 levels) in <top (required)>'
# ./spec/rails_helper.rb:84:in `block (3 levels) in <top (required)>'
# ./spec/rails_helper.rb:83:in `block (2 levels) in <top (required)>'
Now this technically makes sense because my Site model has validates :url, :site_code, presence: true. So the test is failing because the post is not passing the params correctly.
Lastly, here is the controller:
module Api::V1
class SitesController < BaseApiController
before_action :set_site, only: [:show, :update, :destroy]
# GET /sites
def index
#sites = Site.all
render json: #sites
end
# GET /sites/1
def show
render json: #site
end
# POST /sites
def create
#site = Site.new(site_params)
if #site.save
render json: #site, status: :created, location: #site
else
render json: #site.errors, status: :unprocessable_entity
end
end
# PATCH/PUT /sites/1
def update
if #site.update(site_params)
render json: #site
else
render json: #site.errors, status: :unprocessable_entity
end
end
# DELETE /sites/1
def destroy
#site.destroy
end
private
# Use callbacks to share common setup or constraints between actions.
def set_site
#site = Site.find(params[:id])
end
# Only allow a trusted parameter "white list" through.
def site_params
# params.require(:data).require(:attributes).permit(:url, :side_code, :user_id)
# params.require(:site).permit(:url, :side_code, :user_id)
params.fetch(:site, {}).permit(:url, :side_code)
end
end
end
I am speculating that the way I am passing parameters to the post for Rails API is perhaps not formatted or correctly or something else entirely. I did play with the params in the test block trying data: { attributes: valid_attributes } with no luck.
Any thoughts or suggestions are greatly appreciated!
This problem was indeed due to format of the parameters I was passing to POST request in the test block. I tested the POST via the command line and watched the rails server to see how the params were coming through. They looked like this:
Parameters: {"site_code"=>"123456", "url"=>"www.updated.com", "subdomain"=>"api", "id"=>"2", "site"=>{"site_code"=>"123456", "url"=>"www.updated.com"}}
Then in my sites_spec.rb, I copied this format for the valid params of the post request:
let(:valid_attributes) { { "site"=>{"url"=>"www.example.com", "user_id"=>user_id, "site_code"=>"123456"} } }
This works. The JSON format of the params needed to be formatted in the test block the same way they would be if it was real JSON request.

devise and rspec-rails - How to sign-in user in Request type specs (specs tagged with type: :request)?

Environment
Rails 4.2.0
ruby-2.2.1 [ x86_64 ]
devise 3.4.1
rspec-core 3.2.2
rspec-rails 3.2.1
In my /spec/rails_helper.rb I have included Devise helpers for spec files tagged with type: :controller and type: :request
spec/rails_helper.rb
ActiveRecord::Migration.maintain_test_schema!
RSpec.configure do |config|
# Remove this line if you're not using ActiveRecord or ActiveRecord fixtures
config.fixture_path = "#{::Rails.root}/spec/fixtures"
# If you're not using ActiveRecord, or you'd prefer not to run each of your
# examples within a transaction, remove the following line or assign false
# instead of true.
config.use_transactional_fixtures = false
config.before(:suite) do
DatabaseCleaner.strategy = :transaction
DatabaseCleaner.clean_with(:truncation)
end
config.before(:suite) do
begin
DatabaseCleaner.start
FactoryGirl.lint
ensure
DatabaseCleaner.clean
end
end
config.around(:each) do |example|
DatabaseCleaner.cleaning do
example.run # ==================> L-60
end
end
config.include FactoryGirl::Syntax::Methods
# RSpec Rails can automatically mix in different behaviours to your tests
# based on their file location, for example enabling you to call `get` and
# `post` in specs under `spec/controllers`.
#
# You can disable this behaviour by removing the line below, and instead
# explicitly tag your specs with their type, e.g.:
#
# RSpec.describe UsersController, :type => :controller do
# # ...
# end
#
# The different available types are documented in the features, such as in
# https://relishapp.com/rspec/rspec-rails/docs
config.infer_spec_type_from_file_location!
config.include Devise::TestHelpers, type: :controller
config.include Devise::TestHelpers, type: :request
end
With that config in place the type: controller specs runs fine. However when running type: request specs I am getting following error:
Failure/Error: Unable to find matching line from backtrace
NoMethodError:
undefined method `env' for nil:NilClass
# /home/.rvm/gems/ruby-2.2.1#myapp/gems/devise-3.4.1/lib/devise/test_helpers.rb:24:in `setup_controller_for_warden'
# ./spec/rails_helper.rb:60:in `block (3 levels) in <top (required)>'
# /home/.rvm/gems/ruby-2.2.1#simplyhomeapp/gems/database_cleaner-1.4.1/lib/database_cleaner/generic/base.rb:15:in `cleaning'
# /home/.rvm/gems/ruby-2.2.1#simplyhomeapp/gems/database_cleaner-1.4.1/lib/database_cleaner/base.rb:92:in `cleaning'
# /home/.rvm/gems/ruby-2.2.1#simplyhomeapp/gems/database_cleaner-1.4.1/lib/database_cleaner/configuration.rb:86:in `block (2 levels) in cleaning'
# /home/.rvm/gems/ruby-2.2.1#simplyhomeapp/gems/database_cleaner-1.4.1/lib/database_cleaner/configuration.rb:87:in `call'
# /home/.rvm/gems/ruby-2.2.1#simplyhomeapp/gems/database_cleaner-1.4.1/lib/database_cleaner/configuration.rb:87:in `cleaning'
# ./spec/rails_helper.rb:59:in `block (2 levels) in <top (required)>'
https://github.com/plataformatec/devise/blob/master/lib/devise/test_helpers.rb#L24 is following
def setup_controller_for_warden #:nodoc:
#request.env['action_controller.instance'] = #controller # ==================> L-24
end
I am aware that #request instance is not available for :request type specs and hence the error.
Are there any helpers available we can use to sign-in a user in :request type specs when using Devise?
I found a similar issue https://github.com/plataformatec/devise/issues/1114, the reply to which suggests following:
If you're doing integration tests, make sure to sign in your user in the tradicional way, by filling the sign in form and submitting.
But I would like to by pass the actual login for specs which requires a signed-in user.
Thanks.
With the help of a few SO posts(please refer to the References section below) I have managed to achieve the desired solution. I am posting my working code below, in case it can help others looking out for the same:
spec/rails_helper.rb
RSpec.configure do |config|
....
....
config.include Devise::TestHelpers, type: :controller
config.include Warden::Test::Helpers, type: :request
end
spec/shared_contexts.rb
RSpec.shared_context "api request global before and after hooks" do
before(:each) do
Warden.test_mode!
end
after(:each) do
Warden.test_reset!
end
end
RSpec.shared_context "api request authentication helper methods" do
def sign_in(user)
login_as(user, scope: :user)
end
def sign_out
logout(:user)
end
end
/spec/requests/api/logout_spec.rb
require 'rails_helper'
require 'shared_contexts'
RSpec.describe "Api Logout", :type => :request do
include_context "api request authentication helper methods"
include_context "api request global before and after hooks"
let(:email) { 'test_user1#example.com' }
let(:password) { 'password' }
# Assumes you have FactoryGirl included in your application's test group.
let!(:user) { create(:user, email: email, password: password) }
context "DELETE /logout" do
it "responds with 204 and signs out the signed-in user" do
sign_in(user)
# Till not figured out how to assert Warden has successfully logged in the user like we can do in a Devise controller spec by asserting subject.current_user. If anybody knows a way to do it please share.
# expect(subject.current_user).to_not be_nil
delete "/logout"
expect(response).to have_http_status(204)
end
end
end
I have still not figured out how to assert Warden has successfully logged in the user like we can do in a Devise controller spec by asserting expect(subject.current_user).to_not be_nil. If anybody knows a way to do it please share.
References
Integration test with rspec and devise sign_in env
https://github.com/plataformatec/devise/wiki/How-To:-Test-with-Capybara
https://github.com/hassox/warden/blob/master/lib/warden/test/helpers.rb
undefined method 'env' for nil:NilClass
https://github.com/plataformatec/devise/wiki/How-To:-Stub-authentication-in-controller-specs
The code in the above link still relies on a request object which is only available in Controller specs. Thus not useful for type: :request specs.
https://github.com/plataformatec/devise/issues/3555
Thanks,
Jiggnesh
While the popular answer here, also replicated on the Devise wiki, is ok, it is simplest to just:
spec/rails_helper.rb
RSpec.configure do |config|
# ...
config.include Devise::Test::IntegrationHelpers, type: :request
end
And just use sign_in in your request spec. This is the equivalent of declaring include Devise::Test::IntegrationHelpers in an system/feature spec or Rails system/controller test.
Reference
Devise wiki: How to sign in and out a user in request type specs - Simple approach

RSpec file upload raises 'undefined method' for header row

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.

Factory Girl sequences not incrementing

I'm trying to get FactoryGirl to generate some names for me, but the sequence doesn't seem to increment.
# spec/factories/vessel.rb
require 'factory_girl'
FactoryGirl.define do
sequence :vessel_name do |n|
"TK42#{n}"
end
factory :vessel do
name FactoryGirl.generate(:vessel_name)
vessel_type 'fermenter'
volume_scalar 100.0
volume_units 'bbl'
end
end
# spec/models/vessel_spec.rb
require 'spec_helper'
describe Vessel do
context 'working in the factory' do
it 'makes a valid vessel' do
vessel = FactoryGirl.create(:vessel)
vessel.should be_valid, "Invalid vessel #{vessel.valid? || vessel.errors.messages.inspect}"
end
it 'makes another valid vessel' do
vessel = FactoryGirl.create(:vessel)
vessel.should be_valid, "Invalid vessel #{vessel.valid? || vessel.errors.messages.inspect}"
end
end
end
The spec output is
Vessel
working in the factory
makes a valid vessel
makes another valid vessel (FAILED - 1)
Failures:
1) Vessel working in the factory makes another valid vessel
Failure/Error: vessel = FactoryGirl.create(:vessel)
ActiveRecord::RecordInvalid:
Validation failed: Name has already been taken
# ./spec/models/vessel_spec.rb:13:in `block (3 levels) in <top (required)>'
# app/models/vessel.rb
class Vessel < ActiveRecord::Base
attr_accessible :name, :vessel_type, :volume_scalar, :volume_units
validates :name, :presence => true, :uniqueness => true
end
0 HAL:0 work/nrb-brewery-management % bundle show factory_girl_rails rspec
/home/brundage/.rvm/gems/ruby-1.9.3-p0/gems/factory_girl_rails-3.5.0
/home/brundage/.rvm/gems/ruby-1.9.3-p0/gems/rspec-2.11.0
0 HAL:0 work/nrb-brewery-management % rails c test
Loading test environment (Rails 3.2.6)
1.9.3p0 :001 > FactoryGirl.generate :vessel_name
=> "TK422"
1.9.3p0 :002 > FactoryGirl.generate :vessel_name
=> "TK423"
1.9.3p0 :003 > FactoryGirl.generate :vessel_name
=> "TK424"
1.9.3p0 :004 > FactoryGirl.generate :vessel_name
=> "TK425"
Why doesn't FactoryGirl generate a sequence of names in my spec?
That works, bit it will mean that you can't override the name anywhere in the specs, because the after build hook will always run and overwrite any name.
The reason your original example doesn't work is that you're invoking the sequence when the factory is defined, rather than when the factory is run. You can provide a block to attribute definitions which will be invoked every time the factory runs. This way, you get a chance to generate a value for each instance, rather than generating one value for all instances. This is most frequently used for sequences and times.
You can fix your original example with this snippet:
sequence :vessel_name do |n|
"TK42#{n}"
end
factory :vessel do
name { generate(:vessel_name) }
vessel_type 'fermenter'
volume_scalar 100.0
volume_units 'bbl'
end
If all names can be generated with the same format, you could also leave out the value entirely by renaming your sequence:
sequence :name do |n|
"TK42#{n}"
end
factory :vessel do
name
vessel_type 'fermenter'
volume_scalar 100.0
volume_units 'bbl'
end
However, that won't work if you need different name formats for different factories.
And the answer is:
require 'factory_girl'
FactoryGirl.define do
sequence :vessel_name do |n|
"TK42#{n}"
end
factory :vessel do
vessel_type 'fermenter'
volume_scalar 100.0
volume_units 'bbl'
after :build do |v|
v.name = FactoryGirl.generate(:vessel_name)
end
end
end

rspec email validation

I'm learning Rspec. When i run my test case for email validation I'm getting an error which am not able to understand.Please explain.
1) User should check for email validation
Failure/Error: #test_user.should have(1).errors_on(:email)
expected 1 errors on :email, got 2
# ./spec/models/user_spec.rb:17:in `block (2 levels) in <top (required)>'
Finished in 0.12903 seconds
2 examples, 1 failure
My test cases are below:
it"should check whether name is present or not" do
#test_user.name = nil
#test_user.should have(1).errors_on(:name)
end
it "should check for email validation"do
#test_user.email = nil
#test_user.should have(2).errors_on(:email)
end
end
If you're validating the email attribute with both the presence validator and some sort of format validator, then you'd get two errors by setting the attribute to nil.
Maybe try something like:
validates :email, presence: true,
format: { with: %r\A([^#\s]+)#((?:[-a-z0-9]+\.)+[a-z]{2,})\Z/,
allow_nil: true }