I'm trying to clean up some tests and get them to run a little faster, and I have bunch of FactoryBot generated items which are currently created and persisted to the database. Obviously this isn't good for performance, so I'm trying to change the bulk of them over to use build rather than create to avoid the DB bottleneck. My current problem has to do with getting the objects to have a working association. What I have currently:
class User < ApplicationRecord
has_many :transaction_items,
class_name: 'Transaction',
foreign_key: 'user_id'
end
class Transaction < ApplicationRecord
belongs_to :user, class_name: 'User'
end
And the tests use these classes like so:
#user = create(:user)
#transaction1 = create(:transaction, user: #user)
#transaction2 = create(:transaction, user: #user,)
I'm fine with the User object being created and saved to the DB, as I need it to be available for authentication purposes. I need to change the Transaction objects to be instantiated using build. If I make that change the Transaction objects are aware of their associated User object (#transaction1.user returns the User instance) but the inverse is not true, i.e. #user.transaction_items does not return the associated Transaction objects. It does return a Transaction::ActiveRecord_Associations_CollectionProxy object but it is an empty collection. This is causing a failure because the code under tests makes use of the #transaction_items method and it needs to have the associated objects in it.
I'm far from an expert in FactoryBot, and while there is a ton of great information out there on FactoryBot associations for some reason I've not been able to get the association to work properly. What do I need to do here to get this kind of more complex association working in my factories?
Using Rails 5.1, FactoryBot v5.0.2
That's easy
#user = create(:user)
def build_transaction(user)
user.transaction_items.build(attributes_for(:transaction, user: user))
end
#transaction1 = build_transaction(#user)
#transaction2 = build_transaction(#user)
Related
I am trying to create a rspec test for custom validation in a spree extension(like a gem)
I need to validate uniqueness of a variants
option values for a product (all Spree models)
Here is the basic structure of models(Although they are part of spree, a rails based e-commerce building):
class Product
has_many :variants
has_many :option_values, through: :variants #defined in the spree extension, not in actual spree core
has_many :product_option_types
has_many :option_types, through: :product_option_types
end
class Variant
belongs_to :product, touch: true
has_many :option_values_variants
has_many :option_values, through: option_values
end
class OptionType
has_many :option_values
has_many :product_option_types
has_many :products, through: :product_option_types
end
class OptionValue
belongs_to :option_type
has_many :option_value_variants
has_many :variants, through: :option_value_variants
end
So I have created a custom validation to check the uniqueness of a variants option values for a certain product. That is a product(lets say product1) can have many variants. And a variant with option values lets say (Red(Option_type: Color) and Circle(Option_type: Shape)) have to unique for this product
Anyway this is the custom validator
validate :uniqueness_of_option_values
def uniqueness_of_option_values
#The problem is in product.variants, When I use it the product.variants collection is returning be empty. And I don't get why.
product.variants.each do |v|
#This part inside the each block doesn't matter though for here.
variant_option_values = v.option_values.ids
this_option_values = option_values.collect(&:id)
matches_with_another_variant = (variant_option_values.length == this_option_values.length) && (variant_option_values - this_option_values).empty?
if !option_values.empty? && !(persisted? && v.id == id) && matches_with_another_variant
errors.add(:base, :already_created)
end
end
end
And finally here are the specs
require 'spec_helper'
describe Spree::Variant do
let(:product) { FactoryBot.create(:product) }
let(:variant1) { FactoryBot.create(:variant, product: product) }
describe "#option_values" do
context "on create" do
before do
#variant2 = FactoryBot.create(:variant, product: product, option_values: variant1.option_values)
end
it "should validate that option values are unique for every variant" do
#This is the main test. This should return false according to my uniqueness validation. But its not since in the custom uniqueness validation method product.variants returns empty and hence its not going inside the each block.
puts #variant2.valid?
expect(true).to be true #just so that the test will pass. Not actually what I want to put here
end
end
end
end
Anybody know whats wrong here. Thanks in advance
I have a guess at what's happening. I think a fix would be to change your validation with the following line:
product.variants.reload.each do |v|
What I think is happing is that when you call variant1 in your test, it is running the validation for variant1, which calls variants on the product object. This queries the database for related variants, and gets an empty result. However, since variant2 has the same actual product object, that product object will not re-query the database, and remembers (incorrectly) that its variants is an empty result.
Another change which might make your test run is to change your test as follows:
before do
#variant2 = FactoryBot.create(:variant, product_id: product.id, option_values: variant1.option_values)
end
It is subtle and I'd like to know if it works. This sets the product_id field on variant2, but does not set the product object for the association to be the actual same product object that variant1 has. (In practice this is more likely to happen in your actual code, that the product object is not shared between variant objects.)
Another thing for your correct solution (if all this is right) is to do the reload but put all your save code (and your update code) in a transaction. That way there won't be a race condition of two variants which would conflict, because in a transaction the first must complete the validation and save before the second one does its validation, so it will be sure to detect the other one which just saved.
Some suggested debugging techniques:
If possible, watch the log to see when queries are made. You might have caught that the second validation did not query for variants.
Check the object_id. You might have caught that the product objects were in fact the same object.
Also check new_record? to make sure that variant1 saved before you tested variant2. I think it does save, but it would have be nice to know you checked that.
I want to be able to create associations with ID only in each specific test, trying to avoid defining them in the factory.
I've been following Rails 4 Test Prescriptions
Avoid defining associations automatically in factory_girl definitions.
Set them test by test, as needed. You’ll wind up with more manageable
test data.
class Workspace < ActiveRecord::Base
has_many :projects
end
class Project < ActiveRecord::Base
belongs_to :workspace
end
This is what I want
test "" do
project_with_id = build_stubbed(:project)
workspace_with_id = build_stubbed(:workspace)
workspace_with_id.projects.push(project_with_id)
end
I am using build_stubbed to create valid ID's, which gives the following error:
*** RuntimeError Exception: stubbed models are not allowed to access the database - Project#save({:validate=>true})
So, reading factory girl's documentation I came up with working associations, but I don't want to define them in the factory, not even with traits.
FactoryGirl.define do
factory :project do
association :workspace, strategy: :build_stubbed
end
end
test "" do
project = build_stubbed(:project)
end
This works because I can call project.workspace, and both have a valid ID
How can I create valid associations (with ID), but without touching the database, only using Factory girl to create independent objects?
You could do something like this if you are using Rspec
let!(:user1) { FactoryGirl.create(:user) }
let!(:user_social_profile1) { FactoryGirl.create(:user_social_profile, user_id: user1.id) }
also in Rspec
let!(:user1) { FactoryGirl.create(:user) }
let!(:user_social_profile1) { FactoryGirl.create(:user_social_profile, user: user1) }
In minitest/test_unit I believe
user1 = FactoryGirl.create(:user)
user_social_profile1 = FactoryGirl.create(:user_social_profile, user_id: user1.id)
I am sorry I did not explain the issues associated with using build_stubbed with factory associations, this answer does a really good job at explaining that.
build_stubbed initializes a record and fakes persistence. So what you get is a record that answers true to persisted? and has an faked ID.
Its quite useful but the approach does not work when it comes associations as you would need to stub out large parts of ActiveRecord.
Instead you want to use create:
before do
#project = create(:project)
#workshop = create(:workshop, project: #project)
end
As far as I understand, you don't need an inverse to work, e.g. you'd be satisfied with workspace.projects being empty, wouldn't you?
This should work for you then:
workspace = build_stubbed(:workspace)
project = build_stubbed(:project, workspace: workspace)
If you need workspace.projects, you may use this:
project = build_stubbed(:project)
workspace = build_stubbed(:workspace, projects: [project])
project.workspace = workspace
So I've got a User model, a Building model, and a MaintenanceRequest model.
A user has_many :maintenance_requests, but belongs_to :building.
A maintenance requests belongs_to :building, and belongs_to: user
I'm trying to figure out how to send a new, then create a maintenance request.
What I'd like to do is:
#maintenance_request = current_user.building.maintenance_requests.build(permitted_mr_params)
=> #<MaintenanceRequest id: nil, user_id: 1, building_id: 1>
And have a new maintenance request with the user and building set to it's parent associations.
What I have to do:
#maintenance_request = current_user.maintenance_requests.build(permitted_mr_params)
#maintenance_request.building = current_user.building
It would be nice if I could get the maintenance request to set its building based of the user's building.
Obviously, I can work around this, but I'd really appreciate the syntactic sugar.
From the has_many doc
You can pass a second argument scope as a callable (i.e. proc or lambda) to retrieve a specific set of records or customize the generated query when you access the associated collection.
I.e
class User < ActiveRecord::Base
has_many :maintenance_requests, ->(user){building: user.building}, through: :users
end
Then your desired one line should "just work" current_user.building.maintenance_requests.build(permitted_mr_params)
Alternatively, if you are using cancancan you can add hash conditions in your ability file
can :create, MaintenanceRequest, user: #user.id, building: #user.building_id
In my opinion, I think the approach you propose is fine. It's one extra line of code, but doesn't really increase the complexity of your controller.
Another option is to merge the user_id and building_id, in your request params:
permitted_mr_params.merge(user_id: current_user.id, building_id: current_user.building_id)
#maintenance_request = MaintenanceRequest.create(permitted_mr_params)
Or, if you're not concerned about mass-assignment, set user_id and building_id as a hidden field in your form. I don't see a tremendous benefit, however, as you'll have to whitelist the params.
My approach would be to skip
maintenance_request belongs_to :building
since it already belongs to it through the user. Instead, you can define a method
class MaintenanceRequest
belongs_to :user
def building
user.building
end
#more class stuff
end
Also, in building class
class Building
has_many :users
has_many :maintenance_requests, through: :users
#more stuff
end
So you can completely omit explicit building association with maintenance_request
UPDATE
Since users can move across buildings, you can set automatic behavior with a callback. The job will be done like you do it, but in a more Railsey way
class MaintenanceRequest
#stuff
before_create {
building=user.building
}
end
So, when you create the maintenance_request for the user, the building will be set accordingly
I have two models:
class Customer < ActiveRecord::Base
has_many :contacts
end
class Contact < ActiveRecord::Base
belongs_to :customer
validates :customer, presence: true
end
Then, in my controller, I would expect to be able to create both in
"one" sweep:
#customer = Customer.new
#customer.contacts.build
#customer.save
This, fails (unfortunately translations are on, It translates to
something like: Contact: customer cannot be blank.)
#customer.errors.messages #=> :contacts=>["translation missing: en.activerecord.errors.models.customer.attributes.contacts.invalid"]}
When inspecting the models, indeed, #customer.contacts.first.customer
is nil. Which, somehow, makes sense, since the #customer has not
been saved, and thus has no id.
How can I build such associated models, then save/create them, so that:
No models are persisted if one is invalid,
the errors can be read out in one list, rather then combining the
error-messages from all the models,
and keep my code concise?
From rails api doc
If you are going to modify the association (rather than just read from it), then it is a good idea to set the :inverse_of option on the source association on the join model. This allows associated records to be built which will automatically create the appropriate join model records when they are saved. (See the ‘Association Join Models’ section above.)
So simply add :inverse_of to relationship declaration (has_many, belongs_to etc) will make active_record save models in the right order.
The first thing that came to my mind - just get rid of that validation.
Second thing that came to mind - save the customer first and them build the contact.
Third thing: use :inverse_of when you declare the relationship. Might help as well.
You can save newly created related models in a single database transaction but not with a single call to save method. Some ORMs (e.g. LINQToSQL and Entity Framework) can do it but ActiveRecord can't. Just use ActiveRecord::Base.transaction method to make sure that either both models are saved or none of them. More about ActiveRecord and transactions here http://api.rubyonrails.org/classes/ActiveRecord/Transactions/ClassMethods.html
I was hoping I could get feedback on major changes to how a model works in an app that is in production already.
In my case I have a model Record, that has_many PhoneNumbers.
Currently it is a typical has_many belongs_to association with a record having many PhoneNumbers.
Of course, I now have a feature of adding temporary, user generated records and these records will have PhoneNumbers too.
I 'could' just add the user_record_id to the PhoneNumber model, but wouldn't it be better for this to be a polymorphic association?
And if so, if you change how a model associates, how in the heck would I update the production database without breaking everything? >.<
Anyway, just looking for best practices in a situation like this.
Thanks!
There's two approaches that might help you with this.
One is to introduce an intermediate model which handles collections of phone numbers. This way your Record and UserRecord can both belong_to this collection model and from there phone numbers and other contact information can be associated. You end up with a relationship that looks like this:
class Record < ActiveRecord::Base
belongs_to :address_book
delegate :phone_numbers, :to => :address_book
end
class UserRecord < ActiveRecord::Base
belongs_to :address_book
delegate :phone_numbers, :to => :address_book
end
class AddressBook < ActiveRecord::Base
has_many :phone_numbers
end
This kind of re-working can be done with a migration and a bit of SQL to populate the columns in the address_books table based on what is already present in records.
The alternative is to make UserRecord an STI derived type of Record so you don't need to deal with two different tables when defining the associations.
class Record < ActiveRecord::Base
has_many :phone_numbers
end
class UserRecord < Record
end
Normally all you need to do is introduce a 'type' string column into your schema and you can use STI. If UserRecord entries are supposed to expire after a certain time, it is easy to scope their removal using something like:
UserRecord.destroy_all([ 'created_at<=?', 7.days.ago ])
Using the STI approach you will have to be careful to scope your selects so that you are retrieving only permanent or temporary records depending on what you're intending to do. As UserRecord is derived from Record you will find they get loaded as well during default loads such as:
#records = Record.find(:all)
If this causes a problem, you can always use Record as an abstract base class and make a derived PermanentRecord class to fix this:
class PermanentRecord < Record
end
Update during your migration using something like:
add_column :records, :type, :string
execute "UPDATE records SET type='PermanentRecord'"
Then you can use PermanentRecord in place of Record for all your existing code and it should not retrieve UserRecord entries inadvertently.
Maintenance page is your answer.
Generate migration which updates table structure and updates existing data. If you're against data updates in migrations - use rake task.
Disable web access (create maintenance page)
Deploy new code
Run pending migrations
Update data
Enable web access (remove maintenance page).