diff --git a/pytest_django/plugin.py b/pytest_django/plugin.py index 08d961a6..e8e629f4 100644 --- a/pytest_django/plugin.py +++ b/pytest_django/plugin.py @@ -599,7 +599,8 @@ def _dj_autoclear_mailbox() -> None: from django.core import mail - del mail.outbox[:] + if hasattr(mail, "outbox"): + mail.outbox.clear() @pytest.fixture() @@ -608,12 +609,13 @@ def mailoutbox( _dj_autoclear_mailbox: None, ) -> list[django.core.mail.EmailMessage] | None: """A clean email outbox to which Django-generated emails are sent.""" - if not django_settings_is_configured(): - return None + skip_if_no_django() from django.core import mail - return mail.outbox # type: ignore[no-any-return] + if hasattr(mail, "outbox"): + return mail.outbox # type: ignore[no-any-return] + return [] @pytest.fixture() diff --git a/tests/conftest.py b/tests/conftest.py index 16e209f1..e3bfa1f4 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -30,11 +30,13 @@ def _marker_apifun( extra_settings: str = "", create_manage_py: bool = False, project_root: str | None = None, + create_settings: bool = True, ): return { "extra_settings": extra_settings, "create_manage_py": create_manage_py, "project_root": project_root, + "create_settings": create_settings, } @@ -135,14 +137,18 @@ def django_pytester( # Copy the test app to make it available in the new test run shutil.copytree(str(app_source), str(test_app_path)) - tpkg_path.joinpath("the_settings.py").write_text(test_settings) + if options["create_settings"]: + tpkg_path.joinpath("the_settings.py").write_text(test_settings) # For suprocess tests, pytest's `pythonpath` setting doesn't currently # work, only the envvar does. pythonpath = os.pathsep.join(filter(None, [str(REPOSITORY_ROOT), os.getenv("PYTHONPATH", "")])) monkeypatch.setenv("PYTHONPATH", pythonpath) - monkeypatch.setenv("DJANGO_SETTINGS_MODULE", "tpkg.the_settings") + if options["create_settings"]: + monkeypatch.setenv("DJANGO_SETTINGS_MODULE", "tpkg.the_settings") + else: + monkeypatch.delenv("DJANGO_SETTINGS_MODULE", raising=False) def create_test_module(test_code: str, filename: str = "test_the_test.py") -> Path: r = tpkg_path.joinpath(filename) diff --git a/tests/test_fixtures.py b/tests/test_fixtures.py index 39c6666f..f88ed802 100644 --- a/tests/test_fixtures.py +++ b/tests/test_fixtures.py @@ -825,3 +825,62 @@ def mocked_make_msgid(*args, **kwargs): result = django_pytester.runpytest_subprocess("--tb=short", "-vv", "-s") result.stdout.fnmatch_lines(["*test_mailbox_inner*", "django_mail_dnsname_mark", "PASSED*"]) assert result.ret == 0 + + +@pytest.mark.django_project( + create_manage_py=True, + extra_settings=""" + EMAIL_BACKEND = "django.core.mail.backends.dummy.EmailBackend" + """, +) +def test_mail_auto_fixture_misconfigured(django_pytester: DjangoPytester) -> None: + """ + django_test_environment fixture can be overridden by user, and that would break mailoutbox fixture. + + Normally settings.EMAIL_BACKEND is set to "django.core.mail.backends.locmem.EmailBackend" by django, + along with mail.outbox = []. If this function doesn't run for whatever reason, the + mailoutbox fixture will not work properly. + """ + django_pytester.create_test_module( + """ + import pytest + + @pytest.fixture(autouse=True, scope="session") + def django_test_environment(request): + yield + """, + filename="conftest.py", + ) + + django_pytester.create_test_module( + """ + def test_with_fixture(settings, mailoutbox): + assert mailoutbox == [] + assert settings.EMAIL_BACKEND == "django.core.mail.backends.dummy.EmailBackend" + + def test_without_fixture(): + from django.core import mail + assert not hasattr(mail, "outbox") + """ + ) + result = django_pytester.runpytest_subprocess() + result.assert_outcomes(passed=2) + + +@pytest.mark.django_project(create_settings=False) +def test_no_settings(django_pytester: DjangoPytester) -> None: + django_pytester.create_test_module( + """ + def test_skipped_settings(settings): + assert False + + def test_skipped_mailoutbox(mailoutbox): + assert False + + def test_mail(): + from django.core import mail + assert not hasattr(mail, "outbox") + """ + ) + result = django_pytester.runpytest_subprocess() + result.assert_outcomes(passed=1, skipped=2)