rspec testing model validations with subject - error - ruby-on-rails

This is my test for validation, i would like to find the best way to writing model specs, especially for validation. But I have problem with this code below.
require 'spec_helper'
describe Ad, :focus do
let(:ad) { Ad.sham!(:build) }
specify { ad.should be_valid }
it "not creates a new instane given a invalid attribute" do
ad = Ad.new
ad.should_not be_valid
end
[:title, :category_id, :email, :ad_content, :name, :price].each do |attr|
it "should require a #{attr}" do
subject.errors[attr].should include("blank")
end
end
end
When I run this spec i receive this error:
5) Ad should require a name
Failure/Error: subject.errors[attr].should include("blank")
expected [] to include "blank"
Diff:
## -1,2 +1,2 ##
-blank
+[]
# ./spec/model/ad_spec.rb:15:in `block (3 levels) in <top (required)>'

The problem here is that you're not calling valid? in that example before checking for errors. You're calling it (indirectly) in the previous example, but not the one that you're asserting that has errors.
The correct way is this:
[:title, :category_id, :email, :ad_content, :name, :price].each do |attr|
it "should require a #{attr}" do
subject.valid?
subject.errors[attr].should include("blank")
end
end

Related

Rails custom model validator fails rspec tests

I have a custom validator to verify the content of 2 fields in my database. When I use it through my UI, it works fine, however my rspec tests are failing and I can't understand why.
Here is the rspec test:
require 'rails_helper'
RSpec.describe Device, type: :model do
before(:each) { #user = User.create(email: 'test#test.com', password: 'password', password_confirmation: 'password') }
before(:each) { #device = Device.create(user_id: #user.id) }
subject { #device }
it { should allow_value('192.168.1.1').for(:ips_scan) }
it { should allow_value('192.168.1.1').for(:ips_exclude) }
it { should_not allow_value('192.168.1.1, a.b.c.d').for(:ips_scan) }
it { should_not allow_value('a.b.c.d').for(:ips_exclude) }
end
The Device model is:
class Device < ApplicationRecord
belongs_to :user
validates :ips_scan, :ips_exclude, ip: true, on: :update
end
And my ip_validator concern is:
class IpValidator < ActiveModel::Validator
def validate(record)
if record.ips_scan
ips = record.ips_scan.split(',')
ips.each do |ip|
/([0-9]{1,3}\.){3}[0-9]{1,3}(\/([1-2][0-9]|[0-9]|3[0-2]))?(-([0-9]{1,3}))?/ =~ ip
record.errors.add(:ips_scan, 'is not valid') unless $LAST_MATCH_INFO
end
end
if record.ips_exclude
ips = record.ips_exclude.split(',')
ips.each do |ip|
/([0-9]{1,3}\.){3}[0-9]{1,3}(\/([1-2][0-9]|[0-9]|3[0-2]))?(-([0-9]{1,3}))?/ =~ ip
record.errors.add(:ips_exclude, 'is not valid') unless $LAST_MATCH_INFO
end
end
end
end
Ironically, the validator is correctly passing the should_not allow_value tests, however the should allow_value tests are failing:
Failures:
1) Device should allow :ips_scan to be ‹"192.168.1.1"›
Failure/Error: it { should allow_value('192.168.1.1').for(:ips_scan) }
After setting :ips_scan to ‹"192.168.1.1"›, the matcher expected the
Device to be valid, but it was invalid instead, producing these
validation errors:
* ips_scan: ["is not valid"]
# ./spec/models/device_spec.rb:22:in `block (2 levels) in <top (required)>'
2) Device should allow :ips_exclude to be ‹"192.168.1.1"›
Failure/Error: it { should allow_value('192.168.1.1').for(:ips_exclude) }
After setting :ips_exclude to ‹"192.168.1.1"›, the matcher expected the
Device to be valid, but it was invalid instead, producing these
validation errors:
* ips_exclude: ["is not valid"]
# ./spec/models/device_spec.rb:23:in `block (2 levels) in <top (required)>'
At this point, I'm at a loss as to what is wrong now. Any help is much appreciated! Thx!
I found the solution. I don't know why, but the RSPEC doesn't know the $LAST_MATCH_INFO. If you replace it with $~, what it is actually is, it should work. At least, it works for me.
$LAST_MATCH_INFO is part of the library English which should be enabled in Rspec probably. But for me, the problem is resolved.

rails 5 validators not protecting against a missing attribute during Model create?

cannot seem to get my validators to work to ensure all attributes are present to allow a User to be created. Basic User with 2 attributes
class User < ApplicationRecord
validates :name, presence: true
validates :email, presence: true
end
tests to check that name and email are present when created. these #pass
RSpec.describe User, type: :model do
context 'validations' do
subject { FactoryGirl.build(:user) }
it { is_expected.to validate_presence_of(:email) }
it { is_expected.to validate_presence_of(:name) }
it "fails to create user unless both are present" do
expect { User.create(:name => 'jo bloggs1', :noemail => 'c#c.co')}.to raise_error(ActiveModel::UnknownAttributeError)
end
end
end
but if i try and create model with a missing attribute no error is raised
it "fails to create user unless both are present" do
expect { User.create(:name => 'jo bloggs1')}.to raise_error(ActiveModel::MissingAttributeError)
end
result
1) User validations fails to create user unless both are present
Failure/Error: expect { User.create(:name => 'jo bloggs1')}.to raise_error(ActiveModel::MissingAttributeError)
expected ActiveModel::MissingAttributeError but nothing was raised
# ./spec/models/user_spec.rb:12:in `block (3 levels) in <top (required)>'
fyi, FactoryGirl
FactoryGirl.define do
factory :user do
name "MyString"
email "MyString"
end
end
i have tried clever stuff like
class User < ApplicationRecord
# before_create :run_it
after_initialize :all_present?
validates :name, presence: true
validates :email, presence: true
private
def all_present?
if (#email.nil? || #name.nil?)
raise ActiveModel::MissingAttributeError.new()
end
end
end
but cannot seem to raise these manually...?
what am i doing wrong?
tx all
Ben
The problem is that there are 2 methods, create and create!. The first, create
The resulting object is returned whether the object was saved successfully to the database or not
Whereas with create!:
Raises a RecordInvalid error if validations fail, unlike Base#create
So, create fails silently and doesn't raise any exceptions, but you can still inspect the instance and see that it's a new record and has errors and such, and create! fails noisily, by raising the error you are expecting it to raise. In short, your test should be:
it "fails to create user unless both are present" do
expect { User.create!(:name => 'jo bloggs1')}.to raise_error(ActiveModel::MissingAttributeError)
end

Why does this spec of my model's uniqueness validation fail when it should pass?

I am learning testing with RSpec. Something is not working with my tests.
My model:
class User < ActiveRecord::Base
has_secure_password
# Validation macros
validates_presence_of :name, :email
validates_uniqueness_of :email, case_sensitive: false
end
My factory:
FactoryGirl.define do
factory :user do
name "Joe Doe"
email "joe#example.com"
password_digest "super_secret_password"
end
end
And my spec:
require 'rails_helper'
RSpec.describe User, type: :model do
user = FactoryGirl.build(:user)
it 'has a valid factory' do
expect(FactoryGirl.build(:user)).to be_valid
end
it { is_expected.to respond_to(:name) }
it { is_expected.to respond_to(:email) }
it { is_expected.to respond_to(:password) }
it { is_expected.to respond_to(:password_confirmation) }
it { expect(user).to validate_presence_of(:name) }
it { expect(user).to validate_presence_of(:email) }
it { expect(user).to validate_presence_of(:password) }
it { expect(user).to validate_uniqueness_of(:email).case_insensitive }
end
I expected this test to pass. But I get this as a result:
Failures:
1) User should validate that :email is case-insensitively unique
Failure/Error: it { expect(user).to validate_uniqueness_of(:email).case_insensitive }
User did not properly validate that :email is case-insensitively unique.
The record you provided could not be created, as it failed with the
following validation errors:
* name: ["can't be blank"]
# ./spec/models/user_spec.rb:18:in `block (2 levels) in <top (required)>'
Finished in 0.34066 seconds (files took 1.56 seconds to load) 9
examples, 1 failure
Failed examples:
rspec ./spec/models/user_spec.rb:18 # User should validate that :email
is case-insensitively unique
What I am missing?
Update
I think that this is a bug: https://github.com/thoughtbot/shoulda-matchers/issues/830
It is because you are declaring it 2 times IMO! First building user then building same user inside expect().
Just use ur first user that you have built with factory-bot like so:
it 'has a valid factory' do
expect(user).to be_valid
end
P.S
It is better to use Faker gem instead of using harcoded instances like you did in factory.rb
Your Variable Is Currently Only Set Once for All Tests
When you write code like:
RSpec.describe User, type: :model do
user = FactoryGirl.build(:user)
end
you aren't building a new user each time you run a new spec. Likewise, using #let is the wrong approach, because it memoizes the variable even between tests. Instead, you need a to use an RSpec before#each block. For example:
describe User do
before do
#user = FactoryGirl.build :user
end
# some specs
end
If you have tests which are persisting you user to the database, and if you have disabled rollback or database cleaning between tests, then your defined factory (as currently written) will certainly fail the uniqueness validation. In such cases, you may want to try:
User.delete_all in your test, or otherwise cleaning your database between tests.
Using FactoryGirl sequences or the Faker gem to ensure that user attributes are actually unique.
USE let
RSpec.describe User, type: :model do
let(:user) { FactoryGirl.build(:user) }
# other what you need

Test failing when trying to run Rspec with Factory Girl

While trying to incorporate Factory Girl in my project I'm running into an error I just can't seem to solve. I wrote a test that would check if my user's first name was empty:
# spec/models/user_spec.rb
require 'rails_helper'
RSpec.describe User, :type => :model do
it 'is invalid without a first name' do
user = FactoryGirl.build(:user, first_name: nil)
expect(user).to have(1).errors_on(:first_name)
end
end
Unfortnately when I try to run this test I get this error:
1) User is invalid without a first name
Failure/Error: expect(user).to have(1).errors_on(:first_name)
expected 1 errors on :first_name, got 2
# ./spec/models/user_spec.rb:7:in `block (2 levels) in '
Here's what my factories.rb file looks like:
# spec/factories.rb
FactoryGirl.define do
factory :user do
first_name "John"
last_name "Doe"
sequence(:email) {|n| "johndoe#{n}#example.com"}
password "secret"
end
end
If it helps at all here's how my gemfile is setup:
group :development, :test do
gem 'rspec-rails'
gem 'rspec-collection_matchers'
gem 'factory_girl_rails'
end
Update
After checking my User model I believe that the second error was me mistakenly setting the presence validation twice in my model:
validates :first_name, :last_name, :email, :password, presence: true
validates :first_name, :last_name, presence: true, format: {with: /\A([^\d\W]|[-])*\Z/, message: 'cannot have any numbers or special characters'}
What I wonder now is a way for rspec to somehow point out the errors I'm dealing with instead of vaguely telling me:
expected 1 errors on :first_name, got 2
It seems that your user actually have 2 errors on the first_name field
To debug it you can just print the errors
RSpec.describe User, :type => :model do
it 'is invalid without a first name' do
user = FactoryGirl.build(:user, first_name: nil)
puts user.errors.messages[:first_name]
expect(user).to have(1).errors_on(:first_name)
end
end

Rspec: How to write conditional validation when it also works with validate

My Location model which can have a website. The website only has to be present if the location is online. Before it is saved, the website must have the correct format.
location.rb
class Location < ActiveRecord::Base
attr_accessible :online :website
validates_presence_of :website, :if => 'online.present?'
validates_inclusion_of :online, :in => [true, false]
validate :website_has_correct_format, :if => 'website.present?'
private
def website_has_correct_format
unless self.website.blank?
unless self.website.downcase.start_with?('https://', 'http://')
errors.add(:website, 'The website must be in its full format.)
end
end
end
end
I make a spec to test for this:
location_spec.rb
require 'spec_helper'
describe Location do
before(:each) do
#location = FactoryGirl.build(:location)
end
subject { #location }
describe 'when website is not present but store is online' do
before { #location.website = '' && #location.online = true }
it { should_not be_valid }
end
end
The test fails, bringing to me the error:
Failures:
1) Location when website is not present but store is online
Failure/Error: it { should_not be_valid }
NoMethodError:
undefined method `downcase' for true:TrueClass
#./app/models/location.rb:82:in `website_has_correct_format'
#./spec/models/location_spec.rb:72:in `block (3 levels) in <top (required)>'
What is the solution to this problem?
Your spec file is written a little bit wrong. The && is not working the way you expect it to.
require 'spec_helper'
describe Location do
before(:each) do
#location = FactoryGirl.build(:location)
end
subject { #location }
describe 'when website is not present but store is online' do
before do
#location.website = ''
#location.online = true
end
it { should_not be_valid }
end
end

Resources