Introduction
The previous parts of this guide covered Django’s core concepts: project structure, models, relationships, queries, forms, authentication, and APIs. You now have the tools to build a working application on your development machine.
But development machines aren’t where applications live. This part begins addressing the gap between “it works on my laptop” and “it works in production,” focusing on two foundational topics: testing and static files.
These might seem like afterthoughts — things to figure out once the “real” coding is done. They’re not. Testing catches bugs before users do. Static file handling affects both development workflow and production performance. Understanding them early prevents painful rewrites later.
Testing Your Application
Testing is the difference between “I think it works” and “I know it works.” Django provides a testing framework built on Python’s unittest, with additions specific to web applications: test clients that simulate requests, database handling that isolates tests from each other, and utilities for common assertions.
Why Testing Matters
Every developer has experienced the fear of changing code. Will this fix break something else? The codebase is complex, the interactions are subtle, and manual testing catches only the scenarios you remember to check.
Automated tests eliminate this fear. They document what your code should do. They catch regressions immediately. They let you refactor with confidence. The time invested in writing tests pays back every time you make a change.
Django applications particularly benefit from testing because web applications have many integration points: models talk to databases, views process requests, templates render responses, forms validate input. Bugs hide in the gaps between these layers.
Some developers argue that testing slows them down. The opposite is true — but only if you’re measuring the right thing. Writing tests takes time upfront. Not writing tests costs time later: in debugging, in fear-driven development, in bugs that reach production. The math favors testing, especially as projects grow.
Writing Your First Test
Django looks for tests in files named test*.py within your apps. Create blog/tests.py:
from django.test import TestCase
from django.contrib.auth.models import User
from .models import Post
class PostModelTest(TestCase):
def setUp(self):
self.user = User.objects.create_user(
username='testuser',
password='testpass123'
)
def test_post_creation(self):
post = Post.objects.create(
title='Test Post',
content='Test content',
author=self.user
)
self.assertEqual(post.title, 'Test Post')
self.assertEqual(post.author.username, 'testuser')
def test_post_str_representation(self):
post = Post.objects.create(
title='Test Post',
content='Test content',
author=self.user
)
self.assertEqual(str(post), 'Test Post')
The setUp method runs before each test, creating fresh data. This isolation matters: tests should never depend on each other or on data from previous runs. Each test starts with a known state.
Each test method starts with test_ — Django discovers and runs methods with this prefix automatically. The naming convention also serves as documentation: test_post_str_representation tells you exactly what’s being verified.
Run tests with:
python manage.py test
Django creates a separate test database, runs your tests, and destroys it afterward. Your development data remains untouched. This happens automatically; you don’t configure it.
Testing Views
Model tests verify data logic. View tests verify request handling — the full cycle from URL to response. The test client simulates a browser, making requests to your views without an actual HTTP connection:
from django.test import TestCase, Client
from django.urls import reverse
from django.contrib.auth.models import User
from .models import Post
class PostViewTest(TestCase):
def setUp(self):
self.client = Client()
self.user = User.objects.create_user(
username='testuser',
password='testpass123'
)
self.post = Post.objects.create(
title='Test Post',
content='Test content',
author=self.user
)
def test_post_list_view(self):
response = self.client.get(reverse('post_list'))
self.assertEqual(response.status_code, 200)
self.assertContains(response, 'Test Post')
self.assertTemplateUsed(response, 'blog/post_list.html')
def test_post_detail_view(self):
response = self.client.get(
reverse('post_detail', kwargs={'pk': self.post.pk})
)
self.assertEqual(response.status_code, 200)
self.assertContains(response, 'Test content')
def test_create_post_requires_login(self):
response = self.client.get(reverse('post_create'))
self.assertEqual(response.status_code, 302) # redirect to login
def test_create_post_authenticated(self):
self.client.login(username='testuser', password='testpass123')
response = self.client.post(reverse('post_create'), {
'title': 'New Post',
'content': 'New content',
})
self.assertEqual(Post.objects.count(), 2)
The reverse function converts URL names to actual paths. This indirection matters: if you change a URL pattern, tests using reverse continue working. Tests hardcoding paths would break.
Assertions like assertContains and assertTemplateUsed check both content and structure. Status code checks verify the response type: 200 for success, 302 for redirect, 404 for not found, 403 for forbidden.
Notice how testing authentication works: self.client.login() simulates a logged-in user for subsequent requests. This lets you verify that protected views actually require authentication and that authenticated users can access them. The test test_create_post_requires_login confirms unauthenticated users get redirected; test_create_post_authenticated confirms authenticated users succeed.
Testing Forms
Form tests verify validation logic — the rules that determine what input is acceptable:
from django.test import TestCase
from .forms import PostForm
class PostFormTest(TestCase):
def test_valid_form(self):
data = {
'title': 'Valid Title',
'content': 'Valid content here',
}
form = PostForm(data=data)
self.assertTrue(form.is_valid())
def test_title_too_short(self):
data = {
'title': 'Hi',
'content': 'Valid content here',
}
form = PostForm(data=data)
self.assertFalse(form.is_valid())
self.assertIn('title', form.errors)
def test_empty_content(self):
data = {
'title': 'Valid Title',
'content': '',
}
form = PostForm(data=data)
self.assertFalse(form.is_valid())
These tests document your validation rules. When someone asks “what are the requirements for a post title?”, the tests provide an executable answer. They also catch regressions: if someone accidentally removes a validation rule, the test fails.
The pattern is consistent: create form with data, call is_valid(), assert the expected outcome. For invalid data, also verify that errors appear on the correct fields.
What to Test
Not everything needs a test. Testing trivially correct code wastes time. Testing Django itself wastes time — Django has its own test suite. Focus your effort where it matters:
Business logic: Rules specific to your application. If a post can only be edited within 24 hours of creation, test that. If premium users get extra features, test that. These rules are where your application’s value lives.
Edge cases: Empty inputs, very long strings, special characters, boundary conditions. What happens when someone submits a form with no data? With the maximum allowed data? With Unicode characters? Edge cases are where bugs hide.
Integration points: Where models, views, and forms interact. A model might work perfectly, a form might validate correctly, but the view that connects them might have a bug. Test the connections.
Regressions: When you fix a bug, write a test that would have caught it. This prevents the bug from returning. Bug fixes without tests have a tendency to recur.
Don’t test Django itself. You don’t need to verify that CharField stores strings or that ForeignKey creates relationships. Django’s own tests cover that. Test your code, not the framework.
Running Specific Tests
As your test suite grows, running everything takes time. Target specific tests during development:
# Run tests for one app
python manage.py test blog
# Run one test class
python manage.py test blog.tests.PostModelTest
# Run one test method
python manage.py test blog.tests.PostModelTest.test_post_creation
# Run with verbosity
python manage.py test -v 2
The -v 2 flag shows each test name as it runs, useful for tracking down which test is slow or hanging.
Static Files
Static files — CSS, JavaScript, images — present a deployment challenge. During development, Django serves them directly, which is convenient but slow. In production, this is inefficient; web servers like Nginx handle static files much faster than Python can.
Django’s static file system bridges this gap: one workflow for development, a different (faster) path for production, with tools to transition between them.
Understanding the Problem
Consider what happens when a browser requests your CSS file. In development, you want changes to appear immediately — edit the file, refresh the browser, see the result. In production, you want the opposite: aggressive caching so browsers don’t re-download unchanged files.
You also want organization during development — CSS with your blog app, JavaScript with your accounts app — but a single collected location for production deployment.
Django’s staticfiles app solves both problems. It finds files across your project during development and collects them into one place for production.
Development Setup
The staticfiles app is enabled by default. Configure it in settings.py:
STATIC_URL = '/static/'
STATICFILES_DIRS = [
BASE_DIR / 'static',
]
STATIC_ROOT = BASE_DIR / 'staticfiles'
STATIC_URL is the URL prefix for static files — when templates reference static files, they’ll be served from URLs starting with /static/.
STATICFILES_DIRS lists directories containing project-wide static files. This is where you put CSS and JavaScript that isn’t specific to any single app — site-wide styles, for example.
STATIC_ROOT is the destination for collectstatic, which we’ll discuss shortly. During development, this setting isn’t used, but it must be defined for production.
Organizing Static Files
Static files can live in two places: project-wide directories (listed in STATICFILES_DIRS) or within individual apps.
For app-specific files, create a structure like this:
blog/
└── static/
└── blog/
├── css/
│ └── style.css
└── js/
└── script.js
The nested blog/blog/ structure looks redundant but prevents naming conflicts. If both your blog app and your accounts app have a style.css, the extra directory ensures they don’t collide. Without it, one would overwrite the other when collected.
Reference files in templates using the static tag:
{% load static %}
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="{% static 'blog/css/style.css' %}">
</head>
<body>
...
<script src="{% static 'blog/js/script.js' %}"></script>
</body>
</html>
The {% load static %} tag must appear before any {% static %} calls. It’s easy to forget and produces confusing errors — the template renders but URLs are missing.
The {% static %} tag generates the correct URL based on your settings. In development, it might produce /static/blog/css/style.css. In production with a CDN, it might produce https://cdn.example.com/static/blog/css/style.css. Your template doesn’t need to know the difference.
Production Deployment
The development server serves static files automatically, but it’s not suitable for production — it’s slow and not designed for concurrent requests.
Before deploying, collect all static files into STATIC_ROOT:
python manage.py collectstatic
This command finds every static file across your project — from apps, from STATICFILES_DIRS — and copies them into the STATIC_ROOT directory. The output is a single directory containing everything, ready to serve.
In production, your web server serves this directory directly, bypassing Django entirely. A typical Nginx configuration:
location /static/ {
alias /path/to/your/project/staticfiles/;
expires 30d;
add_header Cache-Control "public, immutable";
}
The expires and Cache-Control headers tell browsers to cache these files aggressively. Static files rarely change, and re-downloading them wastes bandwidth.
WhiteNoise: A Simpler Alternative
Configuring Nginx requires server access and operational knowledge. For simpler deployments — especially on PaaS platforms like Heroku — WhiteNoise lets Django serve static files efficiently:
pip install whitenoise
Add to middleware in settings.py:
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'whitenoise.middleware.WhiteNoiseMiddleware', # add after SecurityMiddleware
...
]
WhiteNoise serves files with proper caching headers and can compress them automatically. It’s not as fast as Nginx for very high traffic, but it’s simpler to configure and performs well for most applications.
For additional optimization, use WhiteNoise’s compressed and cached storage:
STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage'
This creates compressed versions of files and adds content hashes to filenames, enabling aggressive caching while ensuring users always get updated files when you deploy changes.
Media Files
User-uploaded files — images, documents, avatars — are “media files,” distinct from static files. The distinction matters: static files are your code, under your control; media files come from users and require different handling.
Configure media files separately:
MEDIA_URL = '/media/'
MEDIA_ROOT = BASE_DIR / 'media'
MEDIA_URL is the URL prefix for uploaded files. MEDIA_ROOT is the filesystem location where uploads are stored.
In development, add URL patterns to serve media files:
from django.conf import settings
from django.conf.urls.static import static
urlpatterns = [
...
]
if settings.DEBUG:
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
The if settings.DEBUG check ensures this only applies in development. In production, your web server handles media files:
location /media/ {
alias /path/to/your/project/media/;
}
Security matters more for media files than static files. Users can upload malicious content. Validate file types, limit file sizes, and consider storing uploads outside your main project directory. Never execute uploaded files or serve them with types that browsers might interpret as code.
What Comes Next
This part covered testing and static files — foundations for reliable development and deployment. Tests give you confidence in your code. Proper static file handling gives you performance and organizational clarity.
The next part will address security configuration, database considerations, deployment options, and production monitoring. These are the final pieces needed to take your application from development to production.
Conclusion - Staff Note
Testing and static files seem unglamorous compared to building features. They don’t produce visible results for users. But they produce results for you: confidence that your code works, performance that users feel without noticing, deployment that doesn’t involve manual file copying.
Write tests as you build features, not after. Test the happy path, then test the edge cases. When a bug appears, write a test before fixing it. This habit compounds over time.
For static files, set up the structure correctly from the start. The nested directory convention feels redundant until you have two apps with conflicting filenames. The {% load static %} tag feels unnecessary until you deploy to a CDN. These conventions exist because people learned the hard way.
The Frontek.dev Team