Skip to content

Commit

Permalink
Added QueryTheDatabaseMatcher
Browse files Browse the repository at this point in the history
This matcher allows you to spec the number of database calls made by a
method.
  • Loading branch information
MDaubs authored and Gabe Berke-Williams committed Mar 23, 2012
1 parent 0746b79 commit 00a9a68
Show file tree
Hide file tree
Showing 3 changed files with 143 additions and 0 deletions.
1 change: 1 addition & 0 deletions lib/shoulda/matchers/active_record.rb
Expand Up @@ -3,6 +3,7 @@
require 'shoulda/matchers/active_record/have_db_index_matcher'
require 'shoulda/matchers/active_record/have_readonly_attribute_matcher'
require 'shoulda/matchers/active_record/serialize_matcher'
require 'shoulda/matchers/active_record/query_the_database_matcher'

module Shoulda
module Matchers
Expand Down
96 changes: 96 additions & 0 deletions lib/shoulda/matchers/active_record/query_the_database_matcher.rb
@@ -0,0 +1,96 @@
module Shoulda # :nodoc:
module Matchers
module ActiveRecord # :nodoc:

# Ensures that the number of database queries is known. Rails 3.1 or greater is required.
#
# Options:
# * <tt>when_calling</tt> - Required, the name of the method to examine.
# * <tt>with</tt> - Used in conjunction with <tt>when_calling</tt> to pass parameters to the method to examine.
# * <tt>or_less</tt> - Pass if the database is queried no more than the number of times specified, as opposed to exactly that number of times.
#
# Examples:
# it { should query_the_database(4.times).when_calling(:complicated_counting_method)
# it { should query_the_database(4.times).or_less.when_calling(:generate_big_report)
# it { should_not query_the_database.when_calling(:cached_count)
#
def query_the_database(times = nil)
QueryTheDatabaseMatcher.new(times)
end

class QueryTheDatabaseMatcher # :nodoc:

def initialize(times)
if times.respond_to?(:count)
@expected_query_count = times.count
else
@expected_query_count = times
end
end

def when_calling(method_name)
@method_name = method_name
self
end

def with(*method_arguments)
@method_arguments = method_arguments
self
end

def or_less
@expected_count_is_maximum = true
self
end

def matches?(subject)
@queries = []

subscriber = ActiveSupport::Notifications.subscribe('sql.active_record') do |*args|
@queries << args[4] unless filter_query(args[4][:name])
end

subject.send(@method_name, *@method_arguments)

ActiveSupport::Notifications.unsubscribe(subscriber)

if @expected_count_is_maximum
@queries.length <= @expected_query_count
elsif @expected_query_count.present?
@queries.length == @expected_query_count
else
@queries.length > 0
end
end

def failure_message
if @expected_query_count
"Expected ##{@method_name} to cause #{@expected_query_count} database queries but it actually caused #{@queries.length} queries:" + friendly_queries
else
"Expected ##{@method_name} to not query the database but it actually caused #{@queries.length} queries:" + friendly_queries
end
end

alias_method :negative_failure_message, :failure_message

private

def friendly_queries
@queries.collect do |query|
"\n (#{query[:name]}) #{query[:sql]}"
end.join
end

def filter_query(query_name)
raise "Rails 3.1 or greater is required" unless rails_3_1?
query_name == 'SCHEMA'
end

def rails_3_1?
::ActiveRecord::VERSION::MAJOR == 3 && ::ActiveRecord::VERSION::MINOR >= 1
end

end
end
end
end
46 changes: 46 additions & 0 deletions spec/shoulda/active_record/query_the_database_matcher_spec.rb
@@ -0,0 +1,46 @@
require 'spec_helper'

describe Shoulda::Matchers::ActiveRecord::QueryTheDatabaseMatcher do

if ::ActiveRecord::VERSION::MAJOR == 3 && ::ActiveRecord::VERSION::MINOR >= 1

before do
@parent = define_model :litter do
has_many :kittens
end
@child = define_model :kitten, :litter_id => :integer do
belongs_to :litter
end
end

it "should accept the correct number of queries when there is a single query" do
@parent.should query_the_database(1.times).when_calling(:count)
end

it "should accept the correct number of queries when there are two queries" do
nonsense = lambda do
@parent.create.kittens.create
end
nonsense.should query_the_database(2.times).when_calling(:call)
end

it "should reject the wrong number of queries" do
@parent.should_not query_the_database(10.times).when_calling(:count)
end

it "should accept fewer than the required count" do
@parent.should query_the_database(5.times).or_less.when_calling(:count)
end

it "should pass arguments to the method to examine" do
model = Class.new do
def self.count(arguments)
arguments.should == "arguments"
end
end
model.should_not query_the_database.when_calling(:count).with("arguments")
end

end

end

0 comments on commit 00a9a68

Please sign in to comment.