﻿id	summary	reporter	owner	description	type	status	component	version	severity	resolution	keywords	cc	stage	has_patch	needs_docs	needs_tests	needs_better_patch	easy	ui_ux
24721	"Add ""test extensions"" (e.g. something similar to mail.outbox for messages)"	Marc Tamlyn	nobody	"'''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."	New feature	closed	Testing framework	dev	Normal	duplicate		alasdair@…	Accepted	1	1	0	1	0	0
