Git Product home page Git Product logo

ar-query-matchers's Introduction

AR Query Matchers

badge

These RSpec matchers allow guarding against N+1 queries by specifying exactly how many queries you expect each of your ActiveRecord models to perform.

They could also help reasoning about which database interactions are happening inside a block of code.

This pattern is a based on how Rails itself tests queries: https://github.com/rails/rails/blob/ac2bc00482c1cf47a57477edb6ab1426a3ba593c/activerecord/test/cases/test_case.rb#L104-L141

Currently, this gem only supports RSpec matchers, but the code is meant to be adapted to support other testing frameworks. If you'd like to pick that up, please have a look at: Gusto#13

Usage

Include it in your Gemfile:

group :test do
  gem 'ar-query-matchers', '~> 0.2.0', require: false
end

Start using it:

require 'ar_query_matchers'

RSpec.describe Employee do
  it 'creating an employee creates exactly one record' do
    expect { 
      Employee.create!(first_name: 'John', last_name: 'Doe') 
    }.to only_create_models('Employee' => '1')
  end
end

Matchers

This gem defines a few categories of matchers:

  • Create: Which models are created during a block
  • Load: Which models are fetched during a block
  • Update: Which models are updated during a block

Each matcher category includes 3 assertions, for example, for the Load category, you could use the following assertions:

  • only_load_models: Strict assertion, not other query is allowed.
  • not_load_models: No models are allowed to be loaded.
  • load_models: Inclusion, other models are allowed to be loaded if not specified in the assertion.

For example:

The following spec will pass only if there are exactly 4 SQL SELECTs that load User records (and 1 for Address, 1 for Payroll) and no other models perform any SELECT queries.

expect { some_code() }.to only_load_models(
  'User' => 4,
  'Address' => 1,
  'Payroll' => 1,
)

The following spec will pass only if there are no select queries.

expect { some_code() }.to not_load_models

The following spec will pass only if there are exactly 4 SQL SELECTs that load User records (and 1 for Address, 1 for Payroll).

expect { some_code() }.to load_models(
  'User' => 4,
  'Address' => 1,
  'Payroll' => 1,
)

This will show some helpful output on failure:

Expected to run queries to load models exactly {"Address"=>1, "Payroll"=>1, "User"=>4} queries, got {"Address"=>1, "Payroll"=>1, "User"=>5}
     Expectations that differed:
         User - expected: 4, got: 5 (+1)

     Source lines:
     User loaded from:
         4 call: /spec/controllers/employees_controller_spec.rb:128:in 'block (4 levels) in <top (required)>'
         1 call: /app/models/user.rb:299

High Level Design:

The RSpec matcher delegates to "query counters", asserts expectations and formats error messages to provide meaningful failures.
The matchers are pretty simple, and delegate instrumentation into specialized QueryCounter classes. The QueryCounters are different classes which instrument a ruby block by listening on all sql, parsing the queries and returning structured data describing the interactions.

  ┌────────────────────────────────────────────────────────────────────────────────────────┐
┌─┤expect { Employee.create!() }.to only_create_models('Employee' => 1)                    │
│ └────────────────────────────────────────────────────────────────────────────────────────┘
└▶┌────────────────────────────────────────────────────────────────────────────────────────┐
┌─┤Queries::CreateCounter.instrument { Employee.create!() } => QueryStats                  │
│ └────────────────────────────────────────────────────────────────────────────────────────┘
└▶┌────────────────────────────────────────────────────────────────────────────────────────┐
  │QueryCounter.new(CreateQueryFilter.new).instrument { Employee.create!() } => QueryStats │
  └────────────────────────────────────────────────────────────────────────────────────────┘                                                                

For more information, see:

  1. ArQueryMatchers::Queries::QueryCounter
  2. ArQueryMatchers::Queries::CreateCounter
  3. ArQueryMatchers::Queries::LoadCounter
  4. ArQueryMatchers::Queries::UpdateCounter

Known problems

  • The Rails 4 ActiveRecord::Base#pluck method doesn't issue a Load or Exists named query and therefore we don't capture the counts with this tool. This may be fixed in Rails 5/6.

ar-query-matchers's People

Contributors

glenoliff avatar iking96 avatar jackdanger avatar jeffcarbs avatar kellysutton avatar mzruya avatar t3ly avatar tubaxenor avatar

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.