Testing N+1 Query Detection Implementation in Bullet
This test suite validates the N+1 query detection functionality in the Bullet gem, focusing on association tracking and notification generation. The tests ensure proper handling of possible and impossible objects, association checking, and stacktrace management.
Test Coverage Overview
Implementation Analysis
Technical Details
Best Practices Demonstrated
flyerhzm/bullet
spec/bullet/detector/n_plus_one_query_spec.rb
# frozen_string_literal: true
require 'spec_helper'
using Bullet::Ext::Object
module Bullet
module Detector
describe NPlusOneQuery do
before(:all) do
@post = Post.first
@post2 = Post.last
end
context '.call_association' do
it 'should add call_object_associations' do
expect(NPlusOneQuery).to receive(:add_call_object_associations).with(@post, :associations)
NPlusOneQuery.call_association(@post, :associations)
end
end
context '.possible?' do
it 'should be true if possible_objects contain' do
NPlusOneQuery.add_possible_objects(@post)
expect(NPlusOneQuery.possible?(@post)).to eq true
end
end
context '.impossible?' do
it 'should be true if impossible_objects contain' do
NPlusOneQuery.add_impossible_object(@post)
expect(NPlusOneQuery.impossible?(@post)).to eq true
end
end
context '.association?' do
it 'should be true if object, associations pair is already existed' do
NPlusOneQuery.add_object_associations(@post, :association)
expect(NPlusOneQuery.association?(@post, :association)).to eq true
end
it 'should be false if object, association pair is not existed' do
NPlusOneQuery.add_object_associations(@post, :association1)
expect(NPlusOneQuery.association?(@post, :association2)).to eq false
end
end
context '.conditions_met?' do
it 'should be true if object is possible, not impossible and object, associations pair is not already existed' do
allow(NPlusOneQuery).to receive(:possible?).with(@post).and_return(true)
allow(NPlusOneQuery).to receive(:impossible?).with(@post).and_return(false)
allow(NPlusOneQuery).to receive(:association?).with(@post, :associations).and_return(false)
expect(NPlusOneQuery.conditions_met?(@post, :associations)).to eq true
end
it 'should be false if object is not possible, not impossible and object, associations pair is not already existed' do
allow(NPlusOneQuery).to receive(:possible?).with(@post).and_return(false)
allow(NPlusOneQuery).to receive(:impossible?).with(@post).and_return(false)
allow(NPlusOneQuery).to receive(:association?).with(@post, :associations).and_return(false)
expect(NPlusOneQuery.conditions_met?(@post, :associations)).to eq false
end
it 'should be false if object is possible, but impossible and object, associations pair is not already existed' do
allow(NPlusOneQuery).to receive(:possible?).with(@post).and_return(true)
allow(NPlusOneQuery).to receive(:impossible?).with(@post).and_return(true)
allow(NPlusOneQuery).to receive(:association?).with(@post, :associations).and_return(false)
expect(NPlusOneQuery.conditions_met?(@post, :associations)).to eq false
end
it 'should be false if object is possible, not impossible and object, associations pair is already existed' do
allow(NPlusOneQuery).to receive(:possible?).with(@post).and_return(true)
allow(NPlusOneQuery).to receive(:impossible?).with(@post).and_return(false)
allow(NPlusOneQuery).to receive(:association?).with(@post, :associations).and_return(true)
expect(NPlusOneQuery.conditions_met?(@post, :associations)).to eq false
end
end
context '.call_association' do
it 'should create notification if conditions met' do
expect(NPlusOneQuery).to receive(:conditions_met?).with(@post, :association).and_return(true)
expect(NPlusOneQuery).to receive(:caller_in_project).and_return(%w[caller])
expect(NPlusOneQuery).to receive(:create_notification).with(%w[caller], 'Post', :association)
NPlusOneQuery.call_association(@post, :association)
end
it 'should not create notification if conditions not met' do
expect(NPlusOneQuery).to receive(:conditions_met?).with(@post, :association).and_return(false)
expect(NPlusOneQuery).not_to receive(:caller_in_project!)
expect(NPlusOneQuery).not_to receive(:create_notification).with('Post', :association)
NPlusOneQuery.call_association(@post, :association)
end
context 'stacktrace_excludes' do
before { Bullet.stacktrace_excludes = [/def/] }
after { Bullet.stacktrace_excludes = nil }
it 'should not create notification when stacktrace contains paths that are in the exclude list' do
in_project = OpenStruct.new(absolute_path: File.join(Dir.pwd, 'abc', 'abc.rb'))
included_path = OpenStruct.new(absolute_path: '/ghi/ghi.rb')
excluded_path = OpenStruct.new(absolute_path: '/def/def.rb')
expect(NPlusOneQuery).to receive(:caller_locations).and_return([in_project, included_path, excluded_path])
expect(NPlusOneQuery).to_not receive(:create_notification)
NPlusOneQuery.call_association(@post, :association)
end
# just a sanity spec to make sure the following spec works correctly
it "should create notification when stacktrace contains methods that aren't in the exclude list" do
method = NPlusOneQuery.method(:excluded_stacktrace_path?).source_location
in_project = OpenStruct.new(absolute_path: File.join(Dir.pwd, 'abc', 'abc.rb'))
excluded_path = OpenStruct.new(absolute_path: method.first, lineno: method.last)
expect(NPlusOneQuery).to receive(:caller_locations).at_least(1).and_return([in_project, excluded_path])
expect(NPlusOneQuery).to receive(:conditions_met?).and_return(true)
expect(NPlusOneQuery).to receive(:create_notification)
NPlusOneQuery.call_association(@post, :association)
end
it 'should not create notification when stacktrace contains methods that are in the exclude list' do
method = NPlusOneQuery.method(:excluded_stacktrace_path?).source_location
Bullet.stacktrace_excludes = [method]
in_project = OpenStruct.new(absolute_path: File.join(Dir.pwd, 'abc', 'abc.rb'))
excluded_path = OpenStruct.new(absolute_path: method.first, lineno: method.last)
expect(NPlusOneQuery).to receive(:caller_locations).and_return([in_project, excluded_path])
expect(NPlusOneQuery).to_not receive(:create_notification)
NPlusOneQuery.call_association(@post, :association)
end
end
end
context '.add_possible_objects' do
it 'should add possible objects' do
NPlusOneQuery.add_possible_objects([@post, @post2])
expect(NPlusOneQuery.possible_objects).to be_include(@post.bullet_key)
expect(NPlusOneQuery.possible_objects).to be_include(@post2.bullet_key)
end
it 'should not raise error if object is nil' do
expect { NPlusOneQuery.add_possible_objects(nil) }
.not_to raise_error
end
end
context '.add_impossible_object' do
it 'should add impossible object' do
NPlusOneQuery.add_impossible_object(@post)
expect(NPlusOneQuery.impossible_objects).to be_include(@post.bullet_key)
end
end
end
end
end