Skip to content

Commit 60df089

Browse files
authored
Merge pull request rails#50373 from fatkodima/improve-queries-assertions-matchers
Expose `assert_queries_match` and `assert_no_queries_match` assertions
2 parents 6c0f897 + f48bbff commit 60df089

File tree

65 files changed

+934
-790
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

65 files changed

+934
-790
lines changed

actiontext/test/unit/model_test.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ class ActionText::ModelTest < ActiveSupport::TestCase
9999
test "eager loading" do
100100
Message.create!(subject: "Subject", content: "<h1>Content</h1>")
101101

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

111-
message = assert_queries(1) { Message.with_all_rich_text.last }
111+
message = assert_queries_count(1) { Message.with_all_rich_text.last }
112112
assert_no_queries do
113113
assert_equal "Content", message.content.to_plain_text
114114
assert_equal "Body", message.body.to_plain_text

actionview/test/activerecord/relation_cache_test.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ def setup
1919
end
2020

2121
def test_cache_relation_other
22-
assert_queries(1) do
22+
assert_queries_count(1) do
2323
cache(Project.all) { concat("Hello World") }
2424
end
2525
assert_equal "Hello World", controller.cache_store.read("views/test/hello_world:fa9482a68ce25bf7589b8eddad72f736/projects-#{Project.count}")

activerecord/CHANGELOG.md

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,21 +23,28 @@
2323

2424
*Jean Boussier*
2525

26-
* Make `assert_queries` and `assert_no_queries` assertions public.
26+
* Make `assert_queries_count`, `assert_no_queries`, `assert_queries_match` and
27+
`assert_no_queries_match` assertions public.
2728

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

3233
```ruby
3334
class ArticleTest < ActiveSupport::TestCase
3435
test "queries are made" do
35-
assert_queries(1) { Article.first }
36+
assert_queries_count(1) { Article.first }
37+
end
38+
39+
test "creates a foreign key" do
40+
assert_queries_match(/ADD FOREIGN KEY/i, include_schema: true) do
41+
@connection.add_foreign_key(:comments, :posts)
42+
end
3643
end
3744
end
3845
```
3946

40-
*Petrik de Heus*
47+
*Petrik de Heus*, *fatkodima*
4148

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

activerecord/lib/active_record/testing/query_assertions.rb

Lines changed: 88 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,29 +5,104 @@ module Assertions
55
module QueryAssertions
66
# Asserts that the number of SQL queries executed in the given block matches the expected count.
77
#
8-
# assert_queries(1) { Post.first }
8+
# # Check for exact number of queries
9+
# assert_queries_count(1) { Post.first }
910
#
10-
# If the +:matcher+ option is provided, only queries that match the matcher are counted.
11+
# # Check for any number of queries
12+
# assert_queries_count { Post.first }
1113
#
12-
# assert_queries(1, matcher: /LIMIT \?/) { Post.first }
14+
# If the +:include_schema+ option is provided, any queries (including schema related) are counted.
1315
#
14-
def assert_queries(expected_count, matcher: nil, &block)
16+
# assert_queries_count(1, include_schema: true) { Post.columns }
17+
#
18+
def assert_queries_count(count = nil, include_schema: false, &block)
1519
ActiveRecord::Base.connection.materialize_transactions
1620

17-
queries = []
18-
callback = lambda do |*, payload|
19-
queries << payload[:sql] if %w[ SCHEMA TRANSACTION ].exclude?(payload[:name]) && (matcher.nil? || payload[:sql].match(matcher))
20-
end
21-
ActiveSupport::Notifications.subscribed(callback, "sql.active_record") do
22-
result = _assert_nothing_raised_or_warn("assert_queries", &block)
23-
assert_equal expected_count, queries.size, "#{queries.size} instead of #{expected_count} queries were executed. Queries: #{queries.join("\n\n")}"
21+
counter = SQLCounter.new
22+
ActiveSupport::Notifications.subscribed(counter, "sql.active_record") do
23+
result = _assert_nothing_raised_or_warn("assert_queries_count", &block)
24+
queries = include_schema ? counter.log_all : counter.log
25+
if count
26+
assert_equal count, queries.size, "#{queries.size} instead of #{count} queries were executed. Queries: #{queries.join("\n\n")}"
27+
else
28+
assert_operator queries.size, :>=, 1, "1 or more queries expected, but none were executed.#{queries.empty? ? '' : "\nQueries:\n#{queries.join("\n")}"}"
29+
end
2430
result
2531
end
2632
end
2733

2834
# Asserts that no SQL queries are executed in the given block.
29-
def assert_no_queries(&block)
30-
assert_queries(0, &block)
35+
#
36+
# assert_no_queries { post.comments }
37+
#
38+
# If the +:include_schema+ option is provided, any queries (including schema related) are counted.
39+
#
40+
# assert_no_queries(include_schema: true) { Post.columns }
41+
#
42+
def assert_no_queries(include_schema: false, &block)
43+
assert_queries_count(0, include_schema: include_schema, &block)
44+
end
45+
46+
# Asserts that the SQL queries executed in the given block match expected pattern.
47+
#
48+
# # Check for exact number of queries
49+
# assert_queries_match(/LIMIT \?/, count: 1) { Post.first }
50+
#
51+
# # Check for any number of queries
52+
# assert_queries_match(/LIMIT \?/) { Post.first }
53+
#
54+
# If the +:include_schema+ option is provided, any queries (including schema related)
55+
# that match the matcher are considered.
56+
#
57+
# assert_queries_match(/FROM pg_attribute/i, include_schema: true) { Post.columns }
58+
#
59+
def assert_queries_match(match, count: nil, include_schema: false, &block)
60+
ActiveRecord::Base.connection.materialize_transactions
61+
62+
counter = SQLCounter.new
63+
ActiveSupport::Notifications.subscribed(counter, "sql.active_record") do
64+
result = _assert_nothing_raised_or_warn("assert_queries_match", &block)
65+
queries = include_schema ? counter.log_all : counter.log
66+
matched_queries = queries.select { |query| match === query }
67+
68+
if count
69+
assert_equal count, matched_queries.size, "#{matched_queries.size} instead of #{count} queries were executed.#{queries.empty? ? '' : "\nQueries:\n#{queries.join("\n")}"}"
70+
else
71+
assert_operator matched_queries.size, :>=, 1, "1 or more queries expected, but none were executed.#{queries.empty? ? '' : "\nQueries:\n#{queries.join("\n")}"}"
72+
end
73+
74+
result
75+
end
76+
end
77+
78+
# Asserts that no SQL queries matching the pattern are executed in the given block.
79+
#
80+
# assert_no_queries_match(/SELECT/i) { post.comments }
81+
#
82+
# If the +:include_schema+ option is provided, any queries (including schema related)
83+
# that match the matcher are counted.
84+
#
85+
# assert_no_queries_match(/FROM pg_attribute/i, include_schema: true) { Post.columns }
86+
#
87+
def assert_no_queries_match(match, include_schema: false, &block)
88+
assert_queries_match(match, count: 0, include_schema: include_schema, &block)
89+
end
90+
91+
class SQLCounter # :nodoc:
92+
attr_reader :log, :log_all
93+
94+
def initialize
95+
@log = []
96+
@log_all = []
97+
end
98+
99+
def call(*, payload)
100+
return if payload[:cached]
101+
102+
sql = payload[:sql]
103+
@log_all << sql
104+
@log << sql unless payload[:name] == "SCHEMA"
105+
end
31106
end
32107
end
33108
end

activerecord/test/cases/adapters/abstract_mysql_adapter/active_schema_test.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -99,15 +99,15 @@ def test_index_in_create
9999
def test_index_in_bulk_change
100100
%w(SPATIAL FULLTEXT UNIQUE).each do |type|
101101
expected = "ALTER TABLE `people` ADD #{type} INDEX `index_people_on_last_name` (`last_name`)"
102-
assert_sql(expected) do
102+
assert_queries_match(expected) do
103103
ActiveRecord::Base.connection.change_table(:people, bulk: true) do |t|
104104
t.index :last_name, type: type
105105
end
106106
end
107107
end
108108

109109
expected = "ALTER TABLE `people` ADD INDEX `index_people_on_last_name` USING btree (`last_name`(10)), ALGORITHM = COPY"
110-
assert_sql(expected) do
110+
assert_queries_match(expected) do
111111
ActiveRecord::Base.connection.change_table(:people, bulk: true) do |t|
112112
t.index :last_name, length: 10, using: :btree, algorithm: :copy
113113
end

activerecord/test/cases/adapters/abstract_mysql_adapter/optimizer_hints_test.rb

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,45 +8,45 @@ class OptimizerHintsTest < ActiveRecord::AbstractMysqlTestCase
88
fixtures :posts
99

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

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

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

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

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

4848
def test_optimizer_hints_with_or
49-
assert_sql(%r{\ASELECT /\*\+ NO_RANGE_OPTIMIZATION\(posts index_posts_on_author_id\) \*/}) do
49+
assert_queries_match(%r{\ASELECT /\*\+ NO_RANGE_OPTIMIZATION\(posts index_posts_on_author_id\) \*/}) do
5050
Post.optimizer_hints("NO_RANGE_OPTIMIZATION(posts index_posts_on_author_id)")
5151
.or(Post.all).load
5252
end

activerecord/test/cases/adapters/postgresql/connection_test.rb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,19 +24,19 @@ def teardown
2424
end
2525

2626
def test_encoding
27-
assert_queries(1, ignore_none: true) do
27+
assert_queries_count(1, include_schema: true) do
2828
assert_not_nil @connection.encoding
2929
end
3030
end
3131

3232
def test_collation
33-
assert_queries(1, ignore_none: true) do
33+
assert_queries_count(1, include_schema: true) do
3434
assert_not_nil @connection.collation
3535
end
3636
end
3737

3838
def test_ctype
39-
assert_queries(1, ignore_none: true) do
39+
assert_queries_count(1, include_schema: true) do
4040
assert_not_nil @connection.ctype
4141
end
4242
end

activerecord/test/cases/adapters/postgresql/optimizer_hints_test.rb

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,45 +12,45 @@ def setup
1212
end
1313

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

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

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

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

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

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

activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -462,14 +462,14 @@ def test_reload_type_map_for_newly_defined_types
462462
@connection.create_enum "feeling", ["good", "bad"]
463463

464464
# Runs only SELECT, no type map reloading.
465-
assert_queries(1, ignore_none: true) do
465+
assert_queries_count(1, include_schema: true) do
466466
result = @connection.select_all "SELECT 'good'::feeling"
467467
assert_instance_of(PostgreSQLAdapter::OID::Enum,
468468
result.column_types["feeling"])
469469
end
470470
ensure
471471
# Reloads type map.
472-
assert_sql(/from pg_type/i) do
472+
assert_queries_match(/from pg_type/i, include_schema: true) do
473473
@connection.drop_enum "feeling", if_exists: true
474474
end
475475
reset_connection
@@ -481,13 +481,13 @@ def test_only_reload_type_map_once_for_every_unrecognized_type
481481
connection.select_all "SELECT 1" # eagerly initialize the connection
482482

483483
silence_warnings do
484-
assert_queries 2, ignore_none: true do
484+
assert_queries_count(2, include_schema: true) do
485485
connection.select_all "select 'pg_catalog.pg_class'::regclass"
486486
end
487-
assert_queries 1, ignore_none: true do
487+
assert_queries_count(1, include_schema: true) do
488488
connection.select_all "select 'pg_catalog.pg_class'::regclass"
489489
end
490-
assert_queries 2, ignore_none: true do
490+
assert_queries_count(2, include_schema: true) do
491491
connection.select_all "SELECT NULL::anyarray"
492492
end
493493
end
@@ -534,7 +534,7 @@ def test_only_check_for_insensitive_comparison_capability_once
534534
self.table_name = "ex"
535535
end
536536
attribute = number_klass.arel_table[:number]
537-
assert_queries :any, ignore_none: true do
537+
assert_queries_count(include_schema: true) do
538538
@connection.case_insensitive_comparison(attribute, "foo")
539539
end
540540
assert_no_queries do

0 commit comments

Comments
 (0)