Why Does A Unit Test For destroy_all fail with let? - ruby-on-rails

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

Related

Rails fixture associations

Struggling to get fixtures to associate. We are (finally!) writing tests for an existing app. We are using Minitest as the framework.
mail_queue_item.rb:
class MailQueueItem < ApplicationRecord
belongs_to :order
...
end
mail_queue_items.yml:
one:
run_at: 2015-01-04 10:22:19
mail_to: test#test.com
order: seven_days_ago
email_template: with_content
customer: basic_customer
status: waiting
orders.yml:
seven_days_ago:
tenant: basic_tenant
ecom_store: basic_store
ecom_order_id: 123-123456-123456
purchase_date: <%= 7.days.ago %>
set_to_shipped_at: <%= 6.days.ago %>
ecom_order_status: shipped
fulfillment_channel: XYZ
customer: basic_customer
In a test:
require 'test_helper'
class MailQueueItemDenormalizerTest < ActiveSupport::TestCase
fixtures :mail_queue_items, :customers, :email_templates, :orders
test 'should make hash' do
#mqi = mail_queue_items(:one)
puts #mqi.order_id.inspect
puts #mqi.order.inspect
order = orders(:seven_days_ago)
puts order.inspect
assert #mqi.order.ecom_order_status == 'shipped'
end
end
The output looks like this:
MailQueueItemDenormalizerTest
447558226
nil
#<Order id: 447558226, tenant_id: 926560165, customer_id: 604023446, ecom_order_id: "123-123456-123456", purchase_date: "2022-08-13 19:18:02.000000000 -0700", last_update_date: nil, ecom_order_status: "shipped", fulfillment_channel: "XYZ", ....>
test_should_make_hash ERROR (5.96s)
Minitest::UnexpectedError: NoMethodError: undefined method `ecom_order_status' for nil:NilClass
test/denormalizers/mail_queue_item_denormalizer_test.rb:26:in `block in <class:MailQueueItemDenormalizerTest>'
So even though the order_id on the mail_queue_item is correct (it matches the id from the object loaded from the fixture) the association does not work.
I have tried the suggestions in Nil Associations with Rails Fixtures... how to fix? of putting ids in everything and the result is the same.
Project is in Rails 6 (long project that started life in Rails 3.1).
The issue turned out to be that the fixtures were creating invalid objects. The objects were valid enough to get written to the database, but were not passing the Rails validations.
The resulting behavior is quite odd I think, but I don't know of a better way to do it.
I discovered this by adding:
puts "#mqi.order.valid? = #{#mqi.order.valid?}"
puts "#mqi.customer.valid? = #{#mqi.customer.valid?}"
puts "#mqi.email_template.valid? = #{#mqi.email_template.valid?}"
puts #mqi.email_template.errors.full_messages
code in there. Yes, it's disgusting.

RSpec not passing 'should save' test

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.

Factorygirl , I can't express the association of 1 on 1 (has_one) with rspec

I want to test #create in Decidingscontroller , and Deciding model has undertaking_id column in association with Undertaking model , and has asking_id column in association with Asking model.
So, My factories/decidings.rb is below.
factory :deciding do
after(:build) do |deciding|
deciding.asking ||=build(:asking, deciding: deciding)
deciding.undertaking ||=build(:undertaking, deciding: deciding)
end
end
and My spec/controllers/decidings_controller_spec.rb is below.
RSpec.describe DecidingsController, type: :controller do
describe '#create' do
before do
#deciding=build(:deciding)
end
context 'correct_user login' do
before do
login_user(#deciding.asking.user)
end
it 'creates with deciding +1' do
expect{post :create , undertaking_id: #deciding.undertaking_id , asking_id: #deciding.asking_id}.to change(Deciding , :count).by(1)
end
end
end
end
but #deciding in this case is below.
#<Deciding id: nil, asking_id: nil, undertaking_id: nil, created_at: nil, updated_at: nil>
so I can't create test because undertaking_id and asking_id is nil.
Why is undertaking_id and asking_id nil? Please help me...
Anyway , My factories/asking.rb is below.
FactoryGirl.define do
factory :asking do
association :user
sequence(:content){|i| "お願いします#{i}"}
end
end
The ids are nil because the records are not persisted. When you use build method, it does not save a record in the database. If a record is not saved, it cannot have ID. Use create method instead.
Try this:
factory :deciding do
after(:build) do |deciding|
deciding.asking ||= create(:asking)
deciding.undertaking ||= create(:undertaking)
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)

RSpec - model uniqueness testing issue

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.

Resources