I'm pretty new to testing.
I have a custom validation at my Profile model
def birth_date_cannot_be_in_the_future
errors.add(:birth_date, "the birth date cannot be in the future") if
!birth_date.blank? && birth_date > Date.today
end
At my factories.rb
sequence(:email) {|n| "person-#{n}#example.com"}
factory :user do
email
password 'password'
password_confirmation 'password'
confirmed_at Time.now
end
factory :profile do
user
first_name { "User" }
last_name { "Tester" }
birth_date { 21.years.ago }
end
At my models/profile_spec.rb
it 'birth date cannot be in the future' do
profile = FactoryGirl.create(:profile, birth_date: 100.days.from_now)
expect(profile.errors[:birth_date]).to include("the birth date cannot be in the future")
expect(profile.valid?).to be_falsy
end
When I run my test I receive the follow message:
Failure/Error: profile = FactoryGirl.create(:profile, birth_date: 100.days.from_now)
ActiveRecord::RecordInvalid:
The validation fails: Birth date the birth date cannot be in the future
What am I doing wrong?
There's a matcher just for catching errors. Without having tested, I'm assuming you can go:
expect { FactoryGirl.create(:profile, birth_date: 100.days.from_now) }.to raise_error(ActiveRecord::RecordInvalid)
Another approach though is to include the shoulda gem, which has the allow_value matcher. This lets you do something more like this in your model spec:
describe Profile, type: :model do
describe 'Birth date validations' do
it { should allow_value(100.years.ago).for(:birth_date) }
it { should_not allow_value(100.days.from_now).for(:birth_date) }
end
end
Generally you wont need FactoryGirl at all when you're testing things like validations. They become super useful in controller tests though.
Just put assertions for your model in a model spec, which tests your model code directly. These usually live in spec/models/model_name_spec.rb There are convenient shoulda matchers for a bunch of common model stuff:
describe SomeModel, type: :model do
it { should belong_to(:user) }
it { should have_many(:things).dependent(:destroy) }
it { should validate_presence_of(:type) }
it { should validate_length_of(:name).is_at_most(256) }
it { should validate_uniqueness_of(:code).allow_nil }
end
It should be using build instead of create
Also profile.valid? should be called before checking profile.errors
it 'birth date cannot be in the future' do
profile = FactoryGirl.build(:profile, birth_date: 100.days.from_now)
expect(profile.valid?).to be_falsy
expect(profile.errors[:birth_date]).to include("the birth date cannot be in the future")
end
Related
I have an object that takes the information from a rest API. Let's say I call Posts from an API and I can specify which fields I want to extract. Just an example of how it looks:
Post.find!(id: 1, fields: [:id, :title, :description])
This does an API call and it returns me a Post object with these specified fields.
For testing, I am stubbing this API call with Factory Bot and returns directly a Post object with all possible fields it can query.
This approach is not the best since the tests are always returning all fields and code itself maybe I just need a couple of fields not all of them
So I am trying to achieve something like (in FactoryBot):
build(:post, fields: [:id,:title]) and set up a Post object just with id and title.
If I do build(:post, fields: [:title, :created_at]) and set up a Post object just with title and created_at. And so on...
Did some research and trying some ideas, but failed in all of them about how to build this flow.
Any idea about how to achieve this behavior?
EDIT
Traits seems a nice alternative, but I must be as consistent as the API call, specifying these fields. So traits are not working for me...
Let's assume that this is your factory for the Post:
FactoryBot.define do
factory :post do
sequence(:title) { |n| "Post no. #{n}" }
description 'Post description'
created_at { DateTime.now }
end
end
When you call build(:post) it will create an object with title, created_at and description set.
But if you will remove those fields from your factory (or move them under trait):
FactoryBot.define do
factory :post do
trait :all_fields do
sequence(:title) { |n| "Post no. #{n}" }
description 'Post description'
created_at { DateTime.now }
end
end
end
Then:
calling build(:post, title: 'Post title', description: 'Post description') will create a post object where only title and description are set (created_at will be nil)
calling build(:post, :all_fields) will create a post object will all fields set.
Edit
I think I understand the problem better now. Let's assume that this is you factory:
FactoryBot.define do
factory :post do
sequence(:title) { |n| "Post no. #{n}" }
created_at { DateTime.now }
description 'Post description'
end
end
Change it to:
FactoryBot.define do
factory :post do
transient do
fields []
end
sequence(:title) { |n| "Post no. #{n}" }
created_at { DateTime.now }
description 'Post description'
after :build do |post, evaluator|
unless evaluator.fields.empty? do
(post.attributes.keys - evaluator.fields).each do |attribute_to_remove|
post.send("#{attribute_to_remove}=", nil)
end
end
end
end
end
Then, you can call it like this:
build(:post) creates post with all fields
build(:post, fields: ['description', 'title']) creates a post where everything except description and title is nil.
This solution should work as expected but it might slow down your tests (and I think it does not look nice :) )
FactoryBot lets you override the factory by passing a hash of attributes - so why not just set the attributes to nil:
build(:post, {
title: nil,
description: nil
})
Best practice in FactoryBot is to have your standard factory produce objects that contain only the required fields for a model. (Note that FactoryBot will generate an id for you automatically.)
Assume you have a model called Post that requires only a title, but has optional fields description and date. Your factory would look like this:
FactoryBot.define do
factory :post do
title 'Post Title'
end
end
Now when you build a Post, it will look like this:
>> FactoryBot.build_stubbed(:post)
#> <Post id: 1001, title: "Post Title", description: nil, date: nil>
You can add the optional fields on a case-by-case basis:
>> FactoryBot.build_stubbed(:post, description: 'This is a test post.')
#> <Post id: 1001, title: "Post Title", description: 'This is a test post.', date: nil>
Or you can add traits within the factory:
FactoryBot.define do
factory :post do
title 'Post Title'
trait :with_description_and_date do
description 'This is a test post.'
date Date.today
end
end
end
>> FactoryBot.build_stubbed(:post, :with_description_and_date)
#> <Post id: 1001, title: "Post Title", description: 'This is a test post.', date: Thu, 12 Apr 2018>
I am beginner to RSpec. I am having a model teacher that has_many :lessons. Here is my FactoryGirls records:
spec/factories/lessons.rb
FactoryGirl.define do
factory :lesson do
title "Rspec test"
description "test description"
company_name "narola pvt"
association :teacher
location "Zwanenplein 34"
days_to_pay 2
end
end
spec/factories/teachers.rb
FactoryGirl.define do
factory :teacher do
first_name "Teacher's name"
last_name "Teacher's last name"
address "los angeles"
city "california"
zip_code "12345"
country "USA"
birthdate nil
phone nil
password "password"
email { "example#{SecureRandom.uuid}#email.dummy" }
end
end
Following is my try with models test:
spec/models/teacher_spec.rb
require 'rails_helper'
RSpec.describe Teacher, type: :model do
let(:teacher) { FactoryGirl.create(:teacher) }
it "should have at least one lesson" do
config.expect_with(Lesson.where(teacher_id: teacher)){|c| c.syntax = :should}
end
end
I am willing to write a rspec test case to find if lesson exists for particular lesson.
Please try this:
it "should have at least one lesson" do
expect(Lesson.where(teacher_id: teacher.id)).to exist
end
Let me know if it's work for you. I haven't try this.
it "should have at least one lesson" do
expect(Lesson.where(teacher_id: teacher.id).exists?).to be_truthy
end
This would be faster because of the use of 'exists?' method compared to
expect(Lesson.where(teacher_id: teacher.id)).to exist
Underlying query execution due to use of 'exists?' the method is fast. More details are here ->
https://www.ombulabs.com/blog/benchmark/performance/rails/present-vs-any-vs-exists.html
I'm with the following problem:
Environment: Ruby: 2.3.1 and Rails 5.0.0.1
I'm trying to validate a datetime field with RSpec and Factory Girl.
I got this error:
expected: "2016-11-11 13:30:31 UTC" (From Factory Girl)
got: "2016-11-11T13:30:31.218Z" (From database)
My code:
klass_object = FactoryGirl.create(:character)
klass = Character
RSpec.shared_examples 'API GET #index' do |klass|
before { get :index, params: params, accept: Mime[:json] }
it "returns a list of #{klass.to_s.underscore.pluralize}" do
object_array = json(response.body)
klass_attributes = klass.attribute_names.without("id", "created_at", "updated_at").map(&:to_sym)
klass_attributes.each do |attribute|
object_array.each do |object|
expect(object[attribute].to_s).to eq(klass_object[attribute].to_s)
end
end
end
...
end
Factory:
FactoryGirl.define do
factory :character do
marvel_id { Faker::Number.number(6).to_i }
name { Faker::Superhero.name }
description { Faker::Hipster.paragraphs(1) }
modified { Faker::Date.between(DateTime.now - 1, DateTime.now) }
factory :invalid_character do
id ''
name ''
marvel_id ''
modified ''
end
end
end
How can I correct this problem?
I did that, it works but I think it is not so good. There is a better way to do it?
object_array.each do |object|
if ActiveSupport::TimeWithZone == klass_object[attribute].class
expect(object[attribute].to_datetime.strftime("%Y-%m-%d %H:%M:%S")).to eq(klass_object[attribute].to_datetime.strftime("%Y-%m-%d %H:%M:%S"))
else
expect(object[attribute].to_s).to eq(klass_object[attribute].to_s)
end
end
Thanks for your help.
I can suggest you to change your approach to compare the results. You can use approach, which based on the idea of the golden master.
In according to this approach you take a snapshot of an object, and then compare all future versions of the object to the snapshot.
In your case you can write json fixture first time, check that json is correct and compare it with result json next time.
For example
approved.json
[
{
"travel_time_seconds": 43200,
"available_seats_amount": 10,
"departure_at": "2016-04-08T02:00:00.000+03:00",
"arrival_at": "2016-04-08T17:00:00.000+03:00",
"source_point_name": "New York",
"destination_point_name": "Moscow",
"tickets_count": 2
}
]
controller_spec.rb
RSpec.shared_examples 'API GET #index' do |klass|
before { get :index, params: params, accept: Mime[:json] }
it "returns a list of #{klass.to_s.underscore.pluralize}" do
verify(format: :json) { json(response.body).map {|o| o.except('id', 'created_at', 'updated_at' }
end
...
end
approvals gem, for example, can help you with that
I know this is a very old question but I just came across the solution to this today and couldn't find another answer. I've been using Faker too, but the Date/Time formats don't seem to work very well with Ruby time math without a lot of finagling.
However, if you use Time in your factory, and then convert it .to_i, it will get sent to the db in the correct format for comparison in rspec.
Migration:
class CreateExperiences < ActiveRecord::Migration[5.2]
def change
create_table :experiences do |t|
t.datetime :start_date
Factory:
FactoryBot.define do
when_it_started = Time.zone.now - rand(3000).days
factory :experience do
start_date { when_it_started.to_i }
Spec:
RSpec.describe "Experiences API", type: :request do
let!(:valid_attributes) { attributes_for(:experience) }
describe "POST /v1/experiences" do
before { post "/v1/experiences", params: valid_attributes }
it "creates an experience" do
expect(JSON.parse(response.body).except("id", "created_at", "updated_at")).to eq(valid_attributes.stringify_keys)
end
Then my tests passed. Hopefully this will help someone else!
Try to use to_datetime instead to_s
expect(object[attribute].to_datetime).to eq(klass_object[attribute].to_datetime)
I try to test validation method that check times overlap for activities.
There are three factories(two of them inherit from activity).
Factories:
activities.rb
FactoryGirl.define do
factory :activity do
name 'Fit Girls'
description { Faker::Lorem.sentence(3, true, 4) }
active true
day_of_week 'Thusday'
start_on '12:00'
end_on '13:00'
pool_zone 'B'
max_people { Faker::Number.number(2) }
association :person, factory: :trainer
factory :first do
name 'Swim Cycle'
description 'Activity with water bicycles.'
active true
day_of_week 'Thusday'
start_on '11:30'
end_on '12:30'
end
factory :second do
name 'Aqua Crossfit'
description 'Water crossfit for evereyone.'
active true
day_of_week 'Thusday'
start_on '12:40'
end_on '13:40'
pool_zone 'C'
max_people '30'
end
end
end
Activities overlaps when are on same day_of_week(activity.day_of_week == first.day_of_week), on same pool_zone(activity.pool_zone == first.pool_zone) and times overlaps.
Validation method:
def not_overlapping_activity
overlapping_activity = Activity.where(day_of_week: day_of_week)
.where(pool_zone: pool_zone)
activities = Activity.where(id: id)
if activities.blank?
overlapping_activity.each do |oa|
if (start_on...end_on).overlaps?(oa.start_on...oa.end_on)
errors.add(:base, "In this time and pool_zone is another activity.")
end
end
else
overlapping_activity.where('id != :id', id: id).each do |oa|
if (start_on...end_on).overlaps?(oa.start_on...oa.end_on)
errors.add(:base, "In this time and pool_zone is another activity.")
end
end
end
end
I wrote rspec test, but unfortunatelly invalid checks.
describe Activity, 'methods' do
subject { Activity }
describe '#not_overlapping_activity' do
let(:activity) { create(:activity) }
let(:first) { create(:first) }
it 'should have a valid factory' do
expect(create(:activity).errors).to be_empty
end
it 'should have a valid factory' do
expect(create(:first).errors).to be_empty
end
context 'when day_of_week, pool_zone are same and times overlap' do
it 'raises an error that times overlap' do
expect(activity.valid?).to be_truthy
expect(first.valid?).to be_falsey
expect(first.errors[:base].size).to eq 1
end
end
end
end
Return:
Failure/Error: expect(first.valid?).to be_falsey
expected: falsey value
got: true
I can't understand why it got true. First create(:activity) should be right, but next shouldn't be executed(overlapping).
I tried add expect(activity.valid?).to be truthy before expect(first.valid?..., but throws another error ActiveRecord::RecordInvalid. Could someone repair my test? I'm newbie with creation tests using RSpec.
UPDATE:
Solution for my problem is not create :first in test but build.
let(:first) { build(:first) }
This line on its own
let(:activity) { create(:activity) }
doesn't create an activity. It only creates an activity, when activity is actually called. Therefore you must call activity somewhere before running your test.
There are several ways to do so, for example a before block:
before { activity }
or you could use let! instead of just let.
I'm setting up a model for recordings with the following constraints
class Recording < ActiveRecord::Base
attr_accessible :agent_id, :confirmation, :filepath, :phone, :call_queue_id, :date
belongs_to :call_queue
PHONE_FORMAT = /^[0-9]+$|Unavailable/
validates_presence_of :call_queue_id, :agent_id, :phone, :filepath, :date
validates :phone, format: { with: PHONE_FORMAT }
end
and am trying to test it with the following spec
describe Recording do
let(:queue) { FactoryGirl.create(:call_queue) }
before { #recording = queue.recordings.build(FactoryGirl.attributes_for(:recording)) }
subject { #recording }
# Stuff omitted...
describe "phone" do
it "should be present" do
#recording.phone = ''
#recording.should_not be_valid
end
context "with a valid format" do
it "should only consist of digits" do
#recording.phone = 'ab4k5s'
#recording.should_not be_valid
end
it "should only match 'Unavailable'" do
#recording.phone = 'Unavailable'
#recording.should be_valid
end
end
end
end
The first two tests pass, but the third fails with the following:
Failure/Error: #recording.should be_valid
expected valid? to return true, got false
I tested my regex with rubular to make sure it was working, and then again using irb just to be sure. I'm really confused why this is failing.
EDIT:
I eventually got the specs to pass by changing my before statement in the rspec:
describe Recording do
let(:queue) { FactoryGirl.create(:call_queue) }
before(:each) { #recording = queue.recordings.create(FactoryGirl.attributes_for(:recording) }
# the rest is the same...
which ultimately makes sense to me, to a point. Was the reason everything was getting messed up (falses were returning true, and vice versa) because once an attribute made the record invalid, I couldn't change it again? It seems like that was the case, I just want to make sure.
try:
PHONE_FORMAT = /^([0-9]+|Unavailable)$/