1. Django自动化测试入门指南
作为Django开发者,我经常被问到:"为什么要花时间写测试?直接手动测试不就行了吗?"这个问题让我想起刚入行时的自己——直到一次线上事故让我彻底改变了看法。那次因为一个看似简单的修改,导致核心功能崩溃,损失了整整两天的业务数据。从那时起,我真正理解了测试的价值。
1.1 测试的基本分类
在Django项目中,我们主要关注两种测试类型:
-
单元测试(Unit Test):针对最小代码单元的测试,比如验证模型方法的返回值是否正确。这类测试通常运行速度快,能快速定位问题所在。
-
集成测试(Integration Test):检查多个组件协同工作的情况,比如测试用户从登录到完成订单的整个流程。这类测试更接近真实用户场景。
实际经验:单元测试应该占测试套件的70%左右,集成测试占30%。这个比例能保证既有快速反馈又有场景覆盖。
1.2 测试驱动开发(TDD)实践
虽然不强制要求TDD,但我推荐在修复bug时采用这种模式:
- 首先重现bug并编写失败的测试用例
- 然后修改代码使测试通过
- 最后重构代码保持整洁
这种流程能确保bug被真正修复,且不会再次出现。就像我们接下来要解决的was_published_recently()问题。
2. 实战:发现并修复模型bug
2.1 问题重现与分析
在Django shell中执行以下代码:
python复制import datetime
from django.utils import timezone
from polls.models import Question
# 创建30天后的未来问题
future_question = Question(pub_date=timezone.now() + datetime.timedelta(days=30))
print(future_question.was_published_recently()) # 错误地返回True
这个方法的业务逻辑应该是:只有当问题发布日期在最近24小时内才返回True。但当前实现漏掉了对未来日期的判断。
2.2 编写测试用例
在polls/tests.py中添加:
python复制from django.test import TestCase
from django.utils import timezone
from .models import Question
class QuestionModelTests(TestCase):
def test_future_question(self):
"""未来日期应返回False"""
time = timezone.now() + datetime.timedelta(days=30)
future_question = Question(pub_date=time)
self.assertIs(future_question.was_published_recently(), False)
运行测试:
bash复制python manage.py test polls
2.3 修复实现
修改polls/models.py:
python复制def was_published_recently(self):
now = timezone.now()
return now - datetime.timedelta(days=1) <= self.pub_date <= now
这个修改确保日期既不超过一天前,也不在未来。
2.4 完整测试覆盖
好的测试应该考虑边界条件:
python复制def test_old_question(self):
"""超过24小时的返回False"""
time = timezone.now() - datetime.timedelta(days=1, seconds=1)
old_question = Question(pub_date=time)
self.assertIs(old_question.was_published_recently(), False)
def test_recent_question(self):
"""24小时内的返回True"""
time = timezone.now() - datetime.timedelta(hours=23, minutes=59)
recent_question = Question(pub_date=time)
self.assertIs(recent_question.was_published_recently(), True)
3. 视图层测试实战
3.1 测试工具函数
创建测试辅助函数:
python复制def create_question(text, days):
"""创建测试问题"""
time = timezone.now() + datetime.timedelta(days=days)
return Question.objects.create(question_text=text, pub_date=time)
3.2 首页视图测试
测试各种场景:
python复制class QuestionIndexViewTests(TestCase):
def test_no_questions(self):
"""空数据库显示提示信息"""
response = self.client.get(reverse('polls:index'))
self.assertContains(response, "No polls available")
def test_past_question(self):
"""显示过去的问题"""
question = create_question("Past", -30)
response = self.client.get(reverse('polls:index'))
self.assertQuerysetEqual(
response.context['latest_question_list'],
[question]
)
def test_future_question(self):
"""不显示未来的问题"""
create_question("Future", 30)
response = self.client.get(reverse('polls:index'))
self.assertContains(response, "No polls available")
3.3 详情页测试
首先修改视图:
python复制class DetailView(generic.DetailView):
def get_queryset(self):
return Question.objects.filter(pub_date__lte=timezone.now())
然后添加测试:
python复制class DetailViewTests(TestCase):
def test_future_question(self):
"""未来问题返回404"""
future_q = create_question("Future", 5)
url = reverse('polls:detail', args=(future_q.id,))
response = self.client.get(url)
self.assertEqual(response.status_code, 404)
4. 静态文件管理最佳实践
4.1 项目结构规范
推荐的组织方式:
code复制polls/
static/
polls/
css/
style.css
js/
scripts.js
images/
logo.png
关键点:
- 在static下创建与应用同名的子目录,避免命名冲突
- 按类型细分目录(css/js/images)
- 图片按用途进一步分类(icons/avatars等)
4.2 CSS开发技巧
示例style.css:
css复制/* 使用CSS变量维护主题色 */
:root {
--primary-color: #2ecc71;
--secondary-color: #3498db;
}
/* 响应式设计 */
@media (max-width: 768px) {
.poll-item {
padding: 10px;
}
}
/* 避免全局样式污染 */
.poll-container .poll-item {
margin-bottom: 15px;
}
4.3 模板集成
在模板中使用:
html复制{% load static %}
<link rel="stylesheet" href="{% static 'polls/css/style.css' %}">
<script src="{% static 'polls/js/scripts.js' %}"></script>
<img src="{% static 'polls/images/logo.png' %}" alt="Logo">
注意事项:
- 开发环境DEBUG=True时Django会自动服务静态文件
- 生产环境需要配置Nginx/Apache或使用whitenoise
- 使用{% static %}标签确保路径正确
5. 生产环境部署要点
5.1 静态文件收集
部署前执行:
bash复制python manage.py collectstatic
这会将所有静态文件收集到STATIC_ROOT目录。
5.2 性能优化技巧
- 启用缓存:
python复制STATICFILES_STORAGE = 'django.contrib.staticfiles.storage.ManifestStaticFilesStorage'
- 使用CDN:
python复制STATIC_URL = 'https://cdn.example.com/static/'
- 压缩资源:
bash复制pip install django-compressor
5.3 安全注意事项
- 禁用目录列表:
python复制STATICFILES_DIRS = [os.path.join(BASE_DIR, 'static')]
- 设置合适的权限:
bash复制chmod 755 /path/to/static
chown www-data:www-data /path/to/static
- 内容安全策略(CSP):
python复制SECURE_CSP_STATIC_URL = "'self' cdn.example.com"
6. 测试覆盖率提升技巧
6.1 使用覆盖率工具
安装:
bash复制pip install coverage
运行:
bash复制coverage run manage.py test
coverage report
coverage html # 生成可视化报告
6.2 常用断言方法
| 方法 | 用途 | 示例 |
|---|---|---|
| assertContains | 检查响应内容 | self.assertContains(res, "Hello") |
| assertTemplateUsed | 检查模板使用 | self.assertTemplateUsed(res, 'polls/index.html') |
| assertRedirects | 检查重定向 | self.assertRedirects(res, '/login/') |
| assertFormError | 检查表单错误 | self.assertFormError(res, 'form', 'username', 'Required') |
6.3 测试性能优化
- 使用setUpTestData替代setUp:
python复制@classmethod
def setUpTestData(cls):
cls.user = User.objects.create(username='test')
- 使用TransactionTestCase谨慎:
- 会重置数据库,速度慢
- 只有测试事务相关代码时才需要
- 使用mock减少外部依赖:
python复制from unittest.mock import patch
@patch('polls.views.get_object_or_404')
def test_view(mock_get):
mock_get.return_value = Question(question_text='Mock')
response = self.client.get('/polls/1/')
self.assertContains(response, 'Mock')
7. 常见问题解决方案
7.1 静态文件404问题
检查清单:
- DEBUG=False时是否运行了collectstatic
- STATIC_ROOT是否正确配置
- Web服务器是否配置了静态文件路径
- 文件权限是否正确
7.2 测试数据库问题
常见错误:
- 测试间数据污染:确保使用TestCase而非SimpleTestCase
- 慢速测试:使用setUpTestData减少数据库操作
- 迁移问题:测试数据库会应用所有迁移
7.3 视图测试技巧
- 测试登录用户:
python复制self.client.force_login(self.user)
- 测试POST请求:
python复制response = self.client.post('/submit/', {'name': 'test'})
- 测试JSON API:
python复制response = self.client.get('/api/polls/', HTTP_ACCEPT='application/json')
8. 进阶测试策略
8.1 使用Factory Boy
替代硬编码模型创建:
python复制import factory
class QuestionFactory(factory.django.DjangoModelFactory):
class Meta:
model = Question
question_text = factory.Sequence(lambda n: f'Question {n}')
pub_date = timezone.now()
8.2 集成Selenium
端到端测试:
python复制from django.test import LiveServerTestCase
from selenium import webdriver
class PollsTest(LiveServerTestCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.driver = webdriver.Chrome()
def test_poll_creation(self):
self.driver.get(f'{self.live_server_url}/polls/')
self.assertIn('Polls', self.driver.title)
8.3 性能测试
使用django.test.utils.setup_test_environment控制测试环境:
python复制from django.test.utils import setup_test_environment
class PerformanceTest(TestCase):
@classmethod
def setUpClass(cls):
setup_test_environment()
cls.client = Client()
def test_homepage(self):
with self.assertNumQueries(3): # 确保查询次数可控
self.client.get('/')
9. 静态文件优化实战
9.1 使用django-compressor
安装配置:
python复制INSTALLED_APPS += ('compressor',)
STATICFILES_FINDERS += (
'compressor.finders.CompressorFinder',
)
模板使用:
html复制{% load compress %}
{% compress css %}
<link rel="stylesheet" href="{% static 'css/style.css' %}">
{% endcompress %}
9.2 图片优化技巧
- 使用WebP格式:
python复制# settings.py
STATICFILES_STORAGE = 'django.contrib.staticfiles.storage.ManifestStaticFilesStorage'
- 响应式图片:
html复制<picture>
<source srcset="{% static 'img/hero.webp' %}" type="image/webp">
<img src="{% static 'img/hero.jpg' %}" alt="Hero">
</picture>
9.3 前端构建集成
示例webpack配置:
javascript复制module.exports = {
entry: {
app: './static/src/js/app.js',
},
output: {
path: path.resolve(__dirname, 'static/dist'),
filename: '[name].bundle.js',
}
}
Django集成:
python复制STATICFILES_DIRS = [
os.path.join(BASE_DIR, 'static/dist'),
]
10. 持续集成配置
10.1 GitHub Actions示例
创建.github/workflows/test.yml:
yaml复制name: Django CI
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: |
python manage.py test
10.2 测试覆盖率徽章
- 安装coverage-badge:
bash复制pip install coverage-badge
- 生成徽章:
bash复制coverage run manage.py test
coverage-badge -o coverage.svg
- 添加到README.md:
markdown复制
11. 项目结构优化建议
11.1 测试目录结构
推荐结构:
code复制polls/
tests/
__init__.py
test_models.py
test_views.py
test_forms.py
factories.py
conftest.py
优势:
- 按功能模块分离测试
- 更容易维护和查找
- 支持pytest等高级特性
11.2 静态文件组织
生产级结构:
code复制project/
static/
css/
base.css
components/
buttons.css
cards.css
js/
app.js
modules/
utils.js
vendor/
bootstrap/
css/
js/
特点:
- 按功能而非类型组织
- 支持组件化开发
- 第三方库单独管理
12. 调试技巧与工具
12.1 测试调试技巧
- 使用--pdb选项:
bash复制python manage.py test --pdb
- 打印查询日志:
python复制from django.db import connection
class MyTest(TestCase):
def test_queries(self):
with self.assertNumQueries(3):
response = self.client.get('/')
print(connection.queries)
12.2 静态文件调试
- 检查静态文件查找:
bash复制python manage.py findstatic polls/css/style.css
- 开发服务器调试:
bash复制python manage.py runserver --insecure
- 使用django-debug-toolbar:
python复制DEBUG_TOOLBAR_CONFIG = {
'SHOW_TOOLBAR_CALLBACK': lambda request: True,
}
13. 安全最佳实践
13.1 测试安全防护
- CSRF测试:
python复制class SecurityTest(TestCase):
def test_csrf(self):
response = self.client.get('/login/')
self.assertContains(response, 'csrfmiddlewaretoken')
- XSS防护测试:
python复制def test_xss(self):
malicious = "<script>alert('xss')</script>"
response = self.client.post('/comment/', {'text': malicious})
self.assertNotContains(response, '<script>')
13.2 静态文件安全
- 内容安全策略:
python复制CSP_DEFAULT_SRC = ("'self'",)
CSP_SCRIPT_SRC = ("'self'", "'unsafe-inline'")
- 签名URL(使用S3时):
python复制from django.core.files.storage import default_storage
url = default_storage.url('protected/file.txt')
14. 性能测试进阶
14.1 使用django-silk
安装配置:
python复制MIDDLEWARE += ['silk.middleware.SilkyMiddleware']
INSTALLED_APPS += ['silk']
测试示例:
python复制from silk.profiling.profiler import silk_profile
@silk_profile(name='View Profiling')
def test_performance(self):
response = self.client.get('/heavy-view/')
self.assertEqual(response.status_code, 200)
14.2 数据库查询优化
- 使用select_related:
python复制def test_query_count(self):
with self.assertNumQueries(2):
# 原始查询可能产生N+1问题
questions = Question.objects.select_related('author')
for q in questions:
print(q.author.username)
- 使用prefetch_related:
python复制def test_m2m_query(self):
with self.assertNumQueries(2):
questions = Question.objects.prefetch_related('tags')
for q in questions:
print(q.tags.all())
15. 移动端适配策略
15.1 响应式测试
使用Selenium测试不同视口:
python复制def test_mobile_view(self):
self.driver.set_window_size(375, 812) # iPhone X尺寸
self.driver.get(self.live_server_url)
self.assertTrue(self.driver.find_element_by_class_name('mobile-menu'))
15.2 触摸事件测试
使用touch动作链:
python复制from selenium.webdriver.common.touch_actions import TouchActions
def test_swipe(self):
element = self.driver.find_element_by_id('carousel')
TouchActions(self.driver).flick_element(element, 0, -100, 0).perform()
16. 国际化测试策略
16.1 多语言测试
测试翻译字符串:
python复制from django.utils.translation import activate
def test_german_translation(self):
activate('de')
response = self.client.get('/')
self.assertContains(response, 'Willkommen')
16.2 本地化静态文件
组织方式:
code复制static/
polls/
css/
style-de.css
style-fr.css
js/
messages-de.js
模板选择:
html复制<link rel="stylesheet"
href="{% static 'polls/css/style-'|add:LANGUAGE_CODE|add:'.css' %}">
17. 无障碍测试指南
17.1 基础无障碍测试
使用axe-core集成:
python复制def test_accessibility(self):
self.driver.get(self.live_server_url)
result = self.driver.execute_async_script(
"var callback = arguments[arguments.length - 1];"
"axe.run(document, function(err, results) { callback(results); });")
self.assertEqual(len(result.violations), 0)
17.2 键盘导航测试
Selenium键盘操作:
python复制def test_keyboard_nav(self):
self.driver.get(self.live_server_url)
body = self.driver.find_element_by_tag_name('body')
body.send_keys(Keys.TAB)
self.assertEqual(
self.driver.switch_to.active_element.get_attribute('id'),
'main-content'
)
18. 微前端集成测试
18.1 组件隔离测试
使用Storybook测试独立组件:
javascript复制// polls.stories.js
export default {
title: 'Polls/QuestionItem',
component: QuestionItem,
}
export const Default = () => <QuestionItem question={...} />
18.2 契约测试
使用Pact进行前后端契约测试:
python复制@mock.pact
def test_api_contract(self):
(self.pact
.given('a question exists')
.upon_receiving('a request for questions')
.with_request('get', '/api/questions/')
.will_respond_with(200, body={
'id': Matcher.integer(),
'text': Matcher.string(),
}))
with self.pact:
response = self.client.get('/api/questions/')
self.assertEqual(response.status_code, 200)
19. 可视化测试策略
19.1 截图对比测试
使用percy.io集成:
python复制def test_homepage_snapshot(self):
self.driver.get(self.live_server_url)
PercySnapshot(self.driver, 'Homepage').capture()
19.2 视觉回归测试
使用dpxdt配置:
python复制def test_visual_regression(self):
self.driver.get(self.live_server_url)
screenshot = self.driver.get_screenshot_as_png()
self.assertScreenshotMatches('homepage', screenshot)
20. 混沌工程实践
20.1 延迟注入测试
使用chaos engineering:
python复制def test_latency_tolerance(self):
with patch('polls.views.time.sleep', side_effect=Exception):
response = self.client.get('/polls/', timeout=10)
self.assertEqual(response.status_code, 200)
20.2 故障注入测试
模拟数据库故障:
python复制def test_db_failure(self):
with patch('django.db.backends.base.base.BaseDatabaseWrapper.ensure_connection',
side_effect=DatabaseError):
response = self.client.get('/polls/')
self.assertEqual(response.status_code, 503)