Two ways for testing preloading/eager-loading of ActiveRecord associations in Rails

As a developer who cares about performance you know to avoid N+1 queries by using #includes , #preload or #eager_load methods . But is there a way of checking out that you are doing your job correctly and making sure that the associations you expect to be preloaded are indeed? How can you test it? There are two ways.

Imagine we have two of these classes in our Rails application. An order can have many order_lines .

class Order < ActiveRecord :: Base has_many :order_lines def self . last_ten limit ( 10 ). preload ( :order_lines ) end end

class OrderLine < ActiveRecord :: Base belongs_to :order end

We implemented Order.last_ten method which returns last 10 orders with one eager loaded association. Let’s see how can make sure that the lines are preloaded after calling it.

association(:name).loaded?

require 'test_helper' class OrderTest < ActiveSupport :: TestCase test "#last_ten eager loading" do o = Order . new () o . order_lines . build o . order_lines . build o . save! orders = Order . last_ten assert orders [ 0 ]. association ( :order_lines ). loaded? end end

Because we preload(:order_lines) we are interested whether order_lines is loaded. To check that we need to get one Order object such as orders[0] verify on it. There is nothing to check on orders collection that could tell us if the association is loaded or not.

The test in Rspec would look quite similar

require 'rails_helper' RSpec . describe Order , type: :model do specify "#last_ten eager loading" do o = Order . new () o . order_lines . build o . order_lines . build o . save! orders = Order . last_ten expect ( orders [ 0 ]. association ( :order_lines ). loaded? ). to eq ( true ) # or alternatively expect ( orders [ 0 ]. association ( :order_lines )). to be_loaded end end

count queries with ActiveSupport::Notifications

ActiveRecord library has a nice helper method called assert_queries which is part of ActiveRecord::TestCase . Unfortunately, ActiveRecord::TestCase is not shipped as part of ActiveRecord. It is only available in rails internal tests to verify its behavior. We can however quite easily emulate it for our needs.

Imagine a scenario in which you operate on a graph of Active Record objects but you don’t return them. You just return a computed values. How can your verify it in such case that you don’t have the N+1 problem? There are no observable side-effects, no returned records to check if they are loaded? . But… aren’t they really?

class Order < ActiveRecord :: Base has_many :order_lines def self . average_line_gross_price_today lines = where ( "created_at > ?" , Time . current . beginning_of_day ). preload ( :order_lines ). flat_map do | order | order . order_lines . map ( & :gross_price ) end lines . sum / lines . size end end class OrderLine < ActiveRecord :: Base belongs_to :order def gross_price # ... end end

In this situation. How can you test that Order.average_line_gross_price_today does not suffer from N+1 queries? Is there a way to make sure order.order_lines.map(&:gross_price) is not triggering a SQL query when reading order_lines ? It turns out there is.

We can use ActiveSupport::Notifications and get notified about every executed SQL statement.

require 'rails_helper' RSpec . describe Order , type: :model do specify "#average_line_gross_price_today eager loading" do o = Order . new () o . order_lines . build o . order_lines . build o . save! count = count_queries { Order . average_line_gross_price_today } expect ( count ). to eq ( 2 ) end private def count_queries & block count = 0 counter_f = -> ( name , started , finished , unique_id , payload ) { unless %w[ CACHE SCHEMA ] . include? ( payload [ :name ]) count += 1 end } ActiveSupport :: Notifications . subscribed ( counter_f , "sql.active_record" , & block ) count end end

If you go that way make sure to create enough records to detect potential issues with eager loading. One order with one line is not enough because with and without the eager loading the number of queries would be the same. In this case only when you have 2 order lines you can see the difference in a number of queries with preloading (2, one for all orders and one for all lines) vs without preloading (3, one for all orders and one for every line separately). Always make sure your test is failing before fixing it :)

While using this approach is possible, it tells me that it could be nice to split the responsibilities into two smaller methods. One responsible for extracting the right records from a database (IO-related) and one for transforming the data and doing the computations (no IO, side-effect free).

You can check out db-query-matchers gem for RSpec matchers to help you with that kind of testing.

Would you like to continue learning more?

If you enjoyed the article, subscribe to our newsletter so that you are always the first one to get the knowledge that you might find useful in your everyday Rails programmer job. Content is mostly focused on (but not limited to) Ruby, Rails, Web-development and refactoring.