Why don't my Django unittests know that MessageMiddleware is installed?
Asked Answered
C

9

44

I'm working on a Django project and am writing unittests for it. However, in a test, when I try and log a user in, I get this error:

MessageFailure: You cannot add messages without installing django.contrib.messages.middleware.MessageMiddleware

Logging in on the actual site works fine -- and a login message is displayed using the MessageMiddleware.

In my tests, if I do this:

from django.conf import settings
print settings.MIDDLEWARE_CLASSES

Then it outputs this:

('django.middleware.cache.UpdateCacheMiddleware',
'django.middleware.common.CommonMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'django.middleware.cache.FetchFromCacheMiddleware',
'debug_toolbar.middleware.DebugToolbarMiddleware')

Which appears to show the MessageMiddleware is installed when tests are run.

Is there an obvious step I'm missing?

UPDATE

After suggestions below, it does look like it's a settings thing.

I currently have settings/__init__.py like this:

try:
    from settings.development import *
except ImportError:
    pass

and settings/defaults.py containing most of the standard settings (including MIDDLEWARE_CLASSES). And then settings.development.py overrides some of those defaults like this:

from defaults import *

DEBUG = True
# etc

It looks like my dev site itself works fine, using the development settings. But although the tests seem to load the settings OK (both defaults and development) settings.DEBUG is set to False. I don't know why, or whether that's the cause of the problem.

Comras answered 13/8, 2012 at 16:11 Comment(2)
Were you able to resolve your issue. If so, can you please share how? I am facing the same thing, running the latest Django 1.6 from the git repo.Kast
I've ended up trying to make my tests work around it, which so far has meant any tricky tests that bring up this issue just end up not written. Which isn't ideal. Good luck.Comras
S
72

Django 1.4 has a expected behavior when you create the request with RequestFactory that can trigger this error.

To resolve this issue, create your request with RequestFactory and do this:

from django.contrib.messages.storage.fallback import FallbackStorage
setattr(request, 'session', 'session')
messages = FallbackStorage(request)
setattr(request, '_messages', messages)

Works for me!

Shulman answered 17/8, 2012 at 19:18 Comment(12)
It's actually no bug but expected behaviourHospers
This should get into the docs in the "testing messages" sectionPrairial
@StephanHoyer While the behaviour (of failing) may be expected, it seems the error message itself is quite misleading :-(Cleric
Still working in Django 1.6. This has saved my day, thank you ;)Santoyo
Still working in Django 1.8 with LiveServerTestCase -- a great fix when testing with Selenium. thanks!Tinkling
This works until I try to create a key/value within session, which causes a 'str' object does not support item assignment... couldn't the 2nd line be setattr(request, 'session', {})? That way you just have an empty dict ready to work with, rather than an immutable str.Tinkling
I am using Django==1.11.6. And I have the similar issue. Is this bug still present?Sessoms
is still present in 1.8.Horripilate
and still present in 2.0.6Schaffhausen
With Django 2.2.5 too, it's ok. ThanksHighlander
It still work with django3.1.1. Thanks man it saves my time.Aiello
and still present in Django 4.0.2Trepang
H
5

A way to solve this quite elegant is to mock the messages module using mock

Say you have a class based view named FooView in app named myapp

from django.contrib import messages
from django.views.generic import TemplateView

class FooView(TemplateView):
    def post(self, request, *args, **kwargs):
        ...
        messages.add_message(request, messages.SUCCESS, '\o/ Profit \o/')
        ...

You now can test it with

def test_successful_post(self):
    mock_messages = patch('myapp.views.FooView.messages').start()
    mock_messages.SUCCESS = success = 'super duper'
    request = self.rf.post('/', {})
    view = FooView.as_view()
    response = view(request)
    msg = _(u'\o/ Profit \o/')
    mock_messages.add_message.assert_called_with(request, success, msg)
Hospers answered 1/2, 2013 at 12:31 Comment(0)
U
1

In my case (django 1.8) this problem occurs in when unit-test calls signal handler for user_logged_in signal, looks like messages app has not been called, i.e. request._messages is not yet set. This fails:

from django.contrib.auth.signals import user_logged_in
...

@receiver(user_logged_in)
def user_logged_in_handler(sender, user, request, **kwargs):

    ...
    messages.warning(request, "user has logged in")

the same call to messages.warning in normal view function (that is called after) works without any issues.

A workaround I based on one of the suggestions from https://code.djangoproject.com/ticket/17971, use fail_silently argument only in signal handler function, i.e. this solved my problem:

messages.warning(request, "user has logged in",
                 fail_silently=True )
Uncinus answered 8/2, 2018 at 13:55 Comment(0)
U
0

Do you only have one settings.py?

Uveitis answered 13/8, 2012 at 16:23 Comment(1)
I have one settings file importing some defaults. More info added to the question.Comras
T
0

Tests create custom (tests) database. Maybe you have no messages there or something... Maybe you need setUp() fixtures or something?

Need more info to answer properly.

Why not simply do something like ? You sure run tests in debug mode right?

# settings.py
DEBUG = True

from django.conf import settings
# where message is sent:
if not settings.DEBUG:
    # send your message ... 
Tamera answered 13/8, 2012 at 17:24 Comment(1)
I've added more info about my settings to the question. Seems to be importing them, and yet DEBUG is False, when it's True in the settings.Comras
T
0

This builds on Tarsis Azevedo's answer by creating a MessagingRequest helper class below.

Given say a KittenAdmin I'd want to get 100% test coverage for:

from django.contrib import admin, messages

class KittenAdmin(admin.ModelAdmin):
    def warm_fuzzy_method(self, request):
        messages.warning(request, 'Can I haz cheezburger?')

I created a MessagingRequest helper class to use in say a test_helpers.py file:

from django.contrib.messages.storage.fallback import FallbackStorage
from django.http import HttpRequest

class MessagingRequest(HttpRequest):
    session = 'session'

    def __init__(self):
        super(MessagingRequest, self).__init__()
        self._messages = FallbackStorage(self)

    def get_messages(self):
        return getattr(self._messages, '_queued_messages')

    def get_message_strings(self):
        return [str(m) for m in self.get_messages()]

Then in a standard Django tests.py:

from django.contrib.admin.sites import AdminSite
from django.test import TestCase

from cats.kitten.admin import KittenAdmin
from cats.kitten.models import Kitten
from cats.kitten.test_helpers import MessagingRequest

class KittenAdminTest(TestCase):
    def test_kitten_admin_message(self):
        admin = KittenAdmin(model=Kitten, admin_site=AdminSite())
        expect = ['Can I haz cheezburger?']
        request = MessagingRequest()
        admin.warm_fuzzy_method(request)
        self.assertEqual(request.get_message_strings(), expect)

Results:

coverage run --include='cats/kitten/*' manage.py test; coverage report -m
Creating test database for alias 'default'...
.
----------------------------------------------------------------------
Ran 1 test in 0.001s

OK
Destroying test database for alias 'default'...
Name                                     Stmts   Miss  Cover   Missing
----------------------------------------------------------------------
cats/kitten/__init__.py                      0      0   100%   
cats/kitten/admin.py                         4      0   100%   
cats/kitten/migrations/0001_initial.py       5      0   100%   
cats/kitten/migrations/__init__.py           0      0   100%   
cats/kitten/models.py                        3      0   100%   
cats/kitten/test_helpers.py                 11      0   100%   
cats/kitten/tests.py                        12      0   100%   
----------------------------------------------------------------------
TOTAL                                       35      0   100%   
Tracee answered 21/4, 2016 at 3:33 Comment(0)
I
0

This happened to me in the login_callback signal receiver function when called from a unit test, and the way around the problem was:

from django.contrib.messages.storage import default_storage

@receiver(user_logged_in)
def login_callback(sender, user, request, **kwargs):
    if not hasattr(request, '_messages'):  # fails for tests
        request._messages = default_storage(request)

Django 2.0.x

Instal answered 15/9, 2018 at 20:37 Comment(0)
C
0

I found when I had a problem patching messages the solution was to patch the module from within the class under test (obsolete Django version BTW, YMMV). Pseudocode follows.

my_module.py:

from django.contrib import messages


class MyClass:

    def help(self):
        messages.add_message(self.request, messages.ERROR, "Foobar!")

test_my_module.py:

from unittest import patch, MagicMock
from my_module import MyClass


class TestMyClass(TestCase):

    def test_help(self):
        with patch("my_module.messages") as mock_messages:
            mock_messages.add_message = MagicMock()
            MyClass().help()  # shouldn't complain about middleware
Compagnie answered 9/7, 2019 at 11:16 Comment(0)
C
-1

If you're seeing a problem in your Middleware, then you're not doing "Unit Test". Unit tests test a unit of functionality. If you interact with other parts of your system, you're making something called "integration" testing.

You should try to write better tests, and this kind of problems shouldn't arise. Try RequestFactory. ;)

def test_some_view(self):
    factory = RequestFactory()
    user = get_mock_user()
    request = factory.get("/my/view")
    request.user = user
    response = my_view(request)
    self.asssertEqual(status_code, 200)
Contredanse answered 13/8, 2012 at 17:34 Comment(5)
I don't quite understand that, or how it helps in this instance, but I'll read those docs a few more times and hopefully it'll sink in! Thanks.Comras
Ah, I think it's clicked. Only problem - I'm using Class Based Views and I'm not sure how to use that instead of my_view(request)... any pointers?Comras
Got it: MyDetailView.as_view()(request, slug='my-item-slug'). Seems odd to have to specify the slug in the factory.get() too, but this seems to work, thanks!Comras
You are using CMS maybe? Have you copied the DB data with pages into your tests database?Tamera
Admin actions are functions or methods that take a request as parameter that you don't want to break down because it is simple enough or because the result should be stupidly more complex (call a sub-function - the one you could actually test w/o request - returning a list of (u"message foo", LOG_LEVEL) and process them back into the original function). This is not much better in the end...Chiaki

© 2022 - 2024 — McMap. All rights reserved.