Testing custom validation method with RSpec - ruby-on-rails

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.

Related

How do I raise two errors with graphql-ruby?

I'm using the graphql-ruby gem and I have a mutation that updates a record. This is all working, but now I want to include the phony_rails gem to validate phone number.
The problem
As a dev with more FE experience, I'm not great with rails or ruby and I'm using this to learn. That said, I have two phone numbers that I want to validate -- home_phone_number and mobile_phone_number. My mutation argument looks like this:
# frozen_string_literal: true
module Mutations
module Person
class UpdatePerson < BaseMutation
visibility_role :introspect_admin
visibility_pundit_class PersonPolicy
argument :id, ID, required: true
argument :email, String, required: false
argument :home_phone_number, String, required: false
argument :mobile_phone_number, String, required: false
field :person, Types::Person::Person, null: true
def call(input = {})
current_user = context[:current_user]
authorize!(current_user)
person = Person.find(input[:id])
person.email = input[:email]
person.home_phone_number = input[:home_phone_number]
person.mobile_phone_number = input[:mobile_phone_number]
person.save!
{ error: nil, person: person }
end
# #param [User|nil] current_user
def authorize!(current_user)
raise Pundit::NotAuthorizedError, 'Not allowed to update person.' unless
PersonPolicy.new(current_user, nil).update?
end
end
end
end
Now I want to add validation to home_phone_number and mobile_phone_number. I have written my tests to look like this:
context 'invalid number home phone number' do
let(:variables) do
{
'input' => {
'id' => person.id,
'homePhoneNumber' => '123'
}
}
end
it 'should return an error if home phone is invalid' do
expect(subject).not_to contain_graphql_errors
expect(data_dig('error')).not_to be_nil
expect(data_dig('error', 'error')).to eq('standard_error')
expect(data_dig('error', 'description')).to eq('Home phone number must be valid')
end
end
context 'invalid number mobile phone number' do
let(:variables) do
{
'input' => {
'id' => person.id,
'mobilePhoneNumber' => '123'
}
}
end
it 'should return an error if mobile phone is invalid' do
expect(subject).not_to contain_graphql_errors
expect(data_dig('error')).not_to be_nil
expect(data_dig('error', 'error')).to eq('standard_error')
expect(data_dig('error', 'description')).to eq('Mobile phone number must be valid')
end
end
What I've tried
What I can get working is this, but not necessarily passing my tests:
def call(input = {})
current_user = context[:current_user]
authorize!(current_user)
validate_phone_numbers(input[:home_phone_number], input[:mobile_phone_number])
# ....
def validate_phone_numbers(home_phone_number, mobile_phone_number)
phone_numbers = [home_phone_number, mobile_phone_number]
phone_numbers.each do |contact|
raise StandardError, 'Phone Number must be valid' if !PhonyRails.plausible_number?(contact) #would this stop execution too?
end
end
As you can see, in doing this, I wouldn't be able to specify which is a home phone number vs mobile phone number.
I've also tried doing this one-by-one:
def validate_phone_numbers(home_phone_number, mobile_phone_number)
home_phone_number_valid = PhonyRails.plausible_number?(home_phone_number)
mobile_phone_number_valid = PhonyRails.plausible_number?(mobile_phone_number)
raise StandardError, 'Home phone number must be valid' if !home_phone_number_valid
raise StandardError, 'Mobile phone number must be valid' if !mobile_phone_number_valid
# Stop execution
return if !home_phone_number_valid || !mobile_phone_number_valid
end
The lines above also do not exactly work.
Some guidance would be immensely appreciated. Thank you!
Instead of returning { error: nil, person: person } in your resolver, try returning { error: errors_array, person: person } where errors_array contains any validation error messages from phony.
eg.
def call
...
errors_array = []
errors_array << 'Home Phone Number must be valid' if !PhonyRails.plausible_number?(input[:home_phone_number])
errors_array << 'Mobile Phone Number must be valid' if !PhonyRails.plausible_number?(input[:mobile_phone_number])
...
{ error: errors_array, person: person }
end
Also make sure cover the case where both the mobile and home phone numbers are invalid in your specs.
I hope this helps!

Checking The WHERE CLAUSE clause in rspec testing

I have a named scope in rails and i have model by name Product
class Product < ApplicationRecord
scope :old_products, -> { where("tagged_with = ?","old") }
end
Have any body encountered the process of checking the subject which is using where in the active record and that can check the what where clause does the named scope actually holds
In rspec spec/models/product_spec.rb
describe Product do
describe "checking scope clauses" do
subject { Product.old_products }
its(:where_clauses) { should eq([
"tagged_with = 'old'"
]) }
end
end
end
By the way i use rspec-2.89 version with the rails-5 version so any chances that we can check and verify the where clauses
I personally don't think checking returned SQL of a scope is sufficient. The way I would test old_products is:
describe Product do
describe "scopes" do
describe "old_products" do
let!(:old_product) {Product.create(tagged_with: 'old')}
let!(:not_old_product) {Product.create(tagged_with: 'sth_else')}
subject { Product.old_products }
it "returns the product(s) with tagged_with = old" do
expect(subject).to eq([old_product])
end
end
end
end
If you still want to check the return query, may want to try:
it "..." do
expect(subject.to_sql).to eq("SELECT \"products\".* FROM \"products\" WHERE \"products\".\"tagged_with\" = 'old'")
end
# This is better, more readable syntax for scope declaration
class Product < ApplicationRecord
scope :old_products, -> { where(tagged_with: 'old') }
end
# Something like this would work
describe Product do
context 'scopes' do
# Set up something that will always be excluded from the scopes
let!(:product) { create :product }
let!(:scoped_list) { create :product, 3, tagged_with: tag }
shared_examples_for 'returns scoped records' do
# This should work with shoulda-matchers
# (https://github.com/thoughtbot/shoulda-matchers)
# Could also not use subject and do something like:
# expect(
# described_class.send(scope_name.to_sym)
# ).to contain_exactly(scoped_list)
# and declare let(:scope_name) in your describe blocks
it 'returns scoped products' do
should contain_exactly(scoped_list)
end
end
describe '.old_products' do
subject(:old_products) { described_class.old_products }
let(:tag) { 'old' }
it_behaves_like 'returns scoped records'
end
describe '.other_scope' do
subject(:other_scope) { described_class.other_scope }
let(:tag) { 'other_tag' }
it_behaves_like 'returns scoped records'
end
end
end
There is no value in testing the actual SQL -- it's generated by Rails; what you want to test is that your scope is returning the correct objects
I would use context instead of describe when declaring the scopes test block because you're not describing a class or instance method
Use single quotes instead of double quotes unless you are doing string interpolation -- it's more performant
If you're early in the project and each Product only has a single tag, I would also rename the tagged_with column to be tag
describe Product do
describe "checking scope clauses" do
subject { Product.old_products }
expect(subject.values[:where].instance_values['predicates'].to eq(["tagged_with = 'old'"])
end
end

Rspec, Factory girl and after_initialize

I'm trying to test this after_initialize callback which is for the item model (which has_many line_items):
after_initialize :build_default_items, unless: :line_items?
callback:
def build_default_items
LineOfBusiness.all.each do |lob|
line_items.new(line_of_business_id: lob.id)
end
end
My test looks like:
describe 'callbacks' do
let(:user) { create :user }
it 'should build default items' do
lob1 = LineOfBusiness.create(id:1, name: "Name1", eff_date: Date.today,exp_date: Date.tomorrow, create_user: user, update_user: user)
lob2 = LineOfBusiness.create(id:2, name: "Name2", eff_date: Date.today,exp_date: Date.tomorrow, create_user: user, update_user: user)
lob_count = LineOfBusiness.all.count # this is correct as 2
item = build :item
expect(item.line_items.count).to eq(lob_count)
end
end
Error message as follows:
expected: 2
got: 0
(compared using ==)
So its failing in the callback method, its seeing the LineOfBusiness.all as Nil
def build_default_items
LineOfBusiness.all.each do |lob| # <-- this is Nil so fails
line_items.new(line_of_business_id: lob.id)
end
end
Any ideas why its Nil in the callback method?
line_items.count will fire query to database, and as you are not saving line_items in after_initialize callback, spec will fail. Instead try using line_items.size.
expect(item.line_items.size).to eq(lob_count)

Testing association validations with RSpec and FactoryGirl

I'm currently trying to write an RSpec test for a validation method. This method is triggered when the record is updated, saved or created. Here is what I have so far:
product.rb (model)
class Product < ActiveRecord::Base
validate :single_product
# Detects if a product has more than one SKU when attempting to set the single product field as true
# The sku association needs to map an attribute block in order to count the number of records successfully
# The standard self.skus.count is performed using the record ID, which none of the SKUs currently have
#
# #return [boolean]
def single_product
if self.single && self.skus.map { |s| s.active }.count > 1
errors.add(:single, " product cannot be set if the product has more than one SKU.")
return false
end
end
end
products.rb (FactoryGirl test data)
FactoryGirl.define do
factory :product do
sequence(:name) { |n| "#{Faker::Lorem.word}#{Faker::Lorem.characters(8)}#{n}" }
meta_description { Faker::Lorem.characters(10) }
short_description { Faker::Lorem.characters(15) }
description { Faker::Lorem.characters(20) }
sku { Faker::Lorem.characters(5) }
sequence(:part_number) { |n| "GA#{n}" }
featured false
active false
sequence(:weighting) { |n| n }
single false
association :category
factory :product_skus do
after(:build) do |product, evaluator|
build_list(:sku, 3, product: product)
end
end
end
end
product_spec.rb (unit test)
require 'spec_helper'
describe Product do
describe "Setting a product as a single product" do
let!(:product) { build(:product_skus, single: true) }
context "when the product has more than one SKU" do
it "should raise an error" do
expect(product).to have(1).errors_on(:single)
end
end
end
end
As you can see from the singe_product method, I'm trying to trigger an error on the single attribute when the single attribute is set to true and the product has more than one associated SKU. However, when running the test the product has no associated SKUs and therefore fails the unit test shown above.
How do I build a record and generate associated SKUs which can be counted (e.g: product.skus.count) and validated before they are all created in FactoryGirl?
You could write this like
it 'should raise an error' do
product = build(:product_skus, single: true)
expect(product).not_to be_valid
end

Testing an expected order of an array in RSpec / Rails

In a RSpec spec file I have the following test
it 'should return 5 players with ratings closest to the current_users rating' do
matched_players = User.find(:all,
:select => ["*,(abs(rating - current_user.rating)) as player_rating"],
:order => "player_rating",
:limit => 5)
# test that matched_players array returns what it is suppose to
end
How would I complete this to test that matched_players is returning the correct users.
I think you should first introduce some test users to the test DB (using for example a Factory) and afterwards see that the test is returning the correct ones.
Also it would make more sense to have a method in your model that would return the matched users.
For example:
describe "Player matching" do
before(:each) do
#user1 = FactoryGirl.create(:user, :rating => 5)
...
#user7 = FactoryGirl.create(:user, :rating => 3)
end
it 'should return 5 players with ratings closest to the current_users rating' do
matched_players = User.matched_players
matched_players.should eql [#user1,#user3,#user4,#user5,#user6]
end
end
Your model shouldn't know about your current user (the controllers know about this concept)
You need to extract this as a method on the User class otherwise there's no point in testing it, i.e. why test logic that isn't even in your app code?
The function that gets the matched players doesn't need to know about the current user, or any user for that matter, just the rating.
To test it, create a bunch of User instances, call the method, and see that the result is a list of the correct user instances you expect.
models/user.rb
class User < ActiveRecord::Base
...
def self.matched_players(current_user_rating)
find(:all,
select: ["*,(abs(rating - #{current_user_rating)) as match_strength"],
order: "match_strength",
limit: 5)
end
...
end
spec/models/user_spec.rb
describe User do
...
describe "::matched_players" do
context "when there are at least 5 users" do
before do
10.times.each do |n|
instance_variable_set "#user#{n}", User.create(rating: n)
end
end
it "returns 5 users whose ratings are closest to the given rating, ordered by closeness" do
matched_players = described_class.matched_players(4.2)
matched_players.should == [#user4, #user5, #user3, #user6, #user2]
end
context "when multiple players have ratings close to the given rating and are equidistant" do
# we don't care how 'ties' are broken
it "returns 5 users whose ratings are closest to the given rating, ordered by closeness" do
matched_players = described_class.matched_players(4)
matched_players[0].should == #user4
matched_players[1,2].should =~ [#user5, #user3]
matched_players[3,4].should =~ [#user6, #user2]
end
end
end
context "when there are fewer than 5 players in total" do
...
end
...
end
...
end

Resources