Unable to test messages in CreateView

I am writing unit tests of ebook catalog applications in Django 5. The book creation page uses the built-in CreateView. The class code looks like this:

class BookCreate(SuccessMessageMixin, PermissionRequiredMixin, CreateView):

    model = Book
    fields = ['title', 'author', 'genre', 'publisher', 'pages', 'cover', 'summary']

    template_name = 'books/add_book.html'
    permission_required = 'catalog.add_book'
    permission_denied_message = 'Not enough permissions to add a book'

    success_message = 'Book successfully added'
    success_url = reverse_lazy('add-book')

It works as follows: the user fills in the book data, clicks the submit button, then the page is redirected to the same page, plus the page displays the message specified in the success_message attribute of the class.

For testing, I use the MessagesTestMixin as described in the Django documentation. Here is the unit test code:

class TestBookCreateView(MessagesTestMixin, TestCase):

    @classmethod
    def setUpTestData(cls):
        cls.test_user = User.objects.create_user(
            email='john123@gmail.com',
            username='John',
            password='Fiesta123'
        )
        cls.add_book_permission = Permission.objects.get(codename='add_book')


    def setUp(self):
        login = self.client.login(username='Ivan', password='Fiesta123')
        self.test_user.user_permissions.set([self.add_book_permission])

    def test_if_view_success_message_attached(self):

        author = Author.objects.create(
            first_name='Charles',
            last_name='Dickens'
        )

        genre = Genre.objects.create(
            genre='Novel'
        )

        book_data = {
            'title': 'Great Expectations',
            'author': author,
            'genre': genre
        }

        response = self.client.post(reverse('add-book'), data=book_data, follow=True)
        self.assertMessages(response, 'Book successfully added')

The test fails, I see an empty list in the results instead of a message. However, in the application, the message is successfully passed to the page template and I see it on the page after the book is created. Storage backend in settings.py is not installed, so the app using storage.fallback.FallbackStorage provided by default.

What could be a problem in this case and in general what are the good practices for messages testing in Django CBV?

Welcome @REDRagnarok !

From the docs for assertMessages:

expected_messages is a list of Message objects.

It looks to me like the second parameter must be a list, and that list must have Message objects in it. Passing a string does not seem like it would work.

See the source for MessagesTestMixin

Try to provide message as a list as follows:

expected_messages_list = ['Book successfully added']
response = self.client.post(reverse('add-book'), data=book_data, follow=True)
self.assertMessages(response, expected_messages_list)

The test failed. Now I am getting in console:

AssertionError: Lists differ: [] != ['Book successfully added']

You’re still not providing a Message object here. A list of strings is not a list of Message objects.

Actually, that’s not quite accurate. This mixin class adds functionality to your CBV by overriding the form_valid method. You’re not creating an instance of this class for your message - you’re using that function to create the messages for you.

If you look at the source for SuccessMessageMixin, you’ll see it has this:

def form_valid(self, form):
    response = super().form_valid(form)
    success_message = self.get_success_message(form.cleaned_data)
    if success_message:
        messages.success(self.request, success_message)
    return response

That next-to-last-line (messages.success(...) works it way through a couple functions, until it ends up in the BaseStorage.add method at this line:
message = Message(level, message, extra_tags=extra_tags)
which creates the message object to be added to the request.

The Message class, in that same file, is the class you want to use to create your Message instances for your test.

Ок, so I created Message object and added it to list as follows:

        message = Message(
                  message='Book successfully added',
                  level=constants.SUCCESS
                   )

        message_list = list()
        message_list.append(message)

        response = self.client.post(reverse('add-book'), data=book_data, follow=True)
        self.assertMessages(response, message_list)

After that nothing changed, the test still fails and the logs show that there are no messages in the request.

The text of the message:

Is not the same as:

I doubt this is the case, as I copied the message exactly in the application code. Especially in the test logs you can see that the empty list and the message from the test are compared. That is, the request does not return any message at all:

AssertionError: Lists differ: [] != [Message(level=25, message='Book successfully added')]

Second list contains 1 additional elements.
First extra element 0:
Message(level=25, message='Book successfully added')

- []
+ [Message(level=25, message='Book successfully added')]

Please post your complete Book model.

Here it is:

def get_default_genre():
    default_genre, _ = Genre.objects.get_or_create(
        genre='undefined'
    )
    return default_genre.pk


def get_default_publisher():
    default_publisher, _ = Publisher.objects.get_or_create(
        publisher='undefined'
    )
    return default_publisher.pk


def get_default_author():
    default_author, _ = Author.objects.get_or_create(
        last_name='undefined', first_name=''
    )
    return default_author.pk

class Book(models.Model):

    title = models.CharField('title', max_length=200)
    author = models.ForeignKey('Author',
                               verbose_name='author',
                               on_delete=models.PROTECT,
                               default=get_default_author)
    genre = models.ForeignKey('Genre',
                              verbose_name='genre',
                              on_delete=models.PROTECT,
                              default=get_default_genre)
    publisher = models.ForeignKey('Publisher',
                                  verbose_name='publisher',
                                  on_delete=models.PROTECT,
                                  default=get_default_publisher,
                                  blank=True)
    pages = models.PositiveSmallIntegerField('pages', default=1, null=True, blank=True)
    cover = models.ImageField('cover', upload_to='covers', default='covers/empty_cover.jpg', blank=True)
    summary = models.TextField('summary', max_length=3000, blank=True)
    borrower = models.ForeignKey(User, verbose_name='client', on_delete=models.PROTECT, null=True, blank=True)

    AVAILABLE = 'available'
    RESERVED = 'reserved'
    APPROVAL = 'on_approval'
    LOANED = 'loaned'

    LOAN_STATUS = {
        AVAILABLE: 'available',
        RESERVED: 'reserved',
        APPROVAL: 'on_approval',
        LOANED:  'loaned'
    }

    status = models.CharField(
        'Статус', max_length=15,
        choices=LOAN_STATUS,
        default=LOAN_STATUS.get(AVAILABLE)
    )

    class Meta:
        verbose_name = 'book'
        verbose_name_plural = 'books'
        constraints = [
            UniqueConstraint(
                fields=['title', 'author'],
                name="unique_title_author",
              
            )
        ]

        permissions = [
            ('manage_status', 'Handle user statuses'),
            ('reserve_book', 'Reserve book')
        ]

    def __str__(self):
        return self.title

I think the easiest thing to check right off-hand would be to add a form_invalid method to your view to print any error messages from the form you’re creating. I’m not entirely sure that the data you’re supplying in the post is sufficient for the form you’re creating.

Testing started at 23:04 ...
Creating test database for alias 'default'...
Found 1 test(s).
System check identified no issues (0 silenced).


title <ul class="errorlist"><li>Required field.</li></ul>
author <ul class="errorlist"><li>Required field.</li></ul>
genre <ul class="errorlist"><li>Required field.</li></ul>

Thanks a lot!
You were right, seems like no data submitted in post request. Going to look for the reason tomorrow.

Eventually I was able to fix the test. There were two problems. First, I was passing strings or a list to assertMessages instead of a Message object. KenWhitesell helped me to figure it out, thank you very much.

KenWhitesell correctly suggested that the form was probably not being saved. I added error output in the form_invalid() method for the BookCreate view and saw that the title, author, and genre fields on the form were not populated from the POST request dictionary.
Therefore, when validating the form, “The field is required” errors were issued, the form was not saved and no success_message was issued.

In the end I displayed the form in the console via get_form() and saw that the prefix “create-book” was added to the field names. So I realized that I was incorrectly naming keys without prefix in the dictionary. For example, I wrote {'title': 'Great Expectations'}, and the correct way was to write {'create-book-title': 'Great Expectations'}.
I rewrote the data dictionary this way:

data = {
            'create-book-title': 'Great Expectations',
            'create-book-author': 1,
            'create-book-genre': 1
        }

Author and genre are foreign keys, so they are passed as numbers.
After that everything worked and the test completed successfully. Thus, the correct test code will look like this:

    def test_view_success_message_attached(self):

        data = {
            'create-book-title': 'Great Expectations',
            'create-book-author': 1,
            'create-book-genre': 1
        }

        response = self.client.post(reverse('add-book'), data, follow=True)

        message = Message(
            level=constants.SUCCESS,
            message='Book was succesfully added'
            )

        messages = list()
        messages.append(message)

        self.assertMessages(response, messages)