Catch bugs before users do. Write tests with TestCase and the test client, run them with pytest, debug with breakpoints and the debug toolbar, add error pages, and configure logging.
Why: TestCase creates a fresh, throwaway database for each run and wraps every test in a transaction, so tests never touch your real data. Where: blog/tests.py. Methods that start with test_ are discovered automatically.
# blog/tests.py
from django.test import TestCase
from .models import Post
class PostModelTests(TestCase):
def test_str_returns_title(self):
post = Post.objects.create(title="Hi", slug="hi", body="...")
self.assertEqual(str(post), "Hi")
def test_defaults_to_unpublished(self):
post = Post.objects.create(title="Draft", slug="d", body="...")
self.assertFalse(post.published)Why: the test client acts like a browser without a running server — it sends requests and inspects the response. Note: check the status code and that the right content appears; reverse() keeps the test in sync with your URLConf.
from django.test import TestCase
from django.urls import reverse
class PostListViewTests(TestCase):
def test_list_shows_published_post(self):
Post.objects.create(title="Live", slug="live", status="published")
response = self.client.get(reverse("blog:post_list"))
self.assertEqual(response.status_code, 200)
self.assertContains(response, "Live")Why: the test runner finds every test_ method across your apps. Note: pytest with the pytest-django plugin is a popular alternative — shorter assert statements and powerful fixtures. Both run the same TestCase classes.
Built-in runner:
python manage.py testpython manage.py test blogOr with pytest (pip install pytest-django):
pytestWhy: breakpoint() pauses execution and drops you into an interactive debugger (pdb) right where the bug is — inspect variables, step line by line, continue. Note: type n (next), s (step in), p var (print), c (continue), q (quit). Remove it before committing.
def post_detail(request, slug):
post = get_object_or_404(Post, slug=slug)
breakpoint() # execution pauses here in the terminal
return render(request, "blog/post_detail.html", {"post": post})
# at the (Pdb) prompt:
# p post.title → inspect a value
# n → next line
# c → continue runningWhy: django-debug-toolbar overlays a panel on every page showing the SQL queries, their timings, templates, and cache hits — the fastest way to spot N+1 queries. When: development only; never enable it in production.
Install, add "debug_toolbar" to INSTALLED_APPS, its middleware, and set INTERNAL_IPS = ["127.0.0.1"]:
pip install django-debug-toolbarNote: when DEBUG is False, Django serves your own templates named 404.html and 500.html from the templates root. Why LOGGING: route warnings and errors to the console or a file so you can see what happened in production.
templates/
├── 404.html # shown for "not found" when DEBUG = False
└── 500.html # shown for server errors# config/settings.py
LOGGING = {
"version": 1,
"disable_existing_loggers": False,
"handlers": {"console": {"class": "logging.StreamHandler"}},
"root": {"handlers": ["console"], "level": "INFO"},
}
# in your code
import logging
logger = logging.getLogger(__name__)
logger.info("Post %s published", post.id)