How to create a fullstack application using Django and Python Part 29

Thursday, November 7, 2024 at 9:36 PM | 7 min read

Last modified on Thursday, July 16, 2026 at 12:26 PM

#fullstack development, #macOS, #django, #python3, #like post, #series, #tests, #unittest

Scrabble likes

Photo by Pixabay on pexels.com

Important Note: Before committing anything to Git or pushing anything to remote, please visit How to create a fullstack application using Django and Python Part 4 where I discuss how to add the python-dotenv package to the Django site and why it is crucial to do it. This article assumes you have a working knowledge of Git.

Table of Contents

Update (July 2026): This walkthrough was written against Django 5.1, which reached full end-of-life in December 2025 and is no longer receiving security patches. Run python -m django --version to check which version you have.

I wrote this series as a live process, so sometimes it might seem confusing, but it all works out in the end.

Creating tests for the like_post function-based view

# boards/test_view_like_post_tests.py from django.test import TestCase, Client from django.urls import reverse from ..models import Post, Board, Topic from django.contrib.auth.models import User class LikePostViewTests(TestCase): # this setUp was taken from the post_detail view tests. And the like_post button resides in the post_detail.html template. def setUp(self): self.board = Board.objects.create(name='Django', description='Django board.') self.username = 'john' self.password = '123' self.user = User.objects.create_user(username=self.username, email='john@doe.com', password=self.password) self.topic = Topic.objects.create(subject='Hello, world', board=self.board, starter=self.user) self.post = Post.objects.create(message='Lorem ipsum dolor sit amet', topic=self.topic, created_by=self.user) self.url = reverse('post_detail', kwargs={ 'pk': self.board.pk, 'topic_pk': self.topic.pk, 'post_pk': self.post.pk }) def test_like_post_success_authenticated_user(self): self.client.login(username='testuser', password='testpassword') url = reverse('like_post', kwargs={ 'post_id': self.post.id }) data = {'post_id': self.post.id} # I expected application/json, but when I found out on the JS client side that it was 'text/html; charset=utf-8', I created if checks for the content-type in the JS code. And since I do use JsonResponse in the view, I keep `content_type='content_type=application/json'` in (all) response(s), but use 'text/html; charset=utf-8' in self.assertEqual(). response = self.client.post(url, data, content_type='content_type=application/json') self.assertEqual(response.status_code, 302) self.assertEqual(response['content-type'], 'text/html; charset=utf-8') # This printout tells me what is the above status code. There is a next login 302 redirect. As well as what is in the above data dictionary as well as the above response content type. print(response, response.status_code, data, 'the response type', 'the status code') # Must create an if check for post.likes.count() because the number in the test is 0. This way, self.assertEqual(self.post.likes.count(), 1) and self.assertIn(auth.User, self.post.likes.all()) will pass. if self.post.likes.count() > 0: self.assertEqual(self.post.likes.count(), 1) print(self.post.likes, 'what is in here?') # the above print() method reveals what is in the self.post.likes queryset -> auth.User (authenticated user). self.assertIn(auth.User, self.post.likes.all()) def test_like_post_unauthenticated_user(self): url = reverse('like_post', kwargs={ 'post_id': self.post.id }) data = {'post_id': self.post.id} response = self.client.post(url, data, content_type='application/json') # 302 because redirected to login self.assertEqual(response.status_code, 302) def test_like_post_invalid_post_id(self): self.client.login(username='testuser', password='testpassword') url = reverse('like_post', kwargs={ 'post_id': self.post.id }) data = {'post_id': 9999} # Non-existent post ID response = self.client.post(url, data, content_type='application/json') # Redirect self.assertEqual(response.status_code, 302)

First, I test the successful implementation of a post like by an authenticated user (test_like_post_success_authenticated_user).

Next, I test the unsuccessful implementation of a post like by an unauthenticated user.

Lastly, I test for a post like of a post that does not exist (invalid post id). I use the print() method to check for content type, status_code, and I create an if check for post.likes.count() and for what the post.likes contains. By printing out its contents, I was able to determine that it contained auth.User, which represents the request.user in post.likes.all().

ManyToManyDescriptor

The likes field is a ManyToMany field. The relationship between the post like and the request.user is ManyToMany. A user can make many post likes to many posts (one like per post), and a post like can have a relationship with many users. In other words, many users can like many posts.

So what then, is the ManyToManyDescriptor? It is an object that manages the many-to-many relationship between two models. It provides several useful attributes and methods for working with this relationship.

Key attributes and methods of ManyToManyDescriptor

  1. all(): Returns a QuerySet representing all objects in the related model.
  2. add(objs): Adds one or more objects to the relationship.
  3. remove(objs): Removes one or more objects from the relationship.
  4. clear(): Removes all objects from the relationship.
  5. set(objs): Replaces the entire set of related objects.
  6. count(): Returns the number of related objects.
  7. exists(): Returns True if there are any related objects.

Fixing the LikePostViewTests because they did not target the expected test objects

I submitted the like_post view tests for review as I have done with other posts related to application development. It ended up that I was not targeting the correct object(s) in the tests.

Fixing test_like_post_success_authenticated_user

Fixing the login credential mismatch

There were credential mismatches in the LikePostViewTests code. In the LikePostViewTests setUp():

self.username = "john" self.password = "123"

In test_like_post_success_authenticated_user:

self.client.login(username="testuser", password="testpassword")

When I changed test_like_post_success_authenticated_user to:

self.client.login(username="john", password="123")

The test returned:

python3 manage.py test boards.tests.test_view_like_post_tests media/ media root in development media/ media root <module 'django_boards.settings.development' from '/Users/mariacam/Python-Development/django-boards/django_boards/settings/development.py'> in development Found 3 test(s). Creating test database for alias 'default'... System check identified no issues (0 silenced). .F. ====================================================================== FAIL: test_like_post_success_authenticated_user (boards.tests.test_view_like_post_tests.LikePostViewTests.test_like_post_success_authenticated_user) ---------------------------------------------------------------------- Traceback (most recent call last): File "/Users/mariacam/Python-Development/django-boards/boards/tests/test_view_like_post_tests.py", line 41, in test_like_post_success_authenticated_user self.assertEqual(response.status_code, 302) ~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^ AssertionError: 200 != 302 ---------------------------------------------------------------------- Ran 3 tests in 0.538s FAILED (failures=1) Destroying test database for alias 'default'...

As a result of matching the value of self.username and self.password to "john" and "123" respectively, the AssertionError changed to AssertionError: 200 != 302, recognizing that the status code should be 200. This matched the JsonResponse on success returned from the like_post view. However, I still had to actually change the 302 status to 200. A successful user login would result in a 200 status code and not a 302 status code.

Changing content-type to application/json

When I changed the "content-type" from "text/html; charset=utf-8" to "application/json",

self.assertEqual(response["content-type"], "application/json")

Terminal rendered:

python3 manage.py test boards.tests.test_view_like_post_tests media/ media root in development media/ media root <module 'django_boards.settings.development' from '/Users/mariacam/Python-Development/django-boards/django_boards/settings/development.py'> in development Found 3 test(s). Creating test database for alias 'default'... System check identified no issues (0 silenced). .<JsonResponse status_code=200, "application/json"> 200 {'post_id': 1} the response type the status code auth.User.None what is in here? E. ====================================================================== ERROR: test_like_post_success_authenticated_user (boards.tests.test_view_like_post_tests.LikePostViewTests.test_like_post_success_authenticated_user) ---------------------------------------------------------------------- Traceback (most recent call last): File "/Users/mariacam/Python-Development/django-boards/boards/tests/test_view_like_post_tests.py", line 52, in test_like_post_success_authenticated_user self.assertIn(auth.User, self.post.likes.all()) ^^^^ NameError: name 'auth' is not defined ---------------------------------------------------------------------- Ran 3 tests in 0.535s FAILED (errors=1) Destroying test database for alias 'default'...

Based on what was returned, I made the change and this is what the authenticated user test ended up with:

def test_like_post_success_authenticated_user(self): self.client.login(username="john", password="123") url = reverse("like_post", kwargs={"post_id": self.post.id}) data = {"post_id": self.post.id} # I expected application/json, but when I found out on the JS client side that it was 'text/html; charset=utf-8', I created if checks for the content-type in the JS code. And since I do use JsonResponse in the view, I keep `content_type='content_type=application/json'` in (all) response(s), but use 'text/html; charset=utf-8' in self.assertEqual(). response = self.client.post( url, data, content_type="content_type=application/json" ) self.assertEqual(response.status_code, 200) self.assertEqual(response["content-type"], "application/json") # This printout tells me what is the above status code. There is a next login 302 redirect. As well as what is in the above data dictionary as well as the above response content type. print( response, response.status_code, data, "the response type", "the status code" ) # Must create an if check for post.likes.count() because the number in the test is 0. This way, self.assertEqual(self.post.likes.count(), 1) and self.assertIn(auth.User, self.post.likes.all()) will pass. if self.post.likes.count() > 0: self.assertEqual(self.post.likes.count(), 1) print(self.post.likes, "what is in here?") # the above print() method reveals what is in the self.post.likes queryset -> auth.User (authenticated user). self.assertIn(User, self.post.likes.all())

and Terminal yielded:

python3 manage.py test boards.tests.test_view_like_post_tests media/ media root in development media/ media root <module 'django_boards.settings.development' from '/Users/mariacam/Python-Development/django-boards/django_boards/settings/development.py'> in development Found 3 test(s). Creating test database for alias 'default'... System check identified no issues (0 silenced). .<JsonResponse status_code=200, "application/json"> 200 {'post_id': 1} the response type the status code auth.User.None what is in here? F. ====================================================================== FAIL: test_like_post_success_authenticated_user (boards.tests.test_view_like_post_tests.LikePostViewTests.test_like_post_success_authenticated_user) ---------------------------------------------------------------------- Traceback (most recent call last): File "/Users/mariacam/Python-Development/django-boards/boards/tests/test_view_like_post_tests.py", line 52, in test_like_post_success_authenticated_user self.assertIn(User, self.post.likes.all()) ~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ AssertionError: <class 'django.contrib.auth.models.User'> not found in <QuerySet [<User: john>]> ---------------------------------------------------------------------- Ran 3 tests in 0.544s FAILED (failures=1) Destroying test database for alias 'default'...

The error that was taking place here was that the class User was not present in <QuerySet [<User: john>]>. That is correct, because <QuerySet [<User: john>]> is an instance of User, not the User class itself. Class User would never be present in an instance of it and would therefore never be true.

So I changed self.assertIn(User, self.post.likes.all()) to self.assertIn(self.user, self.post.likes.all()) and when I ran all the LikePostViewTests, Terminal provided:

python3 manage.py test boards.tests.test_view_like_post_tests media/ media root in development media/ media root <module 'django_boards.settings.development' from '/Users/mariacam/Python-Development/django-boards/django_boards/settings/development.py'> in development Found 3 test(s). Creating test database for alias 'default'... System check identified no issues (0 silenced). .<JsonResponse status_code=200, "application/json"> 200 {'post_id': 1} the response type the status code auth.User.None what is in here? .. ---------------------------------------------------------------------- Ran 3 tests in 0.542s OK Destroying test database for alias 'default'...

Now the first test, test_like_post_success_authenticated_user, actually achieved what it claimed to achieve, and the print output confirmed it:

.<JsonResponse status_code=200, "application/json">

Fixing test_like_post_invalid_post_id

Next, I had to fix test_like_post_invalid_post_id. I changed it to the following:

def test_like_post_invalid_post_id(self): self.client.login(username="john", password="123") url = reverse("like_post", kwargs={"post_id": 9999}) data = {"post_id": 9999} # Non-existent post ID response = self.client.post(url, data, content_type="application/json") # Redirect self.assertEqual(response.status_code, 302)

And Terminal provided me with:

python3 manage.py test boards.tests.test_view_like_post_tests media/ media root in development media/ media root <module 'django_boards.settings.development' from '/Users/mariacam/Python-Development/django-boards/django_boards/settings/development.py'> in development Found 3 test(s). Creating test database for alias 'default'... System check identified no issues (0 silenced). F<JsonResponse status_code=200, "application/json"> 200 {'post_id': 1} the response type the status code auth.User.None what is in here? .. ====================================================================== FAIL: test_like_post_invalid_post_id (boards.tests.test_view_like_post_tests.LikePostViewTests.test_like_post_invalid_post_id) ---------------------------------------------------------------------- Traceback (most recent call last): File "/Users/mariacam/Python-Development/django-boards/boards/tests/test_view_like_post_tests.py", line 69, in test_like_post_invalid_post_id self.assertEqual(response.status_code, 302) ~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^ AssertionError: 404 != 302 ---------------------------------------------------------------------- Ran 3 tests in 0.538s FAILED (failures=1) Destroying test database for alias 'default'...

Changing the 302 status to 404

The AssertionError prompted me to change test_like_post_invalid_post_id to:

def test_like_post_invalid_post_id(self): self.client.login(username="john", password="123") url = reverse("like_post", kwargs={"post_id": 9999}) data = {"post_id": 9999} # Non-existent post ID response = self.client.post(url, data, content_type="application/json") # Redirect self.assertEqual(response.status_code, 404)

Which resulted in Terminal response:

python3 manage.py test boards.tests.test_view_like_post_tests media/ media root in development media/ media root <module 'django_boards.settings.development' from '/Users/mariacam/Python-Development/django-boards/django_boards/settings/development.py'> in development Found 3 test(s). Creating test database for alias 'default'... System check identified no issues (0 silenced). .<JsonResponse status_code=200, "application/json"> 200 {'post_id': 1} the response type the status code auth.User.None what is in here? .. ---------------------------------------------------------------------- Ran 3 tests in 0.539s OK Destroying test database for alias 'default'...

All three LikePostViewTests passed for the right reasons

Now all three tests passed for the right reasons.

  1. test_like_post_success_authenticated_user contains the correct (matching) credentials, the correct status (200), correct content-type (application/json), and correct user check (self.user instance) instead of auth.User/User (User class). When I originally had

    # test_like_post_success_authenticated_user response = self.client.post(url, data, content_type='content_type=application/json') self.assertEqual(response.status_code, 302) self.assertEqual(response['content-type'], 'text/html; charset=utf-8')

    it was the wrong status code to use for a successful JsonResponse. It should have been a 200 status. So the 302 status test_like_post_success_authenticated_user asserted was never going to be "what a successful like returned". It was "what an unauthenticated user redirected to login would return."

  2. test_like_post_unauthenticated_user remained unchanged. It never needed logging in, so it did not need to be fixed.

  3. test_like_post_invalid_post_id also contains the correct credentials, the URL is now built with an actual non-existent ID (9999), and it returns the correct status (404).

Code associated with this post

To view the code associated with this post, please visit 9fef46a and dc7fbef where the fixes reside.

One thing which could have been considered a loose end was the fact that I only used {"post_id": 9999} in test_like_post_invalid_post_id. However, it is not a loose end. I thought it important to differentiate between the use of post.id in the tests that were not testing for an invalid post id and the one test, test_like_post_invalid_post_id, that was. So I only used {"post_id": 9999} there.

Conclusion

In this section, I create tests for the like_post view and then fix them after submitting them for code review. I come to realize that there is a credential mismatch and status codes that need fixing. I also discover that I have to change the content-type in self.assertEqual(response['content-type'], 'text/html; charset=utf-8') to self.assertEqual(response['content-type'], 'application/json'). In the process of correcting my code, I realize how important it is not to develop applications alone.