I am writing RSpec tests to make sure my database associations work properly. I noticed that immediately after resetting the database, my tests always fail. They always expect an object but get nil instead. I also noticed that immediately after writing a new test, the new test will fail. If I run the test suite once again and the tests were correct to begin with, they all pass.
How do I make my tests pass immediately after a test database reset?
Here's a sample test file:
describe 'Player' do
let(:clan) {Clan.first || Clan.create(name: "Test Clan")}
let(:kingdom) {Kingdom.first || Kingdom.create}
let(:player) {Player.first || Player.create(username: "Foo", uuid: "9b15dea6-606e-47a4-a241-420251703c59", clan_id: 1, clan_role: "member", kingdom_id: 1, kingdom_role: "pleb")}
let(:ip_address) {IpAddress.first || IpAddress.create(ip: "55.55.555.55")}
let(:punishment) {Punishment.first || Punishment.create(offender_id: 1)}
let(:connection) {Connection.first || Connection.create(player_id: 1, ip_address_id: 1)}
let(:bank_account) {BankAccount.first || BankAccount.create(account_owner_id: 1)}
let(:sell_offer) {SellOffer.first || SellOffer.create(seller_bank_account_id: 1, item_id: 1)}
let(:buy_offer) {BuyOffer.first || BuyOffer.create(buyer_bank_account_id: 1, item_id: 1)}
let(:owned_item) {OwnedItem.first || OwnedItem.create(owner_bank_account_id: 1, item_id: 1)}
context 'associations' do
it 'has ip addresses' do
expect(player.ip_addresses.first).to eq(ip_address)
end
it 'has connections' do
expect(player.connections.first).to eq(connection)
end
it 'has punishments' do
expect(player.punishments.first).to eq(punishment)
end
it 'has a clan' do
expect(player.clan).to eq(clan)
end
it 'has a bank account' do
expect(player.bank_account).to eq(bank_account)
end
it 'has sell offers' do
expect(player.sell_offers.first).to eq(sell_offer)
end
it 'has buy offers' do
expect(player.buy_offers.first).to eq(buy_offer)
end
it 'has owned items' do
expect(player.owned_items.first).to eq(owned_item)
end
it 'has a kingdom' do
expect(player.kingdom).to eq(kingdom)
end
end
context 'class methods' do
# stuff
end
end
Related
Link to Repo
I'm new to testing Rails so this is probably a very small thing but I can't figure out what's wrong. So I have some models I'd like to test. Right now the tests are simple; testing the presence of attributes and saving if all validations are met.
One of my models Profile belongs_to my Users model and passes all these tests spec/models/profiles_spec.rb:
require 'rails_helper'
RSpec.describe Profile, type: :model do
context 'validation tests' do
it 'ensures user_id presence' do
profile = Profile.new(platform: 0, region: 0, tag: 'GamerTag', sr: 1600).save
expect(profile).to eq(false)
end
it 'ensures platform presence' do
profile = Profile.new(user_id: 1, region: 0, tag: 'GamerTag', sr: 1600).save
expect(profile).to eq(false)
end
it 'ensures region presence' do
profile = Profile.new(user_id: 1, platform: 0, tag: 'GamerTag', sr: 1600).save
expect(profile).to eq(false)
end
it 'ensures tag presence' do
profile = Profile.new(user_id: 1, platform: 0, region: 0, sr: 1600).save
expect(profile).to eq(false)
end
it 'ensures sr presence' do
profile = Profile.new(user_id: 1, platform: 0, region: 0, tag: 'GamerTag').save
expect(profile).to eq(false)
end
it 'should save successfully' do
profile = Profile.new(user_id: 1, platform: 0, region: 0, tag: 'GamerTag', sr: 1600).save
expect(profile).to eq(true)
end
end
end
app/models/profile.rb:
class Profile < ApplicationRecord
validates :platform, presence: true
validates :region, presence: true
validates :tag, presence: true
validates :sr, presence:true
belongs_to :user
enum platform: [:pc, :xbl, :psn]
enum region: [:us, :eu]
end
But then there are my other models, which "pass" all the attribute presence validation tests, which theres something wrong there because they still pass when I comment out their attribute validations and fail the 'should save successfully' test.
The most confusing part? When I run the rails console and manually test it returns the expected value (true), like with my Student model which belongs_to :profile.
So I really have no idea what's going on here. Any ideas please throw them out. If you all need any more information, let me know.
Indeed, it's an error of missing related records. Let's take coach spec, for example:
it 'should save successfully' do
coach = Coach.new(profile_id: 1, roles: ['tank']).save
expect(coach).to eq(true)
end
Here (in my experiments) there's no profile with id=1. In fact, there are no profiles at all. So this spec fails, as expected.
Buuuut, by the time we get to the profile spec:
it 'should save successfully' do
profile = Profile.new(user_id: 1, platform: 0, region: 0, tag: 'GamerTag', sr: 1600)
expect(profile).to eq(true)
end
user with id=1 does exist (likely because user spec was run before this one and successfully created a user record).
Lessons to learn:
Always clean/rollback database between tests (or otherwise make it pristine)
Always run tests in a randomized order. Spec order dependency can be very difficult to detect, as you can see in this thread.
First of all, you are writing tests in a really inefficient way. If you wan't to test validations, then you don't need to test the save method return value but the value of the valid? method AND the errors hash.
RSpec.describe Profile, type: :model do
context 'validation tests' do
it 'ensures user_id presence' do
profile = Profile.new(platform: 0, region: 0, tag: 'GamerTag', sr: 1600, user_id: nil) #you should be explicit with the user_id value being nil, tests should be explicit, it may seem unnecesary but it makes them easier to read
expect(profile).to be_invalid #or expect(profile).not_to be_valid
expect(profile.errors[:user_id]).to be_present #you could test the actual message too, not just the presence on any error
end
end
end
That test actually tests only validations and also ensures that there's an error on the user_id field.
With your actual test you cannot know what is actually preventing the object to be saved. It could be anything: another validation, a before_save callback returning false, an invalid value when inserting into the database, anything. It's also slower since it has to actually write the record on the database, testing valid? is done on memory which is a lot faster.
Id' recommend you to read about FactoryBot so you don't have to repeat Profile.new.... on each test.
If you still want to test the return value of save on the last test, you have to know WHY it's not being saved, you con use save! which raises an exception instead of returning false to debug your code, and you can also inspect profile.errors.full_messages to see if there are any errors that you didn't consider when setting up the test.
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.
So I have this model code:
def self.cleanup
Transaction.where("created_at < ?", 30.days.ago).destroy_all
end
and this rspec unit test:
describe 'self.cleanup' do
before(:each) do
#transaction = Transaction.create(seller:item.user, buyer:user, item:item, created_at:6.weeks.ago)
end
it 'destroys all transactions more than 30 days' do
Transaction.cleanup
expect(#transaction).not_to exist_in_database
end
end
with these factories:
FactoryGirl.define do
factory :transaction do
association :seller, factory: :user, username: 'IAMSeller'
association :buyer, factory: :user, username: 'IAmBuyer'
association :item
end
factory :old_transaction, parent: :transaction do
created_at 6.weeks.ago
end
end
using this rspec custom matcher:
RSpec::Matchers.define :exist_in_database do
match do |actual|
actual.class.exists?(actual.id)
end
end
When I change the spec to this:
describe 'self.cleanup' do
let(:old_transaction){FactoryGirl.create(:old_transaction)}
it 'destroys all transactions more than 30 days' do
Transaction.cleanup
expect(old_transaction).not_to exist_in_database
end
end
the test fails. I also tried manually creating a transaction and assigning it to :old_transaction with let() but that makes the test fail too.
Why is it that it only passes when I use an instance variable in the before(:each) block?
Thanks in advance!
EDIT: FAILED OUTPUT
1) Transaction self.cleanup destroys all transactions more than 30 days
Failure/Error: expect(old_transaction).not_to exist_in_database
expected #<Transaction id: 2, seller_id: 3, buyer_id: 4, item_id: 2, transaction_date: nil, created_at: "2014-02-26 10:06:30", updated_at: "2014-04-09 10:06:32", buyer_confirmed: false, seller_confirmed: false, cancelled: false> not to exist in database
# ./spec/models/transaction_spec.rb:40:in `block (3 levels) in <top (required)>'
let is lazy loaded. So in your failing spec this is the order of events:
Transaction.cleanup
old_transaction = FactoryGirl.create(:old_transaction)
expect(old_transaction).not_to exist_in_database
So the transaction is created after you attempt to clean up.
There are multiple options for you:
Don't use let for this
Unless you have other specs that you want to tell other devs:
I fully intend for all of these specs to reference what should be the exact same object
I personally feel, that you're better off inlining the transaction.
it do
transaction = FactoryGirl.create(:old_transaction)
Transaction.cleanup
expect(transaction).not_to exist_in_database
end
Use the change matcher
This is my personal choice as it clearly demonstrates the intended behavior:
it do
expect{
Transaction.cleanup
}.to change{ Transaction.exists?(old_transaction.id) }.to false
end
This works with let as the change block is run before AND after the expect block. So on the first pass the old_transaction is instantiated so it's id can be checked.
Use before or reference old_transaction before your cleanup
IMO this seems odd:
before do
old_transaction
end
it do
old_transaction # if you don't use the before
Transaction.clean
# ...
end
Use let!
The let! is not lazy loaded. Essentially, it's an alias for doing a normal let, then calling it in a before. I'm not a fan of this method (see The bang is for surprise for details why).
I think you've just accidentally typoed in a ":"
Try this spec:
describe 'self.cleanup' do
let(:old_transaction){FactoryGirl.create(:old_transaction)}
it 'destroys all transactions more than 30 days' do
Transaction.cleanup
expect(old_transaction).not_to exist_in_database
end
end
In debt management app I test the behavior, when user borrow money (create expense_debt) and then return them (create income_debt), app updates expense_debt.returned to true.
My debt_rspec.rb:
require 'rspec'
describe Debt do
user = FactoryGirl.create(:user)
let(:expense_debt) { FactoryGirl.build(:expense_debt, user: user) }
let(:income_debt) { FactoryGirl.build(:income_debt, user: user) }
subject { income_debt }
it 'update expense_debt.returned' do
expense_debt.save
income_debt.save
expect(expense_debt.returned).to be_true
end
end
This test fails, but in development everything works ok.
Then I've found that expense_debt and Debt.first has different values of returned. And if I rewrite test to:
it 'update expense_debt.returned' do
expense_debt.save
income_debt.save
expect(Debt.first.returned).to be_true
end
it passes.
I can't understand, why they are not the same.
# This is expense_debt
#<Debt id: 1, ..., returned: false, ...>
# And this is Debt.first
#<Debt id: 1, ..., returned: true, ...>
Can somebody explain this behavior of RSpec?
may be it is using the cache version. Try this
expect(expense_debt.reload.debt_returned).to be_true
I have something like a job-hunting website. There are Position models and each position has many Offers. When one offer is accepted, the rest are rejected and the position is closed. My tests for Offer pass as expected, however, the tests for Position fail because is_accepted remains nil...
test "accept_offer flips is_accepted bits" do
p = FactoryGirl.create(:position_with_offers, offers: 3)
assert_nil e.offers[0].is_accepted # pass
assert_nil e.offers[1].is_accepted # pass
assert_nil e.offers[2].is_accepted # pass
assert_equal false, p.is_closed # pass
p.accept_offer(e.offers[0])
assert_equal true, p.is_closed # pass
assert_equal true, e.offers[0].is_accepted # fail, == nil
assert_equal false, e.offers[1].is_accepted # fail, == nil
assert_equal false, e.offers[2].is_accepted # fail, == nil
end
...and...
class Position < ActiveRecord::Base
has_many :offers, inverse_of: :position
def accept_offer(_offer)
offers.each do |o|
if o.id == _offer.id
o.accept
else
o.reject
end
o.reload
end
update({ is_closed: true })
end
end
...and...
class Offer < ActiveRecord::Base
def accept
update_column(:is_accepted, true)
end
def reject
update_column(:is_accepted, false)
end
end
I've read that .reload should be called to refresh the model. I've put reload on everything with no success. I've tried variations of update_all, update_attribute, update_column but the offers are always nil. Interestingly, if I puts the values in position.accept_offer, the correct values are rendered.
What do I need to do to get this test to pass?
Reload the offers.
assert_equal true, e.offers[0].reload.is_accepted # fail, == nil
assert_equal false, e.offers[1].reload.is_accepted # fail, == nil
assert_equal false, e.offers[2].reload.is_accepted # fail, == nil