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.
Related
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
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
I'm trying to make some simple model test:
/app/models/album.rb:
class Album < ActiveRecord::Base
has_many :slides, dependent: :restrict_with_exception
validates :name, presence: true
end
/spec/model/album_spec.rb:
require 'spec_helper'
describe Album do
before do
#album = Album.new(name: 'Example Album')
end
describe "when album name is already taken" do
before do
another_album = #album.dup
another_album.save
end
it { should_not be_valid }
end
end
I was expecting it to fail first (as I have no validates :uniqueness and index on the name field) but it passed. So I changed:
it { should_not be_valid }
to
it { should be_valid }
To see what's going on and this is what I got:
1) Album when album name is already taken should be valid
Failure/Error: it { should be_valid }
expected #<Album id: nil, name: nil, created_at: nil, updated_at: nil> to be valid, but got errors: Name can't be blank
# ./spec/models/album_spec.rb:14:in `block (3 levels) in <top (required)>'
I would like to ask you what I did wrong.
One more thing is if I can/should use expect rather than should syntax here ? I read somewhere that should is a bit deprecated and not expect is recomended but I don't know how to use it for model testing (I have it on my Controller/View test in form of expect(page) or expect(current_path). What argument can I use for model ?
I have never seen the it syntax that you are using. First thing, I would checkout the quick start documentation available here: https://github.com/rspec/rspec-rails#model-specs and then make sure that you are familiar with this set of docs as well: http://rspec.info/
From the example on github:
require "spec_helper"
describe User do
it "orders by last name" do
lindeman = User.create!(first_name: "Andy", last_name: "Lindeman")
chelimsky = User.create!(first_name: "David", last_name: "Chelimsky")
expect(User.ordered_by_last_name).to eq([chelimsky, lindeman])
end
end
You would want to change your second describe to an it and then use one or more expect to determine if the test passes. it takes a string that appears in the test output. So generally you want to make it something expressive. Additionally there is no need to use before blocks here. You can do everything in the it block:
require 'spec_helper'
describe Album do
it "fails validation when album name is already taken" do
album = Album.new(name: 'Example Album')
another_album = album.dup
expect {another_album.save!}.to raise_error(ActiveRecord::RecordInvalid,'Validation failed: This question is no longer active.')
end
end
Setup an explicit subject before your example:
subject {#album}
it { should_not be_valid }
Currently, as per the failure error#<Album id: nil, name: nil, created_at: nil, updated_at: nil> an implicit blank instance of Album is created as no explicit subject is found before the example.
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 am new to rails. Newer to FactoryGirl.
I have a model like this.
class Manifest < ActiveRecord::Base
serialize :scopes, Array
:app_description
:app_name
:app_id
:app_verson
:dev_id
:callback
:manifest_ver
:signed_jwt
:scopes
validates :app_name, presence: true
validates :callback, presence: true
I have a factory like this.
factory(:manifest) do
callback "some callback"
app_name "cool birds"
end
The spec regarding above model is like this.
describe Manifest do
describe "validation" do
describe "of object from fractory" do
it "must be ok" do
FactoryGirl.build(:manifest).should be_valid
end
end
end
end
So I expected this test to pass. But it fails giving this output.
1) Manifest validation of object from fractory must be ok
Failure/Error: FactoryGirl.build(:manifest).should be_valid
expected #<Manifest id: nil, dev_id: nil, app_id: nil, app_description: nil, app_name: "cool birds", app_version: nil, manifest_ver: nil, callback: nil, signed_jwt: nil, scopes: [], created_at: nil, updated_at: nil> to be valid, but got errors: Callback can't be blank
It says Callback can't be blank.
It seems FactoryGirl ignores the value for attribute "callback".
when the spec changed to FactoryGirl.build(:manifest, callback: "some callback").should be_valid it works! The test passes.
FactoryGirl works for any other attribute in my model but "callback". Why? What wrong have I or this attribute named "callback" has done? What should I do to find out the problem.