Why yet another gem to assert DB queries?
Unlike other libraries (such as db-query-matchers, rspec-sqlimit, etc.), with n_plus_one_control
you don’t have to specify exact expectations to control your code behaviour (e.g. expect { subject }.to query(2).times
).
Such expectations are rather hard to maintain, because there is a big chance of adding more queries, not related to the system under test.
n_plus_one_control
works differently. It evaluates the code under consideration several times with different scale factors to make sure that the number of DB queries behaves as expected (i.e. O(1) instead of O(N)).
So, it’s for performance testing and not feature testing.
Why not just use bullet
?
Of course, it’s possible to use Bullet in tests (see more here), but it’s not a silver bullet: there can be both false positives and true negatives.
This gem was born after I’ve found myself not able to verify with a test yet another N+1 problem.
Usage
RSpec
First, add NPlusOneControl to your spec_helper.rb
:
# spec_helper.rb
require "n_plus_one_control/rspec"
Then:
# Wrap example into a context with :n_plus_one tag
context "N+1", :n_plus_one do
# Define `populate` callbacks which is responsible for data
# generation (and whatever else).
#
# It accepts one argument, the scale factor (read below)
populate { |n| create_list(:post, n) }
specify do
expect { get :index }.to perform_constant_number_of_queries
end
end
Available modifiers:
# You can specify the RegExp to filter queries.
# By default, it only considers SELECT queries.
expect { get :index }.to perform_constant_number_of_queries.matching(/INSERT/)
# You can also provide custom scale factors
expect { get :index }.to perform_constant_number_of_queries.with_scale_factors(10, 100)
Using scale factor in spec
Let’s suppose your action accepts a parameter which can make an impact on the number of returned records:
get :index, params: {per_page: 10}
Then it is enough to just change per_page
parameter between executions and do not recreate records in DB. For this purpose, you can use current_scale
method in your example:
context "N+1", :n_plus_one do
before { create_list :post, 3 }
specify do
expect { get :index, params: {per_page: current_scale} }.to perform_constant_number_of_queries
end
end
Expectations in execution block
Both RSpec matchers allows you to put additional expectations inside execution block to ensure that tested piece of code actually does what expected.
context "N+1", :n_plus_one do
specify do
expect do
expect(my_query).to eq(actuall_results)
end.to perform_constant_number_of_queries
end
end