2011-03-30 150 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