Skip to content

Commit

Permalink
Expose assert_queries_match and assert_no_queries_match assertions
Browse files Browse the repository at this point in the history
  • Loading branch information
fatkodima committed Dec 20, 2023
1 parent bf725a7 commit f48bbff
Show file tree
Hide file tree
Showing 65 changed files with 934 additions and 790 deletions.
4 changes: 2 additions & 2 deletions actiontext/test/unit/model_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ class ActionText::ModelTest < ActiveSupport::TestCase
test "eager loading" do
Message.create!(subject: "Subject", content: "<h1>Content</h1>")

message = assert_queries(2) { Message.with_rich_text_content.last }
message = assert_queries_count(2) { Message.with_rich_text_content.last }
assert_no_queries do
assert_equal "Content", message.content.to_plain_text
end
Expand All @@ -108,7 +108,7 @@ class ActionText::ModelTest < ActiveSupport::TestCase
test "eager loading all rich text" do
Message.create!(subject: "Subject", content: "<h1>Content</h1>", body: "<h2>Body</h2>")

message = assert_queries(1) { Message.with_all_rich_text.last }
message = assert_queries_count(1) { Message.with_all_rich_text.last }
assert_no_queries do
assert_equal "Content", message.content.to_plain_text
assert_equal "Body", message.body.to_plain_text
Expand Down
2 changes: 1 addition & 1 deletion actionview/test/activerecord/relation_cache_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ def setup
end

def test_cache_relation_other
assert_queries(1) do
assert_queries_count(1) do
cache(Project.all) { concat("Hello World") }
end
assert_equal "Hello World", controller.cache_store.read("views/test/hello_world:fa9482a68ce25bf7589b8eddad72f736/projects-#{Project.count}")
Expand Down
19 changes: 13 additions & 6 deletions activerecord/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,21 +23,28 @@

*Jean Boussier*

* Make `assert_queries` and `assert_no_queries` assertions public.
* Make `assert_queries_count`, `assert_no_queries`, `assert_queries_match` and
`assert_no_queries_match` assertions public.

To assert the expected number of queries are made, Rails internally uses
`assert_queries` and `assert_no_queries`. These assertions can be now
be used in applications as well.
To assert the expected number of queries are made, Rails internally uses `assert_queries_count` and
`assert_no_queries`. To assert that specific SQL queries are made, `assert_queries_match` and
`assert_no_queries_match` are used. These assertions can now be used in applications as well.

```ruby
class ArticleTest < ActiveSupport::TestCase
test "queries are made" do
assert_queries(1) { Article.first }
assert_queries_count(1) { Article.first }
end
test "creates a foreign key" do
assert_queries_match(/ADD FOREIGN KEY/i, include_schema: true) do
@connection.add_foreign_key(:comments, :posts)
end
end
end
```

*Petrik de Heus*
*Petrik de Heus*, *fatkodima*

* Fix `has_secure_token` calls the setter method on initialize.

Expand Down
101 changes: 88 additions & 13 deletions activerecord/lib/active_record/testing/query_assertions.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,29 +5,104 @@ module Assertions
module QueryAssertions
# Asserts that the number of SQL queries executed in the given block matches the expected count.
#
# assert_queries(1) { Post.first }
# # Check for exact number of queries
# assert_queries_count(1) { Post.first }
#
# If the +:matcher+ option is provided, only queries that match the matcher are counted.
# # Check for any number of queries
# assert_queries_count { Post.first }
#
# assert_queries(1, matcher: /LIMIT \?/) { Post.first }
# If the +:include_schema+ option is provided, any queries (including schema related) are counted.
#
def assert_queries(expected_count, matcher: nil, &block)
# assert_queries_count(1, include_schema: true) { Post.columns }
#
def assert_queries_count(count = nil, include_schema: false, &block)
ActiveRecord::Base.connection.materialize_transactions

queries = []
callback = lambda do |*, payload|
queries << payload[:sql] if %w[ SCHEMA TRANSACTION ].exclude?(payload[:name]) && (matcher.nil? || payload[:sql].match(matcher))
end
ActiveSupport::Notifications.subscribed(callback, "sql.active_record") do
result = _assert_nothing_raised_or_warn("assert_queries", &block)
assert_equal expected_count, queries.size, "#{queries.size} instead of #{expected_count} queries were executed. Queries: #{queries.join("\n\n")}"
counter = SQLCounter.new
ActiveSupport::Notifications.subscribed(counter, "sql.active_record") do
result = _assert_nothing_raised_or_warn("assert_queries_count", &block)
queries = include_schema ? counter.log_all : counter.log
if count
assert_equal count, queries.size, "#{queries.size} instead of #{count} queries were executed. Queries: #{queries.join("\n\n")}"
else
assert_operator queries.size, :>=, 1, "1 or more queries expected, but none were executed.#{queries.empty? ? '' : "\nQueries:\n#{queries.join("\n")}"}"
end
result
end
end

# Asserts that no SQL queries are executed in the given block.
def assert_no_queries(&block)
assert_queries(0, &block)
#
# assert_no_queries { post.comments }
#
# If the +:include_schema+ option is provided, any queries (including schema related) are counted.
#
# assert_no_queries(include_schema: true) { Post.columns }
#
def assert_no_queries(include_schema: false, &block)
assert_queries_count(0, include_schema: include_schema, &block)
end

# Asserts that the SQL queries executed in the given block match expected pattern.
#
# # Check for exact number of queries
# assert_queries_match(/LIMIT \?/, count: 1) { Post.first }
#
# # Check for any number of queries
# assert_queries_match(/LIMIT \?/) { Post.first }
#
# If the +:include_schema+ option is provided, any queries (including schema related)
# that match the matcher are considered.
#
# assert_queries_match(/FROM pg_attribute/i, include_schema: true) { Post.columns }
#
def assert_queries_match(match, count: nil, include_schema: false, &block)
ActiveRecord::Base.connection.materialize_transactions

counter = SQLCounter.new
ActiveSupport::Notifications.subscribed(counter, "sql.active_record") do
result = _assert_nothing_raised_or_warn("assert_queries_match", &block)
queries = include_schema ? counter.log_all : counter.log
matched_queries = queries.select { |query| match === query }

if count
assert_equal count, matched_queries.size, "#{matched_queries.size} instead of #{count} queries were executed.#{queries.empty? ? '' : "\nQueries:\n#{queries.join("\n")}"}"
else
assert_operator matched_queries.size, :>=, 1, "1 or more queries expected, but none were executed.#{queries.empty? ? '' : "\nQueries:\n#{queries.join("\n")}"}"
end

result
end
end

# Asserts that no SQL queries matching the pattern are executed in the given block.
#
# assert_no_queries_match(/SELECT/i) { post.comments }
#
# If the +:include_schema+ option is provided, any queries (including schema related)
# that match the matcher are counted.
#
# assert_no_queries_match(/FROM pg_attribute/i, include_schema: true) { Post.columns }
#
def assert_no_queries_match(match, include_schema: false, &block)
assert_queries_match(match, count: 0, include_schema: include_schema, &block)
end

class SQLCounter # :nodoc:
attr_reader :log, :log_all

def initialize
@log = []
@log_all = []
end

def call(*, payload)
return if payload[:cached]

sql = payload[:sql]
@log_all << sql
@log << sql unless payload[:name] == "SCHEMA"
end
end
end
end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,15 +99,15 @@ def test_index_in_create
def test_index_in_bulk_change
%w(SPATIAL FULLTEXT UNIQUE).each do |type|
expected = "ALTER TABLE `people` ADD #{type} INDEX `index_people_on_last_name` (`last_name`)"
assert_sql(expected) do
assert_queries_match(expected) do
ActiveRecord::Base.connection.change_table(:people, bulk: true) do |t|
t.index :last_name, type: type
end
end
end

expected = "ALTER TABLE `people` ADD INDEX `index_people_on_last_name` USING btree (`last_name`(10)), ALGORITHM = COPY"
assert_sql(expected) do
assert_queries_match(expected) do
ActiveRecord::Base.connection.change_table(:people, bulk: true) do |t|
t.index :last_name, length: 10, using: :btree, algorithm: :copy
end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,45 +8,45 @@ class OptimizerHintsTest < ActiveRecord::AbstractMysqlTestCase
fixtures :posts

def test_optimizer_hints
assert_sql(%r{\ASELECT /\*\+ NO_RANGE_OPTIMIZATION\(posts index_posts_on_author_id\) \*/}) do
assert_queries_match(%r{\ASELECT /\*\+ NO_RANGE_OPTIMIZATION\(posts index_posts_on_author_id\) \*/}) do
posts = Post.optimizer_hints("NO_RANGE_OPTIMIZATION(posts index_posts_on_author_id)")
posts = posts.select(:id).where(author_id: [0, 1])
assert_includes posts.explain, "| index | index_posts_on_author_id | index_posts_on_author_id |"
end
end

def test_optimizer_hints_with_count_subquery
assert_sql(%r{\ASELECT /\*\+ NO_RANGE_OPTIMIZATION\(posts index_posts_on_author_id\) \*/}) do
assert_queries_match(%r{\ASELECT /\*\+ NO_RANGE_OPTIMIZATION\(posts index_posts_on_author_id\) \*/}) do
posts = Post.optimizer_hints("NO_RANGE_OPTIMIZATION(posts index_posts_on_author_id)")
posts = posts.select(:id).where(author_id: [0, 1]).limit(5)
assert_equal 5, posts.count
end
end

def test_optimizer_hints_is_sanitized
assert_sql(%r{\ASELECT /\*\+ NO_RANGE_OPTIMIZATION\(posts index_posts_on_author_id\) \*/}) do
assert_queries_match(%r{\ASELECT /\*\+ NO_RANGE_OPTIMIZATION\(posts index_posts_on_author_id\) \*/}) do
posts = Post.optimizer_hints("/*+ NO_RANGE_OPTIMIZATION(posts index_posts_on_author_id) */")
posts = posts.select(:id).where(author_id: [0, 1])
assert_includes posts.explain, "| index | index_posts_on_author_id | index_posts_on_author_id |"
end

assert_sql(%r{\ASELECT /\*\+ \*\* // `posts`\.\*, // \*\* \*/}) do
assert_queries_match(%r{\ASELECT /\*\+ \*\* // `posts`\.\*, // \*\* \*/}) do
posts = Post.optimizer_hints("**// `posts`.*, //**")
posts = posts.select(:id).where(author_id: [0, 1])
assert_equal({ "id" => 1 }, posts.first.as_json)
end
end

def test_optimizer_hints_with_unscope
assert_sql(%r{\ASELECT `posts`\.`id`}) do
assert_queries_match(%r{\ASELECT `posts`\.`id`}) do
posts = Post.optimizer_hints("/*+ NO_RANGE_OPTIMIZATION(posts index_posts_on_author_id) */")
posts = posts.select(:id).where(author_id: [0, 1])
posts.unscope(:optimizer_hints).load
end
end

def test_optimizer_hints_with_or
assert_sql(%r{\ASELECT /\*\+ NO_RANGE_OPTIMIZATION\(posts index_posts_on_author_id\) \*/}) do
assert_queries_match(%r{\ASELECT /\*\+ NO_RANGE_OPTIMIZATION\(posts index_posts_on_author_id\) \*/}) do
Post.optimizer_hints("NO_RANGE_OPTIMIZATION(posts index_posts_on_author_id)")
.or(Post.all).load
end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,19 +24,19 @@ def teardown
end

def test_encoding
assert_queries(1, ignore_none: true) do
assert_queries_count(1, include_schema: true) do
assert_not_nil @connection.encoding
end
end

def test_collation
assert_queries(1, ignore_none: true) do
assert_queries_count(1, include_schema: true) do
assert_not_nil @connection.collation
end
end

def test_ctype
assert_queries(1, ignore_none: true) do
assert_queries_count(1, include_schema: true) do
assert_not_nil @connection.ctype
end
end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,45 +12,45 @@ def setup
end

def test_optimizer_hints
assert_sql(%r{\ASELECT /\*\+ SeqScan\(posts\) \*/}) do
assert_queries_match(%r{\ASELECT /\*\+ SeqScan\(posts\) \*/}) do
posts = Post.optimizer_hints("SeqScan(posts)")
posts = posts.select(:id).where(author_id: [0, 1])
assert_includes posts.explain, "Seq Scan on posts"
end
end

def test_optimizer_hints_with_count_subquery
assert_sql(%r{\ASELECT /\*\+ SeqScan\(posts\) \*/}) do
assert_queries_match(%r{\ASELECT /\*\+ SeqScan\(posts\) \*/}) do
posts = Post.optimizer_hints("SeqScan(posts)")
posts = posts.select(:id).where(author_id: [0, 1]).limit(5)
assert_equal 5, posts.count
end
end

def test_optimizer_hints_is_sanitized
assert_sql(%r{\ASELECT /\*\+ SeqScan\(posts\) \*/}) do
assert_queries_match(%r{\ASELECT /\*\+ SeqScan\(posts\) \*/}) do
posts = Post.optimizer_hints("/*+ SeqScan(posts) */")
posts = posts.select(:id).where(author_id: [0, 1])
assert_includes posts.explain, "Seq Scan on posts"
end

assert_sql(%r{\ASELECT /\*\+ "posts"\.\*, \*/}) do
assert_queries_match(%r{\ASELECT /\*\+ "posts"\.\*, \*/}) do
posts = Post.optimizer_hints("**// \"posts\".*, //**")
posts = posts.select(:id).where(author_id: [0, 1])
assert_equal({ "id" => 1 }, posts.first.as_json)
end
end

def test_optimizer_hints_with_unscope
assert_sql(%r{\ASELECT "posts"\."id"}) do
assert_queries_match(%r{\ASELECT "posts"\."id"}) do
posts = Post.optimizer_hints("/*+ SeqScan(posts) */")
posts = posts.select(:id).where(author_id: [0, 1])
posts.unscope(:optimizer_hints).load
end
end

def test_optimizer_hints_with_or
assert_sql(%r{\ASELECT /\*\+ SeqScan\(posts\) \*/}) do
assert_queries_match(%r{\ASELECT /\*\+ SeqScan\(posts\) \*/}) do
Post.optimizer_hints("SeqScan(posts)").or(Post.all).load
end

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -462,14 +462,14 @@ def test_reload_type_map_for_newly_defined_types
@connection.create_enum "feeling", ["good", "bad"]

# Runs only SELECT, no type map reloading.
assert_queries(1, ignore_none: true) do
assert_queries_count(1, include_schema: true) do
result = @connection.select_all "SELECT 'good'::feeling"
assert_instance_of(PostgreSQLAdapter::OID::Enum,
result.column_types["feeling"])
end
ensure
# Reloads type map.
assert_sql(/from pg_type/i) do
assert_queries_match(/from pg_type/i, include_schema: true) do
@connection.drop_enum "feeling", if_exists: true
end
reset_connection
Expand All @@ -481,13 +481,13 @@ def test_only_reload_type_map_once_for_every_unrecognized_type
connection.select_all "SELECT 1" # eagerly initialize the connection

silence_warnings do
assert_queries 2, ignore_none: true do
assert_queries_count(2, include_schema: true) do
connection.select_all "select 'pg_catalog.pg_class'::regclass"
end
assert_queries 1, ignore_none: true do
assert_queries_count(1, include_schema: true) do
connection.select_all "select 'pg_catalog.pg_class'::regclass"
end
assert_queries 2, ignore_none: true do
assert_queries_count(2, include_schema: true) do
connection.select_all "SELECT NULL::anyarray"
end
end
Expand Down Expand Up @@ -534,7 +534,7 @@ def test_only_check_for_insensitive_comparison_capability_once
self.table_name = "ex"
end
attribute = number_klass.arel_table[:number]
assert_queries :any, ignore_none: true do
assert_queries_count(include_schema: true) do
@connection.case_insensitive_comparison(attribute, "foo")
end
assert_no_queries do
Expand Down
Loading

0 comments on commit f48bbff

Please sign in to comment.