Monday, March 17, 2014

Using Bullet with MiniTest to Identify N+1 Queries

The Bullet gem can be helpful when used on an on-demand basis to identify some n+1 query problems in code called by your integration tests. Although Tommy noticed that Bullet gives a false notification (see #148 for status) when using empty? on a relation that would trigger a query unless there is an includes, it identified some n+1 queries effectively.

In your test group in Gemfile:

# this example is just for testing,
# but it might be helpful in development as well
group :test do
  # ...
  gem 'bullet', '~> 4.8.0'
end

Then at command-line:

bundle install

In test_helper, we require a file that contains something similar to the following:

if ENV['BULLET']
  Bullet.enable = true
  # it is set this way by default, but being explicit
  Bullet.raise = false

  require 'minitest/unit'
  module MiniTestUsesBullet
    def before_setup
      Bullet.start_request
      super if defined?(super)
    end

    def after_teardown
      super if defined?(super)
      Bullet.end_request
      if passed?
        # format however you want
        raise "Bullet failed: #{Bullet.warnings.inspect}" if Bullet.warnings.present?
      end
    end
  end

  class MiniTest::Unit::TestCase
    include MiniTestUsesBullet
  end
end

Then prefix BULLET=1 in command line when running a test or all tests. Bullet docs note similar, but they use rspec and they leave it kind of up-in-the-air of what to use. We originally tried Bullet.raise=true, but Tommy noticed that hides valid ActiveRecord errors because the Bullet (4.8.0 at least) raises at the end of the Rack request handling. So, instead you need to only fail if the test passed and if there are warnings. As Tommy noted, since it is Rack based, you might only add this to your integration tests (not controller tests, etc.) vs. MiniTest::Unit::TestCase, and he said this works for better format:

raise warnings.values.flatten.map(&:full_notice).join('\n') unless warnings.values.empty?

No comments: