简体   繁体   中英

Maintianing ActiveRecord associations created using FactoryBot in controller test

I'm attempting to speed up some tests for a Rails controller and a bottleneck has to do with large numbers of objects being created and persisted to the database. I'm attempting to replace most of those create calls with build calls to address this.

Running Rails 5.1 , and using MiniTest 5.10.3 , with FactoryBot 5.0.2 .

I'm attempting to go from this

@user = create(:user)
@item1 = create(:item)
@item1 = create(:item)

@transaction1 = create(:transaction, buyer: @buyer, item: @item1)
@transaction2 = create(:transaction, buyer: @buyer, item: @item2)

In this application item represents a sellable object, user represents a purchaser and transaction is the object that creates the two. The User class also has an association added to it transaction_checkout_items which returns all Transaction items which are in a state where the purchase can be completed.

So, with each test we're creating a myriad of objects and saving them all to the database. It's slow but it works. Still, I want it to be faster, so I've tried replacing the existing setup with something like this:

@user = create(:user)
def build_transaction_checkout_items(user, item)
  user.transaction_checkout_items.build(attributes_for(:transaction, 
                                                        buyer: user, 
                                                        sale_price: item.sale_price, 
                                                        item: item))
end

@item1 = build_stubbed(:item)
@item2 = build_stubbed(:item)
@transaction1 = build_transaction_checkout_items(@buyer, @item1)
@transaction2 = build_transaction_checkout_items(@buyer, @item2)

This seems to work as long as I'm in the test. If I drop a binding in my test and check the objects @user returns the user object, @user.transaction_checkout_items returns a Transaction::ActiveRecord_Associations_CollectionProxy object containing all my associated transactions, and the individual transactions have their associated items attached. However, If I put a binding.pry into the controller method which actually does the work, and look at the User object I see the correct one, but user.transaction_checkout_items now returns an empty Transaction::ActiveRecord_Associations_CollectionProxy object with nothing in it. Essentially the associations vanish, and this makes sense to me as the controller is pulling the User object from the database and going to work on it, and this new object is missing the associations. I've considered trying to stub out an any_instance method on the User class so that whenever #transaction_checkout_items is called it returns the collection of Transaction objects but I don't see any way to create a new ::ActiveRecord_Associations_CollectionProxy object. I can't simply use an array or other collection for this as there are methods on the ::ActiveRecord_Associations_CollectionProxy that need to be called for the controller logic to work.

So here I am on a Friday blocked. Is my idea of stubbing transaction_checkout_items on any User instance a good one, and if so how do I do it? Or is there an alternate strategy anyone can suggest that will allow the MiniTest stubbed associations to persist and be available when the controller code runs?

Is my idea of stubbing transaction_checkout_items on any User instance a good one, and if so how do I do it?

Stubbing out ActiveRecord methods is almost always a bad idea. It will couple your tests heavily to the implementation and potentially will make it difficult to update Rails / ActiveRecord as if anything changes in the framework your tests start breaking. There might be also lots of funny side effects you haven't thought about.

The question is also what do you actually want to test? If you start stubbing out these methods, what are you testing? In a controller / integration test, I would expect you want to test to fetch the correct records from the database.

Using build vs. create to improve test performance is a good trick but, as you already discovered, is only valuable if you use the same objects in the test. This is unfortunately not possible for integration tests and you need / should persists the records.

Or is there an alternate strategy anyone can suggest that will allow the MiniTest stubbed associations to persist and be available when the controller code runs?

I would think about why this is slow and if this is actually really a problem. How long does your test & whole test suite run or is this just a premature optimisation?

If the reason it's slow is solely because you need to create a lot of test data you could use fixtures or seed your database instead. This would both be faster than using FactoryBot although brings different issues (eg MysteryGuest )

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM