2011-03-30 87 views
39

我想测试一段代码执行尽可能少的SQL查询。计算执行的查询的数量

ActiveRecord::TestCase似乎有自己的assert_queries方法,这将做到这一点。但由于我没有修补ActiveRecord,所以对我来说没什么用处。

RSpec或ActiveRecord是否提供任何官方公开方法来计算在代码块中执行的SQL查询的数量?

回答

44

我觉得你提的assert_queries回答了自己的问题,但在这里有云:

我建议考虑看看后面assert_queries代码并用它来建立你自己的方法,你可以用它来计算查询。这里涉及的主要法宝是这一行:

ActiveSupport::Notifications.subscribe('sql.active_record', SQLCounter.new) 

我有点小炉匠今天上午和拆出来的ActiveRecord的那些查询计数的零件和与此想出了:

module ActiveRecord 
    class QueryCounter 
    cattr_accessor :query_count do 
     0 
    end 

    IGNORED_SQL = [/^PRAGMA (?!(table_info))/, /^SELECT currval/, /^SELECT CAST/, /^SELECT @@IDENTITY/, /^SELECT @@ROWCOUNT/, /^SAVEPOINT/, /^ROLLBACK TO SAVEPOINT/, /^RELEASE SAVEPOINT/, /^SHOW max_identifier_length/] 

    def call(name, start, finish, message_id, values) 
     # FIXME: this seems bad. we should probably have a better way to indicate 
     # the query was cached 
     unless 'CACHE' == values[:name] 
     self.class.query_count += 1 unless IGNORED_SQL.any? { |r| values[:sql] =~ r } 
     end 
    end 
    end 
end 

ActiveSupport::Notifications.subscribe('sql.active_record', ActiveRecord::QueryCounter.new) 

module ActiveRecord 
    class Base 
    def self.count_queries(&block) 
     ActiveRecord::QueryCounter.query_count = 0 
     yield 
     ActiveRecord::QueryCounter.query_count 
    end 
    end 
end 

您将能够在任何地方参考ActiveRecord::Base.count_queries方法。将它传递给您的查询已运行的块,它将返回已执行的查询数:

ActiveRecord::Base.count_queries do 
    Ticket.first 
end 

对我而言返回“1”。为了使这项工作,把它放在一个文件在lib/active_record/query_counter.rb并要求其在config/application.rb文件是这样的:

require 'active_record/query_counter' 

变戏法似的!


可能需要一点解释。当我们将这条线称为:

ActiveSupport::Notifications.subscribe('sql.active_record', ActiveRecord::QueryCounter.new) 

我们挂钩到Rails 3的小通知框架。对于最新的Rails主要版本来说,这是一个闪光点,没有人真正知道。它允许我们通过使用subscribe方法来订阅Rails中事件的通知。我们通过我们想要订阅的事件作为第一个参数,然后将任何响应call的对象作为第二个参数。

在这种情况下,当执行查询时,我们的小查询计数器将尽职地增加ActiveRecord :: QueryCounter.query_count变量,但仅限于实数查询。

无论如何,这很有趣。我希望它对你有用。

+2

伟大的脚本。如果您仅将其用于测试,则可以将其放入{spec | test} /support/query_counter.rb文件中。保留应用程序逻辑的lib文件夹。 – Forrest 2012-04-03 03:56:09

+0

任何方式使这项工作.18.7 – 2015-05-01 16:42:08

+0

对于那些寻找RSpec匹配器,这个答案已经变成了一个宝石:['rspec-sqlimit'](https://github.com/nepalez/rspec-sqlimit) 。 – 2017-09-07 18:54:26

20

我瑞安的脚本(清理了一下,包裹在一个匹配),希望它仍然是实际的人的愿景:

我把这个投机/支持/ query_counter.rb

module ActiveRecord 
    class QueryCounter 

    attr_reader :query_count 

    def initialize 
     @query_count = 0 
    end 

    def to_proc 
     lambda(&method(:callback)) 
    end 

    def callback(name, start, finish, message_id, values) 
     @query_count += 1 unless %w(CACHE SCHEMA).include?(values[:name]) 
    end 

    end 
end 

,并且这是针对spec/support/matchers/exceed_query_limit。RB

RSpec::Matchers.define :exceed_query_limit do |expected| 

    match do |block| 
    query_count(&block) > expected 
    end 

    failure_message_for_should_not do |actual| 
    "Expected to run maximum #{expected} queries, got #{@counter.query_count}" 
    end 

    def query_count(&block) 
    @counter = ActiveRecord::QueryCounter.new 
    ActiveSupport::Notifications.subscribed(@counter.to_proc, 'sql.active_record', &block) 
    @counter.query_count 
    end 

end 

用法:

expect { MyModel.do_the_queries }.to_not exceed_query_limit(2) 
+0

[此要点]中RSpec 3的小更新(https://gist.github.com/rsutphin/af06c9e3dadf658d2293)。 – 2014-07-07 14:41:31

10

这里是瑞安的另一配方和尤里的解决方案,它只是一个功能添加到您test_helper.rb

def count_queries &block 
    count = 0 

    counter_f = ->(name, started, finished, unique_id, payload) { 
    unless payload[:name].in? %w[ CACHE SCHEMA ] 
     count += 1 
    end 
    } 

    ActiveSupport::Notifications.subscribed(counter_f, "sql.active_record", &block) 

    count 
end 

用法就是:

c = count_queries do 
    SomeModel.first 
end 
0

根据Jaime的回答,以下内容支持目前测试用例中查询数量的断言,并且在失败的情况下会记录这些语句。我认为将这样的SQL检查与功能测试结合起来可以很实用,因为它减少了设置工作。

class ActiveSupport::TestCase 

    ActiveSupport::Notifications.subscribe('sql.active_record') do |name, started, finished, unique_id, payload| 
    (@@queries||=[]) << payload unless payload[:name].in? %w(CACHE SCHEMA) 
    end 

    def assert_queries_count(expected_count, message=nil) 
    assert_equal expected_count, @@queries.size, 
     message||"Expected #{expected_count} queries, but #{@@queries.size} queries occurred.#{@@queries[0,20].join(' ')}" 
    end 

    # common setup in a super-class (or use Minitest::Spec etc to do it another way) 
    def setup 
    @@queries = [] 
    end 

end 

用法:

def test_something 
    post = Post.new('foo') 
    assert_queries_count 1 # SQL performance check 
    assert_equal "Under construction", post.body # standard functional check 
end 

注意查询断言应以防其他主张立即发生触发自己额外的查询。

1

这是一个版本,可以很容易地计算匹配给定模式的查询。

module QueryCounter 

    def self.count_selects(&block) 
    count(pattern: /^(\s+)?SELECT/, &block) 
    end 

    def self.count(pattern: /(.*?)/, &block) 
    counter = 0 

    callback = ->(name, started, finished, callback_id, payload) { 
     counter += 1 if payload[:sql].match(pattern) 
     # puts "match? #{!!payload[:sql].match(pattern)}: #{payload[:sql]}" 
    } 

    # http://api.rubyonrails.org/classes/ActiveSupport/Notifications.html 
    ActiveSupport::Notifications.subscribed(callback, "sql.active_record", &block) 

    counter 
    end 

end 

用法:

test "something" do 
    query_count = count_selects { 
    Thing.first 
    Thing.create!(size: "huge") 
    } 
    assert_equal 1, query_count 
end 
2
  • 有用的错误消息
  • 执行删除后的用户

(基于海梅湛的答案)

class ActiveSupport::TestCase 
    def sql_queries(&block) 
    queries = [] 
    counter = ->(*, payload) { 
     queries << payload.fetch(:sql) unless ["CACHE", "SCHEMA"].include?(payload.fetch(:name)) 
    } 

    ActiveSupport::Notifications.subscribed(counter, "sql.active_record", &block) 

    queries 
    end 

    def assert_sql_queries(expected, &block) 
    queries = sql_queries(&block) 
    queries.count.must_equal(
     expected, 
     "Expected #{expected} queries, but found #{queries.count}:\n#{queries.join("\n")}" 
    ) 
    end 
end