#36308 closed Bug (needsinfo)
Parallel testing fails reporting unit test combining subTest and assertNumQueries
Reported by: | Jean Bvt | Owned by: | |
---|---|---|---|
Component: | Testing framework | Version: | 5.2 |
Severity: | Normal | Keywords: | test assertNumQuery reporting |
Cc: | David Winiecki, Jacob Walls | Triage Stage: | Unreviewed |
Has patch: | no | Needs documentation: | no |
Needs tests: | no | Patch needs improvement: | no |
Easy pickings: | no | UI/UX: | no |
Description (last modified by )
Updating from django 5.1.8 to 5.2.0, our test suit fails to correctly reports failed test.
python manage.py test app.module.tests --parallel 8 Found 9 test(s). [...] Destroying test database for alias 'default'... Destroying test database for alias 'default'... Destroying test database for alias 'default'... Traceback (most recent call last): File "/app/manage.py", line 23, in <module> main() File "/app/manage.py", line 19, in main execute_from_command_line(sys.argv) File "/app/.venv/lib/python3.12/site-packages/django/core/management/__init__.py", line 442, in execute_from_command_line utility.execute() File "/app/.venv/lib/python3.12/site-packages/django/core/management/__init__.py", line 436, in execute self.fetch_command(subcommand).run_from_argv(self.argv) File "/app/.venv/lib/python3.12/site-packages/django/core/management/commands/test.py", line 24, in run_from_argv super().run_from_argv(argv) File "/app/.venv/lib/python3.12/site-packages/django/core/management/base.py", line 416, in run_from_argv self.execute(*args, **cmd_options) File "/app/.venv/lib/python3.12/site-packages/django/core/management/base.py", line 460, in execute output = self.handle(*args, **options) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/app/.venv/lib/python3.12/site-packages/django/core/management/commands/test.py", line 63, in handle failures = test_runner.run_tests(test_labels) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/app/.venv/lib/python3.12/site-packages/django/test/runner.py", line 1099, in run_tests result = self.run_suite(suite) ^^^^^^^^^^^^^^^^^^^^^ File "/app/.venv/lib/python3.12/site-packages/django/test/runner.py", line 1026, in run_suite return runner.run(suite) ^^^^^^^^^^^^^^^^^ File "/usr/local/lib/python3.12/unittest/runner.py", line 240, in run test(result) File "/usr/local/lib/python3.12/unittest/suite.py", line 84, in __call__ return self.run(*args, **kwds) ^^^^^^^^^^^^^^^^^^^^^^^ File "/app/.venv/lib/python3.12/site-packages/django/test/runner.py", line 553, in run subsuite_index, events = test_results.next(timeout=0.1) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/usr/local/lib/python3.12/multiprocessing/pool.py", line 873, in next raise value multiprocessing.pool.MaybeEncodingError: Error sending result: '(17, [('startTest', 0), ('addDuration', 0, 0.02817973299897858), ('addSuccess', 0), ('stopTest', 0), ('startTest', 1), ('addDuration', 1, 0.02015996399859432), ('addSuccess', 1), ('stopTest', 1), ('startTest', 2), ('addDuration', 2, 0.024580582001362927), ('addSuccess', 2), ('stopTest', 2), ('startTest', 3), ('addSubTest', 3, <unittest.case._SubTest testMethod=runTest>, (<class 'AssertionError'>, AssertionError('3 != 2 : 3 queries executed, 2 expected\nCaptured queries were:\n1. SELECT "app_tokenapi"."id", "app_tokenapi"."user_id", "app_tokenapi"."key", "app_tokenapi"."name", "app_tokenapi"."scope_level", "app_tokenapi"."created_at", "app_tokenapi"."expired_at", "app_tokenapi"."expire_reason", "auth_user"."id", "auth_user"."password", "auth_user"."last_login", "auth_user"."is_superuser", "auth_user"."username", "auth_user"."first_name", "auth_user"."last_name", "auth_user"."email", "auth_user"."is_staff", "auth_user"."is_active", "auth_user"."date_joined" FROM "app_tokenapi" INNER JOIN "auth_user" ON ("app_tokenapi"."user_id" = "auth_user"."id") WHERE "app_tokenapi"."key" = \'abcdabcd-abcd-abcd-abcd-abcdabcd\' ORDER BY "app_tokenapi"."id" ASC LIMIT 1\n2. SELECT "my_models_entite"."id", "my_models_entite"."deleted", "my_models_entite"."deleted_by_cascade", "my_models_entite"."some_id", "my_models_entite"."parent_id", "my_models_entite"."type_id", "my_models_entite"."code", "my_models_entite"."libelle", "my_models_entite"."libelle_court", "my_models_entite"."url_logo", "my_models_entite"."begin_date", "my_models_entite"."end_date", "my_models_entite"."created_at", "my_models_entite"."modified_at", "my_models_entite"."json", "my_models_entite"."allow_externe_id", "my_models_entitetype"."id", "my_models_entitetype"."name", "my_models_entitetype"."id_rfu_link", "my_models_entitetype"."id_imotep_link", "my_models_entitetype"."created_at", "my_models_entitetype"."modified_at" FROM "my_models_entite" INNER JOIN "my_models_entitetype" ON ("my_models_entite"."type_id" = "my_models_entitetype"."id") WHERE "my_models_entite"."deleted" IS NULL ORDER BY "my_models_entite"."libelle" ASC\n3. SELECT "my_models_entite"."id", "my_models_entite"."deleted", "my_models_entite"."deleted_by_cascade", "my_models_entite"."some_id", "my_models_entite"."parent_id", "my_models_entite"."type_id", "my_models_entite"."code", "my_models_entite"."libelle", "my_models_entite"."libelle_court", "my_models_entite"."url_logo", "my_models_entite"."begin_date", "my_models_entite"."end_date", "my_models_entite"."created_at", "my_models_entite"."modified_at", "my_models_entite"."json", "my_models_entite"."allow_externe_id" FROM "my_models_entite" WHERE ("my_models_entite"."id") IN ((54), (NULL), (59), (62), (56), (55), (58), (57))'), <traceback object at 0x744f93f72e80>)), ('addDuration', 3, 0.032738704001531005), ('stopTest', 3), ('startTest', 4), ('addDuration', 4, 0.01990954599750694), ('addSuccess', 4), ('stopTest', 4), ('startTest', 5), ('addDuration', 5, 0.10901878799995757), ('addSuccess', 5), ('stopTest', 5), ('startTest', 6), ('addDuration', 6, 0.01989884100112249), ('addSuccess', 6), ('stopTest', 6)])'. Reason: 'AttributeError("Can't get local object 'convert_exception_to_response.<locals>.inner'")' 6998993075), ('addSuccess', 6), ('stopTest', 6)])'. Reason: 'AttributeError("Can't get local object 'convert_exception_to_response.<locals>.inner'")'
(I've renamed some name in this SQL queries for privacy reason)
# the culprit test
def test_queryset_performances(self): EntiteFactory.create() with self.subTest("sub test"): with self.assertNumQueries(2): response = self.client.get(self.url)
# Reproductibility
This errors does not happen if
- We call only this test or only its TestCase (9 tests)
self.client.get(self.url)
is not called,assertNumQueries
still raise AssertionError but reporting works fine.- replacing
self.client.get(...)
with a simpler QuerySet call still raise AssertionError but reporting works fine. - we remove
self.subTest(...)
The problem does not appear in django 5.1.8 because the test succeeded in 5.1.8, it seems that 5.2 unexpectedly raised the number of query from 2 to 3 (notice the third query look redundant with the second query) so the core issue with reporting may be older than 5.2.
Change History (6)
comment:1 by , 6 months ago
Description: | modified (diff) |
---|
follow-up: 3 comment:2 by , 6 months ago
Resolution: | → needsinfo |
---|---|
Status: | new → closed |
comment:3 by , 6 months ago
Replying to Sarah Boyce:
I cannot reproduce, but I think you would need to share your view here.
Does the view raise an error on it's own?
I tried:
tests/test_utils/tests.py
diff --git a/tests/test_utils/tests.py b/tests/test_utils/tests.py index 16692500e3..2b2c3cf2bf 100644
a b class AssertNumQueriesTests(TestCase): 237 237 238 238 self.assertNumQueries(2, test_func) 239 239 240 def test_assert_num_queries_with_client_and_subtest(self): 241 person = Person.objects.create(name="test") 242 with self.subTest("sub test"): 243 with self.assertNumQueries(2): 244 self.client.get("/test_utils/get_person/%s/" % person.pk) 245 240 246 241 247 class AssertNumQueriesUponConnectionTests(TransactionTestCase): And running
./runtests.py test_utils --parallel 2
but this failed in an expected manner.
The view does not raise an exception, the raised exception contains a message related to AssertNumQueries:
(<class 'AssertionError'>, AssertionError('3 != 2 : 3 queries executed, 2 expected\nCaptured queries were:\n1. SELECT "app_tokenapi"."id", "app_tokenapi"."user_id", "app_tokenapi"."key", "app_tokenapi"."name", "app_tokenapi"."scope_level", "app_tokenapi"."created_at", "app_tokenapi"."expired_at", "app_tokenapi"."expire [...]
making me believe its the reporting that failed.
Anyway, here is my view (and my serializer since I am using DRF)
@extend_schema(tags=["entite", "v2"]) class EntitesView(ListAPIView): permission_classes = [AllowAny] queryset = Entite.objects.prefetch_related("parent").select_related("type") serializer_class = EntiteSerializer logger_instance = logging.getLogger("entites.entites") filter_backends = (DjangoFilterBackend,) filterset_class = EntitesFilter
from rest_framework.serializers import ModelSerializer from rest_framework.relations import SlugRelatedField from my_models.models.entite import Entite, EntiteType from rest_framework import serializers class EntiteSerializer(ModelSerializer): parent = SlugRelatedField( slug_field="uness_id", queryset=Entite.objects.all(), error_messages={ "does_not_exist": "L'identifiant {value} ne correspond à aucune entité." }, required=False, ) type = SlugRelatedField( slug_field="name", queryset=EntiteType.objects.all(), error_messages={ "does_not_exist": "Le type {value} ne correspond à aucun type d'entité." }, ) allow_externe_id = serializers.SerializerMethodField() def get_allow_externe_id(self, obj): return obj.allow_externe_id class Meta: model = Entite fields = ( "allow_externe_id", "code", "json", "libelle_court", "libelle", "parent", "type", "uness_id", "id", ) read_only_fields = [ "code", "libelle_court", "libelle", "parent", "type", "uness_id", "id", ]
comment:4 by , 6 months ago
Cc: | added |
---|
follow-up: 6 comment:5 by , 6 months ago
The problem does not appear in django 5.1.8 because the test succeeded in 5.1.8, it seems that 5.2 unexpectedly raised the number of query from 2 to 3 (notice the third query look redundant with the second query) so the core issue with reporting may be older than 5.2.
The first query looks related to auth, and queries 2 and 3 are as I would expect when using .prefetch_related()
. If you didn't see that third query before, it seems your test data may have changed. Django will not execute the .prefetch_related("parent")
if the queryset it is chained to returned no results. You could verify this quickly by counting Entite.objects.all()
at the top of your test and let us know if that varies between 5.1.8 and 5.2.
Seeing your TestCase would be helpful. For instance, does it subclass TestCase
or TransactionTestCase
? Do you have a setUpClass()
method?
Finally, a git bisect
would be very helpful here. You can clone the django repo, pip install -e path-to-local-copy-of-django
, run your test case, and in a matter of minutes report the commit that caused the symptom of the problem to appear.
comment:6 by , 6 months ago
Replying to Jacob Walls:
The problem does not appear in django 5.1.8 because the test succeeded in 5.1.8, it seems that 5.2 unexpectedly raised the number of query from 2 to 3 (notice the third query look redundant with the second query) so the core issue with reporting may be older than 5.2.
The first query looks related to auth, and queries 2 and 3 are as I would expect when using
.prefetch_related()
. If you didn't see that third query before, it seems your test data may have changed. Django will not execute the.prefetch_related("parent")
if the queryset it is chained to returned no results. You could verify this quickly by countingEntite.objects.all()
at the top of your test and let us know if that varies between 5.1.8 and 5.2.
Seeing your TestCase would be helpful. For instance, does it subclass
TestCase
orTransactionTestCase
? Do you have asetUpClass()
method?
Finally, a
git bisect
would be very helpful here. You can clone the django repo,pip install -e path-to-local-copy-of-django
, run your test case, and in a matter of minutes report the commit that caused the symptom of the problem to appear.
Thank you for your reply, I'll try to do all that once I've some time
I cannot reproduce, but I think you would need to share your view here.
Does the view raise an error on it's own?
I tried:
tests/test_utils/tests.py
And running
./runtests.py test_utils --parallel 2
but this failed in an expected manner.