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
Adding mailoutbox fixture, and removing internal _django_clear_outbox #410
Conversation
f76127d
to
cc396bc
Compare
This change would break existing test suites which depend on having a clean I'd prefer to recommend from django.core import mail
@pytest.fixture
def outbox():
return mail.outbox Closing this PR, please leave a comment and reopen it if there is another angle of this PR that I missed! |
@pelme Are you refering to test_mail and test_mail_again in the test suite? I see this as an hidden functionality, as it is an "private" autouse fixture that is not documented. Wouldn't it be more clear for the user, if pytest-django provides the "outbox" fixture it self, and that fixture handles whatever "magic" needed to clear the mail.outbox? And of course documented :) |
|
@pelme, any comments? |
Agreed, it is cleaner and probably the correct approach. We just need to think about the backward compatibility. (I have a bunch of tests that depend on this myself (maybe it's only me since I wrote that fixture :)):
) I'm fine with getting rid of the old way as long as we provide some kind of deprecation warning and not just break test suites that depend on the old behavior. |
@pelme I'll fix it... |
…mail.outbox really is mailoutbox fixture
cc396bc
to
19f92f6
Compare
@pelme , in my branch:
|
Related to deprecation warning, we should just issue one warning, so we could do that somewhere in setup of the plugin. I don't know where it is best practise to issue DeprecationWarnining in pytest plugins :/ |
The grep command was run from my current project that uses pytest-django, not pytest-django itself. I have about 24 places that would be broken if we merge this change as is. I'm personally fine fixing those, however a bunch of test suites probably use the same kind of pattern (even though this is not currently documented). I don't want to break their test suites (or even worse, make tests pass without failures because of emails that suddenly "exists" because they were not cleared :)). Maybe we could replace i.e., in a test
To emit a warning, pytest's |
We could as an intermediate step introduce a flag --clear-django-mailbox that enables the autouse fixture, with an FutureDepcreationWarning that it will be removed in future release of pytest-django. This means, that projects that relies on this will just need to add --clear-django-mailbox to their pytest config, and they should be fine... And we drop the functionality in later release... (or never) :) |
def test_important_email():
send_important_email()
assert mail.outbox
def test_another_important_email():
send_another_important_email()
assert mail.outbox If suddenly there is a bug in I don't think opt-in via a flag like |
… of _django_clear_outbox
I get your point with that those tests, however it isn't that pytestic to do it that way. The way how the mailoutbox fixture works is very much like the db fixtures/functions, that provides a transactional behavior that will restore the mail.outbox to the way it was before requesting the fixture. I have updated changelog to highlight the change as an important change. And I take back my suggestion to have the --clear-django-mailbox as it would encourage, in my opinion, bad practise to rely on automatic cleaning of the mail.outbox. |
I am in favor of having the mailoutbox fixture and think it is a good idea. You are correct: It is more pytestic (nice word! :)) to explicitly ask for fixtures that setup/teardown stuff you care about. I agree that those tests in my example are bad that I and should not be encouraged/used. However, those tests works today and are valid. They exists in real tests suites (my current project included). People rely on them working as the did (even though it is not pytestic). There may be test suites of 5000 tests where some of these depend on a clear mailbox to be valid. Maybe the person who wrote those test left those companies and someone else is upgrading pytest-django this year? My point is that we can avoid all of that with a small effort with a safeguard to class _OutboxError(object):
def _raise_assertion(*args, **kwargs):
raise AssertionError('Use the mailoutbox fixture')
__len__ = __getitem__ = __nonzero__ = __bool__ = append = _raise_assertion
@pytest.fixture(autouse=True)
def _error_on_mailbox_access():
from django.core import mail
old = mail.outbox
mail.outbox = _OutboxError()
yield
mail.outbox = old |
Sounds fair, but can it be guaranteed that an autouse fixture will be executed before all normal non-autouse fixtures? I hope you have deeper insight in that, as you are way more involved with pytest development than I am :) Because if this cannot be guaranteed, mailoutbox fixture might will not function if _error_on_mailbox_access fixture will be applied after mailoutbox fixture... |
It can be listed as a dependency for mailoutbox which will ensure it is run before/teared down after: @pytest.fixture
def mailoutbox(_error_on_mailbox_access):
.... |
Of course, thanks :) Implementation coming... |
This will be awesome ✨ 👍 Btw, we need to make sure Django TestCases still works without errors, _error_on_mailbox_access probably needs to check for that and avoid make sure mail.outbox works as before in those cases (Django TestCase should take care of it but we just need to not mess it up). |
…aise AssertionError if trying to access mail.outbox directly test case that validates it, and also test case that assure we don't get exception when sending email...
def test_one(self): | ||
assert len(mail.outbox) == 0 | ||
mail.send_mail('subject', 'body', 'from@example.com', ['to@example.com']) | ||
assert len(mail.outbox) == 1 |
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.
Please add test_two that does the same thing and checks to make sure the clearing works :)
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.
done
# | ||
|
||
|
||
@pytest.yield_fixture(autouse=True) |
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.
Shouldn't this be scope="session"
? If it's session-scoped then the "protection" would be on for other fixtures too.
if _old_mailbox is not None: | ||
setattr(mail, 'outbox', _old_mailbox) | ||
else: | ||
delattr(mail, 'outbox') |
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 the outbox
was actually None
(and not missing) now it would be removed. Is this acceptable behavior?
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.
mail.outbox will never be None, mail.outbox is only setup when locmem.EmailBackend is used, and setup either in constructor of locmem.EmailBackend or in django.test.util.setup_test_environment
Just pushed last update of changelog, I think this is ready for final review... |
Thanks a lot for this feature, it will be great! ✨ |
Your welcome. Thanks for an awesome pytest plugin. |
pytest-django doesn't provide any mechanism to get the mail outbox, however it is clearing the mailbox, that effects external fixtures that might want to interact with the mail.outbox. This fixture stores previous outbox (if any), creates a new list for usage in fixture, and then restores the state to what it was previously...
This PR is initiated to add a mailoutbox fixture, and if interested of getting this released, I would enhance it with documentation and possible tests...
PR #407 would not be needed if this is chosen as the path to go...