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.
Related
I'm writing a test for my Rails 5 API to confirm a password_reset_token is generated whenever a user requests a password reset.
I have a short password_reset_controller that handles the initial request:
class PasswordResetController < ApplicationController
def new
#user = User.find_by(email: params[:email].to_s.downcase)
#user.send_password_reset if #user
end
end
send_password_reset is a method in my user model which calls the the generate_password_reset_instructions:
def send_password_reset
self.generate_password_reset_instructions
UserMailer.password_reset(self).deliver
end
def generate_password_reset_instructions
self.password_reset_token = SecureRandom.hex(10)
self.password_reset_sent_at = Time.now.utc
save
end
This should save the password_reset_token to my db (indeed, when I debug it, I know it goes through these lines of code). The following test suite passes with the exception of the last one:
require 'rails_helper'
RSpec.describe 'Password Reset API' do
let!(:user) { create(:user) }
describe 'POST password_reset/new' do
it 'returns status code 204 with user found' do
post '/password_reset/new', params: {email: user.email}
expect(response).to have_http_status(204)
end
it 'returns status code 204 with no user found' do
post '/password_reset/new', params: {email: 'not#anemail.com'}
expect(response).to have_http_status(204)
end
it "sends an email" do
expect { UserMailer.password_reset(user).deliver }.to change { ActionMailer::Base.deliveries.count }.by(1)
end
it 'confirmation_email is sent to the right user' do
UserMailer.password_reset(user).deliver
mail = ActionMailer::Base.deliveries.last
expect(mail.to[0]).to eq user.email
end
it 'generates password_reset_token' do
expect { post '/password_reset/new', params: {email: user.email} }.to change { user.password_reset_token }
end
end
end
The result I get in the terminal is:
Failures:
1) Password Reset API POST password_reset/new generates password_reset_token
Failure/Error: expect { post '/password_reset/new', params: {email: user.email} }.to change { user.password_reset_token }
expected result to have changed, but is still nil
# ./spec/requests/password_reset_spec.rb:28:in `block (3 levels) in <top (required)>'
# ./spec/rails_helper.rb:56:in `block (3 levels) in <top (required)>'
# ./spec/rails_helper.rb:55:in `block (2 levels) in <top (required)>'
This seems like it might be caused by the value not saving to the test db, but following suggestions in other posts (Like moving ENV['RAILS_ENV'] ||= 'test') to the top of rails_helper.rb does not solve the problem. I know that the actual methods work, as I'm able to get the desired result manually by running the server and making requests, I just can't write a test to confirm that it is indeed working. As the syntax in my test would indicate, I'm using FactoryGirl in my tests to generate users.
Any ideas as to why the last test (it 'generates password_reset_token' do) does not pass?
RSpec.describe 'Password Reset API' do
let!(:user) { create(:user) }
....
it 'generates password_reset_token' do
expect { post '/password_reset/new', params: {email: user.email} }.to change { user.password_reset_token }
end
end
What you expect:
You're creating a User and expecting it to be changed.
What is happening:
You create a Ruby User object in your test
You expect that object to be changed
Your Rails code initializes a new User object which updates the database and changes the attribute
Your test object remains untouched
What you need to do is to reload your test object with fresh attribute values from the database using user.reload.password_reset_token
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
There are so many moving parts to Rails testing that it's difficult to debug a failing test sometimes.
As a case in point, I am attempting to use zeus, guard, rspec, capybara, Poltergeist, PhantomJS, and FactoryGirl to simply build a User factory.
I am met with the following error message:
spec/models/user.rb
require 'spec_helper'
describe User do
describe 'Factories' do
it { expect(build :user).to be_valid }
end
end
spec/factories/users.rb
FactoryGirl.define do
factory :user do
sequence(:email) { |n| Forgery(:internet).email_address }
sequence(:company) { |n| Forgery(:name).company_name }
password "password"
password_confirmation "password"
end
end
Now I have a request spec that fails on the following line, which is the setup at the beginning:
let!(:user) { login_user }
The failure is this:
Failure/Error: let!(:user) { login_user }
NoMethodError:
undefined method `last_logged_in_at=' for #<User:0x007fe60aca95e8>
# ./app/models/user.rb:136:in `set_login_time'
# ./app/controllers/sessions_controller.rb:24:in `create'
# ./spec/support/login_macros.rb:7:in `login_user'
# ./spec/requests/app_integration_spec.rb:5:in `block (2 levels) in <top (required)>'
# ./custom_plan.rb:12:in `spec'
# -e:1:in `<main>'
So the problem appears to be that since User is not a real object, there is no such attribute as last_logged_in_at. So I should have to mock that somewhere. I tried putting this inside the factory code:
before(:build) do |u|
u.stub!(:read_attribute).with(:last_logged_in_at).and_return(Time.now)
end
The error remains the same. The test simply can not find this attribute. Keep in mind this code works perfectly in the browser. I am out of my league. Where am I going wrong?
Is your test db schema up-to-date?
rake db:test:prepare
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
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