Any links to documentation proving or disproving my thoughts here would be very appreciated; I can't seem to find any.
AFAIK, if you had a Rails application with a Product model, you could define a FactoryGirl factory as
FactoryGirl.define do
factory :product do
# stuffs
end
end
and then call your factory in tests with (RSpec example)
let(:product) { FactoryGirl.create(:product) }
but you may also call it with
let(:product) { FactoryGirl.create(Product) }
This is helpful if you're wanting to keep your model tests a bit more dynamic and free to change with RSpec's described_class helper.
My problem:
I've got a model that happens to be namespaced
class Namespace::MyModel < ActiveRecord::Base
# model stuffs
end
with a factory
FactoryGirl.define do
factory :my_model, class: Namespace::MyModel do
# factory stuffs
end
end
and when attempting to use RSpec's helpers...
RSpec.describe Namespace::MyModel do
let(:my_object) { FactoryGirl.create(described_class) }
# testing stuffs
end
FactoryGirl complains of a missing factory
Factory not registered: Namespace::MyModel
Am I missing this feature of FactoryGirl, without understanding its true purpose? Or is there another way I can define my factory to resolve correctly?
Why don't you try
RSpec.describe Namespace::MyModel do
let(:my_object) { FactoryGirl.create(:my_factory) }
# testing stuffs
end
FactoryGirl is usually used by factory name, but not class name, that is defines.
You can have a multiple factories, that define instances of the same class. The difference between them can be in fields values, for example.
Or you can dynamicly get factory name, from described_class name.
It is already answered at How do you find the namespace/module name programmatically in Ruby on Rails?
Related
I'm making a big change in my system, so I changed one of my main tables into a STI, and create subclasses to implement the specific behavior.
class MainProcess < ApplicationRecord
end
class ProcessA < MainProcess
end
class ProcessB < MainProcess
end
In the application code, if I run MainProcess.new(type: 'ProcessA') it will return a ProcessA as I want.
But in the Rspec tests when I run FactoryBot::create(:main_process, type: 'ProcessA') it is returning a MainProcess and breaking my tests.
My factor is something like this
FactoryBot.define do
factory :main_process do
foo { 'bar' }
end
factory :process_a, parent: :main_process, class: 'ProcessA' do
end
factory :process_b, parent: :main_process, class: 'ProcessB' do
end
end
Is there some way to make FactoryBot have the same behavior of normal program?
I found the solution
FactoryBot.define do
factory :main_process do
initialize_with do
klass = type.constantize
klass.new(attributes)
end
end
...
end
The answer was founded here http://indigolain.hatenablog.com/entry/defining-factory-for-sti-defined-model (in japanese)
Edit #1:
⚠⚠⚠ Important ⚠⚠⚠
As mentioned here initialize_with is part of a private FactoryBot API.
According to the documentation:
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
So avoid to use if you can. (although I didn't find any other way to achieve this result without use it)
Edit #2
Besides the warning in the gem documentation (described above), the GETTING_STARTED.md actually suggest you use it
If you want to use factory_bot to construct an object where some attributes are passed to initialize or if you want to do something other than simply calling new on your build class, you can override the default behavior by defining initialize_with on your factory
If you just modify your original code to specify the class as the class type instead of a string, it works:
FactoryBot.define do
factory :main_process do
foo { 'bar' }
end
factory :process_a, parent: :main_process, class: ProcessA do
end
factory :process_b, parent: :main_process, class: ProcessB do
end
end
Here's the relevant section of the FactoryBot documentation.
initialize_with is marked as part of FactoryBot's Private API and not recommended for external use.
I think you can use nested factories to accomplish this.
factory :process do
factory :type_a_process, class: Process::TypeA do
type {"Process::TypeA"}
end
factory :type_b_process, class: Process::TypeB do
type {"Process::TypeB"}
end
end
end
FactoryBot.create(:type_b_process)
This is better:
initialize_with { type.present? ? type.constantize.new : Invoice.new }
https://dev.to/epigene/simple-trick-to-make-factorybot-work-with-sti-j09
I would like to create factory using local variable.
Currently I have the following factory:
FactoryGirl.define do
factory :offer_item, class: BackOffice::OfferItem do
service
variant
end
end
My expectation is to create something like below
FactoryGirl.define do
variant = FactroyGirl.create(:variant)
factory :offer_item, class: BackOffice::OfferItem do
service
variant { variant }
after(:create) do |offer_item|
offer_item.service.variants << variant
end
end
end
but then I get:
/.rvm/gems/ruby-2.2.3/gems/factory_girl-4.7.0/lib/factory_girl/registry.rb:24:in `find': Factory not registered: variant (ArgumentError)
All models are nested inside BackOffice module. Generally I want the same object has association with two other objects. I think there is a some problem with scope in my factory.
Variant factory is inside other separated file.
The issue is you are trying to create a factory before FactoryGirl has finished loading all of the factory definitions. This is because you defined the variable at the scope of a factory definition. Even if this did work, you would likely end up sharing the same variant record between multiple offer_items that you create in your test because this code would only get executed once during initialization.
Since you defined an association to Variant, you probably don't need to create it as an extra step. We can leverage FactoryGirl's ability to create our associations for us and then copy the object into your #service in your after(:create) callback. Maybe it would look something like this (untested):
FactoryGirl.define do
factory :offer_item, class: BackOffice::OfferItem do
service
variant
after(:create) do |offer_item|
offer_item.service.variants << offer_item.variant
end
end
end
I'm performing the simplest test on the following class (inside model's folder):
class Offer
attr_accessor :title, :payout, :thumbnail
def initialize(title, payout, thumbnail)
#title = title
#payout = payout
#thumbnail = thumbnail
end
end
The thing is there's no 'offers' db table. The objects created out of this class are never saved in a database.
Then i perform the tests using rspec:
describe Offer do
it "has a valid factory" do
expect(FactoryGirl.create(:offer)).to be_valid
end
...
end
and FactoryGirl:
FactoryGirl.define do
factory :offer do
skip_create
title { Faker::Name.name }
payout { Faker::Number.number(2) }
thumbnail { Faker::Internet.url }
initialize_with { new(title, payout, thumbnail)}
end
end
And i get the following error:
> undefined method `valid?' for #<Offer:0x00000002b78958>
Because your Offer class is not inheriting from ActiveRecord::Base, you're not getting any of the stuff that comes along with it (such as validations). valid? is a method provided through ActiveRecord's modules, not by Ruby directly, so it won't be available on a basic Ruby class.
If all you care about is validations, then you can include the ActiveModel::Validations module in your class and it will give you valid? as well as validates_presence_of, etc.:
class Offer
include ActiveModel::Validations
...
end
You can also just include ActiveModel to get a couple other things such as ActiveRecord's naming and conversion benefits (as well as validation).
I've noticed a couple examples in Rspec and FactoryGirl where some people put the class name in quotes, and some don't.
Example rspec:
describe "User" do
...specs...
end
describe User do
...specs...
end
Example FactoryGirl
factory :high_school_account, class: "Account" do
Name "Test Account Name"
AccountTypeId 1
end
factory :high_school_account, class: Account do
Name "Test Account Name"
AccountTypeId 1
end
I thought I read somewhere it had to do when the class is loaded into the ruby environment, but I might be completely making that up.
Is there a difference between the quoted and non-quoted versions?
From the factory_girl source code:
module FactoryGirl
class Factory
# ...
def build_class
#build_class ||= if class_name.is_a? Class
class_name
else
class_name.to_s.camelize.constantize
end
end
end
end
So no, in this context there is no difference - both are accepted. A String is simply converted to the class. Even a symbol would work.
Almost every spec file I come accross I end up writing stuff like:
before :each do
#cimg = Factory.build :cimg_valid
#cimg.stub(:validate_img).and_return true
#cimg.stub(:validate_img_url).and_return true
#cimg.stub(:save_images).and_return true
#cimg.stub(:process_image).and_return true
#cimg.stub(:img).and_return true
end
I mean, the model I get from Factory.build is completely valid. But if I don't stub that stuff it saves things in the filesystem, and validates stuff I'm not testing...
What I mean, I think it would be cleaner to do something like this:
before :each do
#cimg = Factory.build :cimg_for_testing_tags
end
If stubbing within the Factory is even possible.
What is the proper way to stub the model?
#fkreusch's answer works great until you use the new RSpec expect() syntax (3.0+)
Putting this into rails_helper.rb works for me:
FactoryBot::SyntaxRunner.class_eval do
include RSpec::Mocks::ExampleMethods
end
In the OP's example, you can now do:
FactoryBot.define do
factory :cimg_for_testing_tags do
... # Factory attributes
after(:build) do |cimg|
allow(cimg).to receive(:validate_img) { true }
end
end
end
Credit: github.com/printercu, see: https://github.com/thoughtbot/factory_bot/issues/703#issuecomment-83960003
In recent versions of factory_girl you have an after_build callback, so I believe you could define your factory like this:
FactoryGirl.define do
factory :cimg_for_testing_tags do
... # Factory attributes
after_build do |cimg|
cimg.stub(:validate_img).and_return true
end
end
end
UPDATE
After factory_girl 3.3.0, the syntax has changed to following:
FactoryGirl.define do
factory :cimg_for_testing_tags do
... # Factory attributes
after(:build) do |cimg|
cimg.stub(:validate_img).and_return true
end
end
end
A factory should produce "real world" objects therefore it's a bad practice (and error prone) to change behaviour (i.e. stub) in a factory.
You can do
let(:user) instance_double(User, FactoryGirl.attributes_for(:user))
before do
allow(user).to receive(:something).and_return('something')
end
and if your before clause gets too big you may want to extract it to a separate method or create a mock child class that overrides methods you want to stub.
You might also consider using FactoryGirl#build_stubbed.