New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Expose assert_queries_match
and assert_no_queries_match
assertions
#50373
Expose assert_queries_match
and assert_no_queries_match
assertions
#50373
Conversation
c1572f3
to
3a0ee30
Compare
3a0ee30
to
9a1543b
Compare
assert_queries(6) do | ||
# 6 queries: | ||
# users x 1 | ||
assert_queries(5) do |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Strange 🤔 Any idea why this changed?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The assert_queries
method originally counted for cached queries, now it is not.
We can probably also have an option to configure if we count for cached queries or not, but not sure if it will be used much.
I like that this cleans up the ActiveRecord code base and makes sure we only have one |
My original idea for this PR was exactly to make |
So in my opinion we should keep the assert relatively simple and straight forward, I'd rather avoid to cram too much in there. We can however add a few more assertions, e.g. for the regexp we could have |
Wdyt if I left |
if expected_count.nil? | ||
assert_operator matched_queries.size, :>=, 1, "1 or more queries expected, but none were executed." | ||
else | ||
assert_equal expected_count, matched_queries.size, "#{matched_queries.size} instead of #{expected_count} queries were executed. Queries: #{matched_queries.join("\n\n")}" | ||
end | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would it be valuable to support Range
instances too?
Something like assert_queries(2..3) { ... }
. It'd also support endless Ranges: assert_queries(..3) { ... }
.
if expected_count.nil? | |
assert_operator matched_queries.size, :>=, 1, "1 or more queries expected, but none were executed." | |
else | |
assert_equal expected_count, matched_queries.size, "#{matched_queries.size} instead of #{expected_count} queries were executed. Queries: #{matched_queries.join("\n\n")}" | |
end | |
end | |
case expected_count | |
when NilClass | |
assert_operator matched_queries.size, :>=, 1, "1 or more queries expected, but none were executed." | |
when Range | |
assert_includes expected_count, matched_queries.size, "expected #{matched_queries.size} executed queries to be within #{expected_count}. Queries: #{matched_queries.join("\n\n")}" | |
else | |
assert_equal expected_count, matched_queries.size, "#{matched_queries.size} instead of #{expected_count} queries were executed. Queries: #{matched_queries.join("\n\n")}" | |
end | |
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you provide a use case for this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The main motivation is flexibility and maintenance.
I'm imagining introducing this assertion into an existing application test suite in the hopes that it'd drive efforts to minimize queries (either as an optimization or as a reaction to issues like N+1s). In this hypothetical application, I might introduce the assertion with a specific query, pass CI, then merge my change. A teammate might make a change that would make a query and the test might fail. They could update the assertion to use the more correct number, pass CI, then merge their change.
Depending on the goals of the team using this assertion that type of churn might start a virtuous cycle that forces the team to change the test's number for necessary queries. Alternatively, it might change often enough to develop a negative perception, feel onerous, and ultimately get removed.
In cases where the assertion is used as an optimization, teams might be interested in providing a Range
(like "fewer than 10" or ..10
) that they could use as a arbitrary goal.
Personally, my preference is for the specific, exact count version. In my experiences, constant churn can deter that style of usage.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Personally, my preference is for the specific, exact count version.
This is my preference too. Let's see what other people say.
Yeah, I'd prefer that. As long as |
Just to clarify my stance, I think we should aim for the 20/80 here. 20% of the features that are sufficient for 80% of users. |
9a1543b
to
a22acbd
Compare
Made some changes in a separate commit - reverted an ability to provide multiple regexes. Currently (as before), it allows only one regexp. Now, the only changes this PR introduces is the ability to not pass an exact number of queries and ability to account for internal queries (like Considering, that these changes are now pretty simple imo and even cover everything from If this is ok, I will squash my commits. |
# | ||
# assert_queries(ignore_none: true) { Post.columns } | ||
# | ||
def assert_queries(expected_count = nil, matcher: nil, ignore_none: false, &block) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I really don't like ignore_none
as a parameter name, as it's a double negation.
Also I wonder that the use case is?
I still think it would be much simpler to use if that capability was in another |
Can you explain, which exact capability do you mean? Yeah, I was thinking about I would appreciate, if you will help me separate capabilities between |
Ok. So my thinking is I see two use cases. The main one is to assert how many queries are performed by a block of code, this generally is for performance critical sections of code, you want to make sure you don't accidentally introduce a new query. That is what Then the other use case it to assert that a specific query was (or wasn't) performed. To be honest I don't quite see as uch value in this one, it's more niche I think, but I don't mind so much having it.
Should it though? We're making this public now, so we have the opportunity to rethink the interface. I'm deep in the |
a22acbd
to
5d7fc51
Compare
assert_queries
and assert_no_queries
with more matching optionsassert_sql
and assert_no_sql
assertions
Updated |
# | ||
# assert_sql(include_all: true) { Post.columns } | ||
# | ||
def assert_sql(expected_count = nil, matcher: nil, include_all: false, &block) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How do you feel about renaming the :matcher
keyword argument to :match
? It'd mirror the new :match
keyword argument that assert_raises supports, and would correspond to the Regexp#match method name.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍
If assert_sql_match(/create index/i, include_all: true) do
do_something
end Or make it more inline with assert_query_match(/create index/i, include_all: true) do
do_something
end |
Currently, it does not require a |
5d7fc51
to
17bdd93
Compare
# | ||
# If the +:matcher+ option is provided, only queries that match the matcher are counted. | ||
# | ||
# assert_no_sql(1, match: /LIMIT \?/) { Post.first } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You probably meant:
# assert_no_sql(1, match: /LIMIT \?/) { Post.first } | |
# assert_no_sql(match: /LIMIT \?/) { Post.first } |
# | ||
# assert_sql(include_all: true) { Post.columns } | ||
# | ||
def assert_sql(expected_count = nil, match: nil, include_all: false, &block) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think I'd inverse the parameters:
def assert_sql(expected_count = nil, match: nil, include_all: false, &block) | |
def assert_sql(match, count: nil, include_all: false, &block) |
What do you think?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I originally thought for match
to not be required to be able to write something like assert_sql(include_all: true)
to test that any SQL query was made.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IMO assert_sql
means I want to assert some query matches something.
If I only care about the count, it should be assert_queries
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I agree with you. There is a need to update quite a few test cases to specify these concrete sqls, like in https://github.com/rails/rails/pull/50373/files#diff-9bc4b7719b110cde3044f7e2ce6f8cda8f8e3263790d600e6d6792e40c02fa70R831
Will try to update with the suggestion.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, does the count apply to the matching queries or all queries?
Edit: Nevermind, this is rather obvious.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Made matcher a required argument.
17bdd93
to
d765ecb
Compare
@@ -1437,7 +1437,7 @@ def test_changing_column_null_with_default | |||
raise "need an expected query count for #{classname}" | |||
} | |||
|
|||
assert_queries(expected_query_count, ignore_none: true) do | |||
assert_sql(//, count: expected_query_count, include_all: true) do |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This suggest assert_queries
should have an include_all
too.
Starting to look good to me. Only the |
d765ecb
to
ec23031
Compare
If I only see Maybe something like: assert_sql_match(/ADD FOREIGN KEY/i, include_schema: true) do
@connection.add_foreign_key(:comments, :posts)
end |
I'm not even sure ignoring transactions make that much sense. Looking at our private implementation, it ignore schema queries by default (because they tend to be flaky), but transactions are included. And IMO it makes sense because |
What if then we rename
|
Or Just a though, no strong opinion here.
Yeah that's the idea. |
I like |
You probably meant |
3d886f1
to
1176988
Compare
assert_sql
and assert_no_sql
assertionsassert_queries_match
and assert_no_queries_match
assertions
1176988
to
118d67e
Compare
118d67e
to
f48bbff
Compare
Changed names: and transaction related queries are now always counted. |
LGTM. What do you think @p8 ? |
Looks great! |
Thank you all! ❤️ That was quite a lengthy discussion 💪 😅 |
Thanks for the work @fatkodima ! 😄 |
Follow up to rails#50382. `assert_queries` was renamed to `assert_queries_count` in in rails#50373.
Follow up to rails#50382. `assert_queries` was renamed to `assert_queries_count` in rails#50373.
This was initially changed in rails#50373, with an explanation that the cached query was not included in the count. rails#50373 (comment) However, it seems that this is no longer the case. /cc @fatkodima
This was initially changed in rails#50373, with an explanation that the cached query was not included in the count. rails#50373 (comment) However, it seems that this is no longer the case. /cc @fatkodima
Follow up to #50281.
The originally introduced (exposed) helpers are a bit limited. For example:
These limitations work most of the time for regular users, but not when trying to test some lower behavior (for example, inside some gem's tests).
For example, it was not possible to test that an index and a foreign key are created when running some operation.
Now, this can be done via:
cc @byroot @p8