-
-
Notifications
You must be signed in to change notification settings - Fork 32.7k
Fixed #35728 -- Computed error messages in assertions only on test failures. #19402
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
base: main
Are you sure you want to change the base?
Conversation
django/test/testcases.py
Outdated
if count is None or count == 0: | ||
# Does needle exist regardless of count? | ||
real_count = int(parsed_needle in parsed_haystack) | ||
else: | ||
real_count = parsed_haystack.count(parsed_needle) | ||
if (count is None and real_count > 0) or count == real_count: | ||
# Shortcut. | ||
return | ||
|
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 performance gains of this are difficult to test using assertInHTML
because parse_html()
incurs somewhere from 50–100x the cost of that assertion (compared to _count()
) when I run it using the HTML in the PR description, checking for <span>Hello</span>
for example.
Hello @adamchainz! Should I extend the patch for the other assertions as well, e.g. |
No, I think those are safe. |
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.
Thank you @cliff688. I've added a thought that I had looking through this
It would be great if we add benchmarks to django-asv for our custom test assertions as that might have been able to catch that this is a performance regression from 1dae65d (Django 5.1) (refs ticket-34657)
You might want to ref this ticket/commit in some commit messages
if (count is None and real_count > 0) or ( | ||
(count is not None and real_count == count) | ||
): | ||
return |
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.
By doing these early returns, we have already done the work of whether the assertion should fail and could just fail rather than doing the comparisons. This could look something like:
--- a/django/test/testcases.py
+++ b/django/test/testcases.py
@@ -606,23 +606,16 @@ class SimpleTestCase(unittest.TestCase):
text_repr = self._text_repr(text, force_string=html)
if count is not None:
- self.assertEqual(
- real_count,
- count,
- (
- f"{msg_prefix}Found {real_count} instances of {text_repr} "
- f"(expected {count}) in the following response\n"
- f"{response_content!r}"
- ),
+ msg = (
+ f"Found {real_count} instances of {text_repr} (expected {count}) in "
+ f"the following response\n{response_content!r}"
)
else:
- self.assertTrue(
- real_count != 0,
- (
- f"{msg_prefix}Couldn't find {text_repr} in the following response\n"
- f"{response_content!r}"
- ),
- ),
+ msg = (
+ f"Couldn't find {text_repr} in the following response\n"
+ f"{response_content!r}"
+ )
+ self.fail(f"{msg_prefix}{msg}")
def assertNotContains(
self, response, text, status_code=200, msg_prefix="", html=False
@@ -1012,15 +1005,9 @@ class SimpleTestCase(unittest.TestCase):
f"Found {real_count} instances of {needle!r} (expected {count}) in "
f"the following response\n{haystack_repr}"
)
- self.assertEqual(real_count, count, f"{msg_prefix}{msg}")
else:
- self.assertTrue(
- real_count != 0,
- (
- f"{msg_prefix}Couldn't find {needle!r} in the following response\n"
- f"{haystack_repr}"
- ),
- )
+ msg = f"Couldn't find {needle!r} in the following response\n{haystack_repr}"
+ self.fail(f"{msg_prefix}{msg}")
def assertNotInHTML(self, needle, haystack, msg_prefix=""):
self.assertInHTML(needle, haystack, count=0, msg_prefix=msg_prefix)
diff --git a/tests/test_utils/tests.py b/tests/test_utils/tests.py
index 16692500e3..99154d16ce 100644
--- a/tests/test_utils/tests.py
+++ b/tests/test_utils/tests.py
@@ -990,15 +990,14 @@ class HTMLEqualTests(SimpleTestCase):
class InHTMLTests(SimpleTestCase):
def test_needle_msg(self):
msg = (
- "False is not true : Couldn't find '<b>Hello</b>' in the following "
- "response\n'<p>Test</p>'"
+ "Couldn't find '<b>Hello</b>' in the following response\n'<p>Test</p>'"
)
with self.assertRaisesMessage(AssertionError, msg):
self.assertInHTML("<b>Hello</b>", "<p>Test</p>")
def test_msg_prefix(self):
msg = (
- "False is not true : Prefix: Couldn't find '<b>Hello</b>' in the following "
+ "Prefix: Couldn't find '<b>Hello</b>' in the following "
'response\n\'<input type="text" name="Hello" />\''
)
with self.assertRaisesMessage(AssertionError, msg):
@@ -1010,9 +1009,8 @@ class InHTMLTests(SimpleTestCase):
def test_count_msg_prefix(self):
msg = (
- "2 != 1 : Prefix: Found 2 instances of '<b>Hello</b>' (expected 1) in the "
+ "Prefix: Found 2 instances of '<b>Hello</b>' (expected 1) in the "
"following response\n'<b>Hello</b><b>Hello</b>'"
- ""
)
with self.assertRaisesMessage(AssertionError, msg):
self.assertInHTML(
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.
Thanks @sarahboyce for the review!
The only motivation for using unittest
's assertions, knowing the assertion has already failed, was to preserve the message format. But I can drop them if that's okay.
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 we can keep the assertion messages the same.
So something like:
--- a/django/test/testcases.py
+++ b/django/test/testcases.py
@@ -606,23 +606,17 @@ class SimpleTestCase(unittest.TestCase):
text_repr = self._text_repr(text, force_string=html)
if count is not None:
if count is not None:
- self.assertEqual(
- real_count,
- count,
- (
- f"{msg_prefix}Found {real_count} instances of {text_repr} "
- f"(expected {count}) in the following response\n"
- f"{response_content!r}"
- ),
+ msg = (
+ f"{real_count} != {count} : {msg_prefix}Found {real_count} instances "
+ f"of {text_repr} (expected {count}) in the following response\n"
+ f"{response_content!r}"
)
else:
- self.assertTrue(
- real_count != 0,
- (
- f"{msg_prefix}Couldn't find {text_repr} in the following response\n"
- f"{response_content!r}"
- ),
- ),
+ msg = (
+ f"False is not true : {msg_prefix}Couldn't find {text_repr} in the "
+ f"following response\n{response_content!r}"
+ )
+ self.fail(msg)
def assertNotContains(
self, response, text, status_code=200, msg_prefix="", html=False
@@ -639,9 +633,9 @@ class SimpleTestCase(unittest.TestCase):
if real_count != 0:
text_repr = self._text_repr(text, force_string=html)
self.fail(
- f"{msg_prefix}{text_repr} unexpectedly found in the following response"
- f"\n{response_content!r}"
- ),
+ f"{real_count} != {0} : {msg_prefix}{text_repr} unexpectedly found in "
+ f"the following response\n{response_content!r}"
+ )
def _check_test_client_response(self, response, attribute, method_name):
"""
@@ -1012,14 +1006,11 @@ class SimpleTestCase(unittest.TestCase):
f"Found {real_count} instances of {needle!r} (expected {count}) in "
f"the following response\n{haystack_repr}"
)
- self.assertEqual(real_count, count, f"{msg_prefix}{msg}")
+ self.fail(f"{real_count} != {count} : {msg_prefix}{msg}")
else:
- self.assertTrue(
- real_count != 0,
- (
- f"{msg_prefix}Couldn't find {needle!r} in the following response\n"
- f"{haystack_repr}"
- ),
+ self.fail(
+ f"False is not true : {msg_prefix}Couldn't find {needle!r} in the "
+ f"following response\n{haystack_repr}"
)
I noticed that you had updated assertNotContains
from an assertEqual
to a fail
, which has updated the message format but no test has failed so perhaps we want to make sure all of the failure messages are covered by tests
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 we can keep the assertion messages the same.
Yes, that would work... How do you feel about preserving f"{real_count} != {count}
, etc., which sort of summarize the failure, and OTH dropping False is not true
which doesn't add any value to the message?
perhaps we want to make sure all of the failure messages are covered by tests
I did an audit and added tests in #19588 👍🏾. Let me know if you think we should also add for cases like [] is not true
and False is not true
(which I am inclined to replace with self.fail()
in this PR).
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 preserving f"{real_count} != {count}, etc., which sort of summarize the failure, and OTH dropping False is not true which doesn't add any value to the message?
I don't think either prefix adds too much value given the messages that follow. For someone to be relying on our failure message would be strange.
So I'm open to these changing, but would appreciate more opinions (maybe @adamchainz @nessita )
…sertNotContains only on test failure. Thanks to Adam Johnson for the report. Performance regression in 1dae65d. Co-authored-by: Sarah Boyce <42296566+sarahboyce@users.noreply.github.com>
Thanks to Adam Johnson for the report and to Sarah Boyce for the review.
I took a stab at adding benchmarks in django/django-asv#91. |
Trac ticket number
ticket-35728
Branch description
There are two main approaches used.
False is not true
to an error message.2. Compute the error message in the failing conditional branch, forself.fail(msg)
.A
additional(now removed) commit db0cfda makes sure that we use__contains__
for checking existence of an element in HTML assertions. This has the benefit of usingElement._count(elem, count=False)
under the hood, and short-circuiting when a single match is found.Checklist
main
branch.