During the development of this blog, I was mindful of the edge cases that may be encountered throughout the site. Feeling that I was still learning many aspects of web development, I tended to manually invoke them when testing. However, at a certain point this workflow became inefficient and I took the time to pause and learn how to write and use tests with the Django test suite. I was already familiar with the Python unittest framework, but only really had surface-level testing knowledge.
Consulting testing tutorials, the recommendation to split model, view, and form tests into separate files within a "/tests/" module made sense and I followed this structure. I began with some simple tests around edge cases which I was already familiar with, for example assuring that the post listing excluded posts with a publication date in the future:
def create_post(
user, title="Test Post", content="This is test content.", pub_date=None
):
if pub_date is None:
pub_date = timezone.now()
return Post.objects.create(
title=title, content=content, pub_date=pub_date, user=user
)
def create_future_and_recent_post(user):
future_post = create_post(
user=user,
title="Future Post",
content="This is a future post.",
pub_date=timezone.now() + datetime.timedelta(days=1),
)
recent_post = create_post(
user=user,
title="Recent Post",
content="This is a recent post.",
pub_date=timezone.now() - datetime.timedelta(days=1),
)
return future_post, recent_post
class HomeViewTests(TestCase):
def test_listing_excludes_future_posts(self):
future_post, recent_post = create_future_and_recent_post(
User.objects.create_user(username="test_user", password="12345")
)
response = self.client.get(reverse("blog:home"))
self.assertQuerySetEqual(response.context["paginated_posts"], [recent_post])
After becoming more comfortable with testing, I added docstrings for clarity, used the setUpTestData class method for class-wide data setup and wrote more detailed tests, such as test_tree_post_list_order, which not only asserts that posts are correctly ordered in the back-end, but also in the front-end:
def create_post(
user, title="Test Post", content="This is test content.", pub_date=None
):
if pub_date is None:
pub_date = timezone.now()
return Post.objects.create(
title=title, content=content, pub_date=pub_date, user=user
)
def create_date_staggered_posts(user):
recent_post = create_post(
user=user, title="Recent Title", content="This is recent content."
)
old_post = create_post(
user=user,
title="Old Title",
content="This is old content.",
pub_date=timezone.now() - datetime.timedelta(days=1),
)
oldest_post = create_post(
user=user,
title="Oldest Title",
content="This is the oldest content.",
pub_date=timezone.now() - datetime.timedelta(days=2),
)
return recent_post, old_post, oldest_post
class HomeViewTests(TestCase):
@classmethod
def setUpTestData(cls):
cls.user = User.objects.create_user(username="test_user", password="12345")
cls.paginate_by = BlogHomeView.paginate_by
cls.multi_post_count = (cls.paginate_by * 3) // 2 # For pagination tests
def test_tree_post_list_order(self):
"""
Verify the post tree object list is in descending order in the back-end
and front-end.
"""
recent_post, old_post, oldest_post = create_date_staggered_posts(user=self.user)
response = self.client.get(reverse("blog:home"))
response_content = response.content.decode()
self.assertQuerySetEqual(
response.context["tree_posts"], [recent_post, old_post, oldest_post]
)
# Compare indexes to check posts are rendered in the correct order
self.assertTrue(
response_content.index(f"{recent_post.title}")
< response_content.index(f"{old_post.title}")
< response_content.index(f"{oldest_post.title}")
)
Writing tests actually brought to light things I had overlooked when manually testing, such as feedback to the user when the post listing is empty (I'd almost always used at least 1 post for layout testing):
def test_empty_post_listing(self):
"""
Verify empty state message is shown in place of an empty post listing.
"""
response = self.client.get(reverse("blog:home"))
self.assertQuerySetEqual(response.context["paginated_posts"], [])
self.assertContains(
response, '<p class="empty-listing">No posts available.</p>'
)
All in all, writing and using Django tests proved to be incredibly powerful and useful. They assisted me in speeding up my workflow, gave me peace of mind that existing functionality broken by new developments can be easily identified, and even highlighted missing features.


So, at below tablet viewport size, I did the same. The search and archive functions would be in a menu that was, for the most part, identical to the sidebar in design and functionality, and accessed by pressing a fixed floating hamburger menu button.
Admittedly, it was rather tricky to achieve the result as I had imagined it, but in the end also reassuring that it was possible and didn't have to resort to using