I'm converting my application over to use factories instead of fixtures with Factory_Girl_Rails. I have the following factory defined:
factory :requirement do
sequence(:reqTitle) {|t| "Test Requirement #{t}"}
ignore do
categoryName " "
categoryAbbr " "
end
reqText "This is a test general requirement for the purpose of, um, testing things"
status "Approved"
factory :reqWithCat do
category
end
factory :reqWithNamedCat do
category {create(:category, catName: categoryName, catAbbr: categoryAbbr)}
end
factory :reqFromUserRequirement do
user_requirement
end
end
Then, in the setup section, I run the following snippet:
(0..5).each do |x|
requirement = create(:reqWithCat)
requirement.ind_requirements {|ir| [create(:ind_requirements)]}
end
(0..5).each do |x|
create(:reqWithNamedCat, categoryName: "User Interface", categoryAbbr: "UI")
end
However, my tests are failing, apparently because records aren't being created (for instance, the index test on the requirements controller tells me that there 0 records returned when there should be 10). I run the tests in debug mode, and discover that every requirement created has the exact same id value. Also, each record has the same sequence value.
I believe the duplicate records are failing to save, which is why I'm getting a 0 return. However, I can't see what I've set up incorrectly. What am I missing here?
Once I fixed the user factories to create the related many properly, I then discovered that I had written this factory incorrectly based on the way I was defining scopes in my models. Once that was fixed, the "broken" test began working.
Related
I am trying to build an RSpec test spec for my model: Logo that will ensure that only a singular record can be saved to the database. When I utilize the .build method for the second call to build a Logo, my test fails because FactoryBot is able to build out a Logo.
However, if I use the .create method for the second Logo entry in FactoryBot I receive an error for the test because my model raises an error, as instructed, based upon my model's method for the :only_one_row method.
How can I make this work using RSpec and FactoryBot?
Here is the code I have tried, unsuccessfully:
# app/models/logo.rb
class Logo < ApplicationRecord
before_create :only_one_row
private
def only_one_row
raise "You can only have one logo file for this website application" if Logo.count > 0
end
end
# spec/factories/logos.rb
FactoryBot.define do
factory :logo do
image { File.open(File.join(Rails.root, 'spec', 'fixtures', 'example_image.jpg')) }
end
end
# spec/logo_spec.rb
require 'rails_helper'
RSpec.describe Logo, type: :model do
it 'can be created' do
example_logo = FactoryBot.create(:logo)
expect(example_logo).to be_valid
end
it 'can not have more than one record' do
# Ensure there are no logo records in the database before this test is run.
Logo.destroy_all
example_logo_one = FactoryBot.create(:logo)
# This is where the trouble lies...
# If I go with .create method I error with the raised error defined in my model file...
example_logo_two = FactoryBot.create(:logo)
# ... if I go with the .build method I receive an error as the .build method succeeds
# example_logo_two = FactoryBot.build(:logo)
expect(example_logo_two).to_not be_valid
end
end
Your validation here is implemented as a hook, not a validation, which is why the be_valid call will never fail. I want to note, there's no real issue here from a logical perspective -- a hard exception as a sanity check seems acceptable in this situation, since it shouldn't be something the app is trying to do. You could even re-write your test to test for it explicitly:
it 'can not have more than one record' do
# Ensure there are no logo records in the database before this test is run.
Logo.destroy_all
example_logo_one = FactoryBot.create(:logo)
expect { FactoryBot.create(:logo) }.to raise_error(RuntimeError)
end
But, if there's a possibility the app might try it and you want a better user experience, you can build this as a validation. The tricky part there is that the validation looks different for an unsaved Logo (we need to make sure there are no other saved Logos, period) versus an existing one (we just need to validate that we're the only one). We can make it one single check just by making sure that there are no Logos out there that aren't this one:
class Logo < ApplicationRecord
validate do |logo|
if Logo.first && Logo.first != logo
logo.errors.add(:base, "You can only have one logo file for this website application")
end
end
end
This validation will allow the first logo to save, but should immediately know that the second logo is invalid, passing your original spec.
When I utilize the .build method for the second call to build a Logo, my test fails because FactoryBot is able to build out a Logo.
That is correct, build does not save the object.
However, if I use the .create method for the second Logo entry in FactoryBot I receive an error for the test because my model raises an error, as instructed, based upon my model's method for the :only_one_row method.
Catch the exception with an expect block and the raise_error matcher.
context 'with one Logo already saved' do
let!(:logo) { create(:logo) }
it 'will not allow another' do
expect {
create(:logo)
}.to raise_error("You can only have one logo file for this website application")
end
end
Note this must hard code the exception message into the test. If the message changes, the test will fail. You could test for RuntimeError, but any RuntimeError would pass the test.
To avoid this, create a subclass of RuntimeError, raise that, and test for that specific exception.
class Logo < ApplicationRecord
...
def only_one_row
raise OnlyOneError if Logo.count > 0
end
class OnlyOneError < RuntimeError
MESSAGE = "You can only have one logo file for this website application".freeze
def initialize(msg = MESSAGE)
super
end
end
end
Then you can test for that exception.
expect {
create(:logo)
}.to raise_error(Logo::OnlyOneError)
Note that Logo.destroy_all should be unnecessary if you have your tests and test database set up correct. Each test example should start with a clean, empty database.
Two things here:
If your whole application only ever allows a single logo at all (and not, say, a single logo per company, per user or whatever), then I don't think there's a reason to put it in the database. Instead, simply put it in the filesystem and be done with it.
If there is a good reason to have it in the database despite my previous comment and you really want to make sure that there's only ever one logo, I would very much recommend to set this constraint on a database level. The two ways that come to mind is to revoke INSERT privileges for the relevant table or to define a trigger that prevents INSERT queries if the table already has a record.
This approach is critical because it's easily forgotten that 1) validations can be purposefully or accidentally circumvented (save(validate: false), update_column etc.) and 2) the database can be accessed by clients other than your app (such as another app, the database's own console tool etc.). If you want to ensure data integrity, you have to do such elemental things on a database level.
I have an RSpec test that uses factory_bot to create instances. The test passes except when the first method is used in a view.
This is the code being tested:
def order_confirm_email(id, items, order, address, coupon)
#user = User.find(id)
#items = items
#order = order
#address = address
if coupon == nil
#coupon = ''
else
#coupon = coupon.discount
end
mail(to: #user.email, subject: 'Order completed')
end
This is the test:
it 'sends an email upon checkout process completion' do
user = create(:user)
items = create(:base_item)
order = create(:base_item)
address = create(:address)
coupon = create(:coupon)
expect(orderConfirmationMail.subject).to eq('Order completed')
end
So far so good. But when one of the views attempts to access the first instance, as below:
<h1>Order id - <%= #order.first.id %></h1>
Then I receive the following error:
Failures:
1) UserMailer user_emails sends an email upon checkout process completion
Failure/Error: <!-- <h1>Order id - <%= #order.first.id %></h1> -->
ActionView::Template::Error:
undefined method `first' for 3:Fixnum
According to my understanding, the instances should be persisting since I use create instead of build. But apparently that is not happening. Changing the view is not an option except as a last resort. How do I resolve this?
UPDATE 1:
This is the order_items.rb Factory file:
FactoryBot.define do
factory :base_item, class: OrderItem do
item_name_en "Sample Item"
item_link "http://www.foo.com"
qty 5
available_qty 100
item_price 10
seller_name "foo"
status "foo"
foo_item_id "123456"
foo_id "654321"
end
factory :order_item1, parent: :base_item do
foo_item_id "123456"
foo_seller_id "654321"
association :order
end
factory :order_item2, parent: :base_item do
foo_item_id "123457"
foo_seller_id "654321"
end
factory :order_item3, parent: :base_item do
foo_item_id "123458"
foo_seller_id "654322"
end
end
UPDATE 2:
I am encountering a similar issue when writing another test. I receive the following error:
Failures:
1) UserMailer user_emails sends an email upon abandoned cart
Failure/Error: <% #items.each do |item| %>
ActionView::Template::Error:
undefined method `each' for 1:Fixnum
This seems to indicate that it is not an issue with the first method per se, but rather with the interpolated Ruby in the views throwing an error when I run RSpec. This generalizes the problem and hopefully makes it more easily solvable.
that is not an issue, as earlier in the spec I include this line that I know is working correctly because I use the same format in another test successfully
let(:orderConfirmationMail) { UserMailer.order_confirm_email(1,2,3,4,nil) }
There it is. If you included this line from the start, your question would've been answered in two seconds.
Considering the signature of order_confirm_email
def order_confirm_email(id, items, order, address, coupon)
Why do you think order is set to 3? Because you pass it this way!
Mystery solved.
sorry, I don't have any idea. I would give a look to your OrderItem model, also I don't get why you do #order.first because that is just 1 object. Could make sense to me #orders.first but not #order.first. When things don't make sense in rails it is hard to build applications. also order and item should be two different models, we use order_items for building joins between two different models
An order can have many or just one item, you decide the relationship.
with rspec you can debug and also you can test your factories in the rails c test environment. There you can check what is the result of create(:base_item) or of FactoryGirl.create(:base_item)
I believe that should be an object, so I don't understand why it is saying undefined method 'first' for 3:Fixnum
Maybe the you factory is not what we expect
but you can understand this by debugging and testing in the console your factory
In the Ruby application I have a class(Resque Job) that has method that affect the values of a different class when called with the id of the latter class.
class ResqueKlass
def self.perform(id)
obj = EditKlass.find(id)
obj.update(value: 0)
end
end
I want to use rspec to test that this value was indeed changed within method
describe 'Something' do
let(:obj){FactoryGirl.create(:editklass)}
scenario 'Change obj value' do
ResqueKlass.perform(obj.id)
expect(obj.value).to eq(0)
end
end
This test fails where it expect 0 it get the value that was set in the factory girl.
I have also tried not using factory girl create 'obj' with let but that still does not work. I have placed bindings in the ResqueKlass perform method and i can see that the value is being updated.
PS please bear in mind that i am new to Ruby and rspec. These are not the exact classes that i am working with, the reason for that is the actual classes contain some sensitive data.
That happens, because you do not reload that record and therefore your obj still shows the old version.
Try reloading the obj with obj.reload:
describe 'Something' do
let(:obj){FactoryGirl.create(:editklass)}
scenario 'Change obj value' do
ResqueKlass.perform(obj.id)
expect(obj.reload.value).to eq(0)
end
end
I am experiencing something weird when trying to test a model through rspec. I have the code
it "has might that varies by atmost 30" do
instance.init(100)
expect(instance.members.count).to eq(1)
instance.members.each do |member|
puts "#{member.unit.name}"
end
expect(instance.members.count).to eq(1)
end
Whenenver I run the above test, both expect methods pass, signaling that there is only a single "member" record associated with the instance, but when I print out the name of each member, it prints a name twice, saying that there is two different members?
EDIT: Also, the method works correctly on the development server, but only have this issue during tests?
EDIT: method in question
def init(might)
transaction do
self.group.delegations.each do |delegation|
unit = delegation.unit
amount = (might / unit.might) * delegation.fraction
amount = amount.round
unless amount < 1
self.members.create(unit: unit, amount: amount)
end
end
end
end
I'm having some problems creating an rspec test to my rails application.
Let say that I have a model called MyModel, with the following function, that obtains the instances of all the MyModels that have an empty text
self.searchEmtpy:
self.where(:text => nil)
end
I've defined the following test, that checks that, new MyModels with empty text, should be returned by the previous function. I use FactoryGirl for the model creation.
describe "get empty models" do
before do
#previousModels=MyModel.searchEmtpy
#newModel=Factory(:myModel, :text => nil)
end
it "new empty models should appear" do
currentModels=MyModel.searchEmtpy
(previousModels << #newModel).should eq(currentModels)
end
end
The test is quite simple, but its not working. I don't know why, but, for what I understand from the output, it seams that, on the "should" line, previousModels already contains the newModel on it, so the test fails (it contains #newModel 2 times.
I'm missing something obvious? Aren't the instructions inside "it" called in order?
To clarify, the following test does not fail, where it should:
describe "get empty models" do
before do
#previousModels=MyModel.searchEmtpy
#newModel=Factory(:myModel, :text => nil)
end
it "new empty models should appear" do
currentModels=MyModel.searchEmtpy
(previousModels).should eq(currentModels)
end
end
self.where(:text => nil)
Is an ActiveRecord::Relation - the query doesn't actually fire until you try to do something to it (like iterate over it, append to it etc.)
In this case that happens on the same line as your call to should, ie after the factory has created the instance.
One way to fix this would be to force the evaluation of the relation in your before block, for example call .all on it.