第一次接触测试驱动开发(TDD)这个概念时,我正深陷在一个Django项目的泥潭中。那是个电商平台,随着功能不断增加,每次修改代码都像在走钢丝——你不知道哪处看似无害的调整会突然让整个支付系统崩溃。直到我尝试了《Python Web开发:测试驱动方法》中的实践,才真正体会到什么是"写代码时的安全感"。
测试驱动开发不是简单的"先写测试",而是一种彻底改变开发节奏的思维方式。它的核心循环"红-绿-重构"(先写失败测试->让测试通过->优化代码)强制你在动手写功能代码前先明确需求边界。对于Web开发这种涉及前后端交互、数据库操作和业务逻辑的复杂场景,这种纪律性尤为重要。
Python生态中有两个主流测试框架选择:
unittest:标准库自带,xUnit风格
python复制import unittest
class TestUserModel(unittest.TestCase):
def test_user_creation(self):
user = User(username="test")
self.assertEqual(user.username, "test")
pytest:第三方框架,更简洁的语法
python复制def test_user_creation():
user = User(username="test")
assert user.username == "test"
我强烈推荐pytest,因为它:
对于Web项目,这几个工具必不可少:
HTTP客户端:requests用于API测试,WebTest用于WSGI应用
python复制def test_api_returns_200():
response = requests.get("https://api.example.com/users")
assert response.status_code == 200
浏览器自动化:Selenium或Playwright
python复制# Playwright示例
def test_login(playwright):
browser = playwright.chromium.launch()
page = browser.new_page()
page.goto("https://example.com/login")
page.fill("#username", "admin")
page.click("text=Login")
assert page.url.endswith("/dashboard")
测试数据库:使用内存SQLite或Django测试数据库
python复制@pytest.mark.django_db
def test_user_creation():
User.objects.create(username="test")
assert User.objects.count() == 1
在项目根目录添加.github/workflows/test.yml:
yaml复制name: Test
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up Python
uses: actions/setup-python@v2
with:
python-version: '3.9'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
- name: Run tests
run: |
pytest --cov=.
我们先从用户注册功能开始,在tests/test_auth.py中:
python复制from django.test import TestCase
from django.urls import reverse
class AuthTests(TestCase):
def test_user_registration(self):
# 测试用户能访问注册页面
response = self.client.get(reverse('register'))
self.assertEqual(response.status_code, 200)
self.assertContains(response, "Register")
# 测试提交注册表单能创建用户
data = {
'username': 'testuser',
'email': 'test@example.com',
'password1': 'complexpassword123',
'password2': 'complexpassword123'
}
response = self.client.post(reverse('register'), data)
self.assertEqual(response.status_code, 302) # 重定向表示成功
self.assertTrue(User.objects.filter(username='testuser').exists())
运行测试会失败(红),因为我们还没有注册视图。
在auth/views.py中:
python复制from django.shortcuts import render, redirect
from django.contrib.auth import get_user_model
User = get_user_model()
def register_view(request):
if request.method == 'POST':
User.objects.create_user(
username=request.POST['username'],
email=request.POST['email'],
password=request.POST['password1']
)
return redirect('home')
return render(request, 'auth/register.html')
添加URL路由和简单模板后,测试应该通过(绿)。
现在我们可以:
python复制def test_weak_password_rejected(self):
data = {
'username': 'testuser',
'password1': '123',
'password2': '123'
}
response = self.client.post(reverse('register'), data)
self.assertEqual(response.status_code, 200) # 不是302重定向
self.assertContains(response, "This password is too short")
测试独立单元如模型方法、工具函数:
python复制def test_user_full_name():
user = User(first_name="John", last_name="Doe")
assert user.get_full_name() == "John Doe"
测试组件间交互,如视图与模板:
python复制def test_login_view_uses_correct_template():
response = client.get(reverse('login'))
assertTemplateUsed(response, 'auth/login.html')
测试完整用户流程:
python复制def test_user_can_login_and_logout(selenium):
selenium.get("https://example.com/login")
selenium.find_element(By.ID, "username").send_keys("admin")
selenium.find_element(By.ID, "password").send_keys("password123")
selenium.find_element(By.TAG_NAME, "form").submit()
assert "Welcome" in selenium.page_source
selenium.find_element(By.LINK_TEXT, "Logout").click()
assert "Login" in selenium.page_source
测试数据污染:每个测试用例应该独立
python复制@pytest.fixture
def test_user():
return User.objects.create(username="test")
def test_one(test_user):
assert User.objects.count() == 1
def test_two(test_user): # 会使用新的数据库
assert User.objects.count() == 1
使用工厂函数替代固定数据:
python复制import factory
class UserFactory(factory.django.DjangoModelFactory):
class Meta:
model = User
username = factory.Sequence(lambda n: f"user{n}")
is_active = True
def test_user_factory():
user = UserFactory()
assert user.is_active
使用pytest-xdist并行运行测试:
bash复制pytest -n auto
区分快慢测试:
python复制@pytest.mark.slow
def test_complex_calculation():
...
# 只运行快速测试
pytest -m "not slow"
使用内存数据库:
python复制@pytest.fixture(scope='module')
def django_db_setup():
from django.conf import settings
settings.DATABASES['default'] = {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': ':memory:'
}
安装pytest-cov后:
bash复制pytest --cov=myapp --cov-report=html
在htmlcov/index.html中可以看到:
在pyproject.toml中:
toml复制[tool.pytest.ini_options]
min_cov = 80
fail_under = 80
这样当覆盖率低于80%时测试会失败。
使用responses模拟HTTP请求:
python复制import responses
@responses.activate
def test_external_api():
responses.add(
responses.GET,
'https://api.example.com/data',
json={'key': 'value'},
status=200
)
response = requests.get('https://api.example.com/data')
assert response.json() == {'key': 'value'}
用hypothesis生成测试用例:
python复制from hypothesis import given
from hypothesis.strategies import text
@given(text())
def test_username_validation(username):
user = User(username=username)
assert len(user.username) <= 150 # Django的默认限制
用syrupy断言输出不变:
python复制def test_api_response(snapshot):
response = requests.get('https://api.example.com/data')
assert response.json() == snapshot
当API响应变化时,测试会失败并显示差异。
刚开始实践TDD时,最难掌握的是"何时停止写测试"。我的经验法则是:
例如开发一个博客系统:
test_can_view_post(失败)test_post_model(失败)test_post_detail_view(失败)这种由外而内、由粗到细的开发节奏,能确保你始终专注于当前最需要解决的问题。