#24721: Introduce something similar to mail.outbox for messages
--------------------------------------------+------------------------
               Reporter:  mjtamlyn          |          Owner:  nobody
                   Type:  New feature       |         Status:  new
              Component:  contrib.messages  |        Version:  master
               Severity:  Normal            |       Keywords:
           Triage Stage:  Unreviewed        |      Has patch:  0
    Needs documentation:  0                 |    Needs tests:  0
Patch needs improvement:  0                 |  Easy pickings:  0
                  UI/UX:  0                 |
--------------------------------------------+------------------------
 '''TL;DR'''
 - Testing messages is tricky, and depends on the choice of storage used
 - Testing email has been abstracted nicely, let's copy that
 - Doing so from a contrib app is hard, let's leverage app configs as a
 place for hooks for contrib apps to inject their own testing needs into
 Django's normal test environment

 '''The long version'''

 When testing emails with Django, it automatically changes your email
 backend to a clever in memory one, and exposes {{{mail.outbox}}} which
 will contain any emails sent during that test and tidy itself up
 afterwards for you.

 With messages we have no such machinery. If you want to check a message
 has been sent on a GET this is quite straightforwards as you can simply
 check {{{response.context['messages']}}} for it. However with POST
 requests it is rather more complex, especially if you use
 {{{assertRedirects}}} in it's normal form - the check of the destination
 of the redirect will remove the message!

 As an example, consider this test, which is based on an existing test
 ({{{messages.tests.test_mixins.SuccessMessageMixinTests}}}):
 {{{
 from django.core.urlresolvers import reverse
 from django.test import TestCase, override_settings


 @override_settings(ROOT_URLCONF='messages_tests.urls')
 class TestRedirectionMessage(TestCase):
     def test_simple(self):
         author = {'name': 'John Doe',
                   'slug': 'success-msg'}
         add_url = reverse('add_success_msg')
         response = self.client.post(add_url, author)
         # response.context['messages'] does not exist as response is a 302
         self.assertRedirects(response, reverse('show_message'))
         # Message is now gone, even if I were to make another GET request.
 }}}

 In this case, the original test inspects {{{response.cookies}}} for the
 302 response, which is fairly easy, but it's more complex with other
 message storages. If you use the default {{{FallbackStorage}}}, you would
 have to change your test if the message got too long to inspect the
 session storage instead of the cookie...

 Here's the version of that test I would like:
 {{{
     def test_simple(self):
         author = {'name': 'John Doe',
                   'slug': 'success-msg'}
         add_url = reverse('add_success_msg')
         response = self.client.post(add_url, author)
         self.assertRedirects(response, reverse('show_message'))
         self.assertEqual(len(messages.outbox), 1)
         self.assertEqual(messages.outbox[0].message, 'John Doe was created
 successfully')
         self.assertEqual(messages.outbox[0].level, messages.SUCCESS)
 }}}

 The use of the name {{{outbox}}} here is somewhat strange I admit, but as
 yet I haven't found an alternative.

 The actual implementation detail is fairly straightforwards in some sense
 - create a new {{{InMemoryStorage}}} with the same basic infrastructure as
 the corresponding email backend. The difficult part is that due to the
 fact this is a contrib app. The engineering to make the email backend work
 takes place in two distinct locations - first in the
 {{{setup_test_environment}}} to initialise it, and then in
 {{{TestCase._pre_setup}}} to reset before each test.

 The best proposal I have for designing this is to add some optional
 methods to the {{{AppConfig}}} class which are hooks called by both of
 these methods. This would also be extremely useful for other packages
 which interact with external resources and/or have alternate backends. For
 example, django-redis could use these hooks to switch to another redis
 database and automatically clear it before each test (like we do with
 other data stores), or django-celery could automatically switch to using
 an in-memory broker and provide a good api for checking that the task has
 been queued, then flushing the queue later if desired, if not then reset
 the queue after each test. This is likely to be more expressive and
 performant than {{{CELERY_ALWAYS_EAGER = True}}}.

 Backwards compatibility with existing tests which expect the request to
 have been modified with messages is an issue, I suspect this may need to
 be phased in, or set as non default. This could be done via an alternative
 app config if this is the route we choose to go.

--
Ticket URL: <https://code.djangoproject.com/ticket/24721>
Django <https://code.djangoproject.com/>
The Web framework for perfectionists with deadlines.

-- 
You received this message because you are subscribed to the Google Groups 
"Django updates" group.
To unsubscribe from this group and stop receiving emails from it, send an email 
to [email protected].
To post to this group, send email to [email protected].
To view this discussion on the web visit 
https://groups.google.com/d/msgid/django-updates/051.90aa664cf7c24b6588fc14e4c7308f0e%40djangoproject.com.
For more options, visit https://groups.google.com/d/optout.

Reply via email to