我想测试一段代码执行尽可能少的SQL查询。
ActiveRecord::TestCase
似乎有自己的assert_queries
方法,它就是这样做的。但是因为我没有修补ActiveRecord,所以对我来说没什么用。
RSpec或ActiveRecord是否提供任何官方的,公开的方法来计算代码块中执行的SQL查询的数量?
答案 0 :(得分:47)
我认为你提到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'
嘿presto!
可能需要一些解释。当我们称这一行:
ActiveSupport::Notifications.subscribe('sql.active_record', ActiveRecord::QueryCounter.new)
我们挂钩了Rails 3的小通知框架。这是Rails的最新主要版本的一个闪亮的小补充,没有人真正知道。它允许我们使用subscribe
方法订阅Rails中的事件通知。我们将要订阅的事件作为第一个参数传递,然后传递响应call
作为第二个参数的任何对象。
在这种情况下,当执行查询时,我们的小查询计数器将尽职地增加ActiveRecord :: QueryCounter.query_count变量,但仅用于真正的查询。
无论如何,这很有趣。我希望它对你有用。
答案 1 :(得分:21)
我对Ryan剧本的看法(清理了一下并用一个匹配器包裹起来),希望它对某人来说仍然是真实的:
我把它放到spec / support / 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 / beyond_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)
答案 2 :(得分:11)
这是Ryan和Yuriy解决方案的另一种表述,它只是您添加到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
答案 3 :(得分:5)
(基于Jaime Cham的回答)
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
答案 4 :(得分: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
答案 5 :(得分: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
请注意,如果其他断言本身触发了额外的查询,则应立即进行查询断言。
答案 6 :(得分:0)
我添加了基于Yuriy解决方案检查每个表查询的功能
traverse
RSpec匹配器看起来像
# spec/support/query_counter.rb
require 'support/matchers/query_limit'
module ActiveRecord
class QueryCounter
attr_reader :queries
def initialize
@queries = Hash.new 0
end
def to_proc
lambda(&method(:callback))
end
def callback(name, start, finish, message_id, values)
sql = values[:sql]
if sql.include? 'SAVEPOINT'
table = :savepoints
else
finder = /select.+"(.+)"\..+from/i if sql.include? 'SELECT'
finder = /insert.+"(.+)".\(/i if sql.include? 'INSERT'
finder = /update.+"(.+)".+set/i if sql.include? 'UPDATE'
finder = /delete.+"(.+)" where/i if sql.include? 'DELETE'
table = sql.match(finder)&.send(:[],1)&.to_sym
end
@queries[table] += 1 unless %w(CACHE SCHEMA).include?(values[:name])
return @queries
end
def query_count(table = nil)
if table
@queries[table]
else
@queries.values.sum
end
end
end
end
用法
# spec/support/matchers/query_limit.rb
RSpec::Matchers.define :exceed_query_limit do |expected, table|
supports_block_expectations
match do |block|
query_count(table, &block) > expected
end
def query_count(table, &block)
@counter = ActiveRecord::QueryCounter.new
ActiveSupport::Notifications.subscribed(@counter.to_proc, 'sql.active_record', &block)
@counter.query_count table
end
failure_message_when_negated do |actual|
queries = 'query'.pluralize expected
table_name = table.to_s.singularize.humanize.downcase if table
out = "expected to run a maximum of #{expected}"
out += " #{table_name}" if table
out += " #{queries}, but got #{@counter.query_count table}"
end
end
RSpec::Matchers.define :meet_query_limit do |expected, table|
supports_block_expectations
match do |block|
if expected.is_a? Hash
results = queries_count(table, &block)
expected.all? { |table, count| results[table] == count }
else
query_count(&block) == expected
end
end
def queries_count(table, &block)
@counter = ActiveRecord::QueryCounter.new
ActiveSupport::Notifications.subscribed(@counter.to_proc, 'sql.active_record', &block)
@counter.queries
end
def query_count(&block)
@counter = ActiveRecord::QueryCounter.new
ActiveSupport::Notifications.subscribed(@counter.to_proc, 'sql.active_record', &block)
@counter.query_count
end
def message(expected, table, negated = false)
queries = 'query'.pluralize expected
if expected.is_a? Hash
results = @counter.queries
table, expected = expected.find { |table, count| results[table] != count }
end
table_name = table.to_s.singularize.humanize.downcase if table
out = 'expected to'
out += ' not' if negated
out += " run exactly #{expected}"
out += " #{table_name}" if table
out += " #{queries}, but got #{@counter.query_count table}"
end
failure_message do |actual|
message expected, table
end
failure_message_when_negated do |actual|
message expected, table, true
end
end
答案 7 :(得分:0)
我最终创建了一个小小的宝石来抽象这个问题:sql_spy。
只需将其添加到您的Gemfile中即可
gem "sql_spy"
将您的代码包装在SqlSpy.track { ... }
中:
queries = SqlSpy.track do
# Some code that triggers ActiveRecord queries
users = User.all
posts = BlogPost.all
end
...并在断言中使用该块的返回值:
expect(queries.size).to eq(2)
expect(queries[0].sql).to eq("SELECT * FROM users;")
expect(queries[0].model_name).to eq("User")
expect(queries[0].select?).to be_true
expect(queries[0].duration).to eq(1.5)