测试
测试是软件开发的重要组成部分。Django 提供了完整的测试框架,帮助你编写和运行测试。
测试基础
创建测试文件
Django 默认在每个应用中创建 tests.py 文件。你也可以创建 tests 目录来组织测试:
blog/
└── tests/
├── __init__.py
├── test_models.py
├── test_views.py
└── test_forms.py
基本测试
# tests.py
from django.test import TestCase
from django.contrib.auth.models import User
from .models import Post, Category
class PostModelTest(TestCase):
"""模型测试"""
def setUp(self):
"""测试前准备"""
self.user = User.objects.create_user(
username='testuser',
password='testpass123'
)
self.category = Category.objects.create(
name='技术',
slug='tech'
)
self.post = Post.objects.create(
title='测试文章',
slug='test-post',
author=self.user,
content='这是测试内容',
category=self.category,
status='published'
)
def test_post_creation(self):
"""测试文章创建"""
self.assertEqual(self.post.title, '测试文章')
self.assertEqual(self.post.author, self.user)
self.assertEqual(self.post.status, 'published')
def test_post_str(self):
"""测试 __str__ 方法"""
self.assertEqual(str(self.post), '测试文章')
def test_post_ordering(self):
"""测试排序"""
post2 = Post.objects.create(
title='第二篇文章',
slug='second-post',
author=self.user,
content='内容',
status='published'
)
posts = Post.objects.all()
self.assertEqual(posts[0], post2) # 最新的在前
测试类型
模型测试
from django.test import TestCase
from django.contrib.auth.models import User
from .models import Post, Category
class CategoryModelTest(TestCase):
def test_category_creation(self):
category = Category.objects.create(name='技术', slug='tech')
self.assertEqual(str(category), '技术')
def test_category_slug_unique(self):
Category.objects.create(name='技术', slug='tech')
with self.assertRaises(Exception):
Category.objects.create(name='技术2', slug='tech')
class PostModelTest(TestCase):
def setUp(self):
self.user = User.objects.create_user(username='test', password='pass')
self.category = Category.objects.create(name='技术', slug='tech')
def test_post_publish_auto_set(self):
"""测试发布时间自动设置"""
post = Post.objects.create(
title='测试',
slug='test',
author=self.user,
content='内容'
)
self.assertIsNotNone(post.publish)
def test_post_default_status(self):
"""测试默认状态"""
post = Post.objects.create(
title='测试',
slug='test',
author=self.user,
content='内容'
)
self.assertEqual(post.status, 'draft')
视图测试
from django.test import TestCase, Client
from django.urls import reverse
from django.contrib.auth.models import User
from .models import Post, Category
class PostViewTest(TestCase):
"""视图测试"""
def setUp(self):
self.client = Client()
self.user = User.objects.create_user(
username='testuser',
password='testpass123'
)
self.category = Category.objects.create(name='技术', slug='tech')
self.post = Post.objects.create(
title='测试文章',
slug='test-post',
author=self.user,
content='内容',
category=self.category,
status='published'
)
def test_post_list_view(self):
"""测试文章列表视图"""
response = self.client.get(reverse('blog:post_list'))
self.assertEqual(response.status_code, 200)
self.assertContains(response, '测试文章')
self.assertTemplateUsed(response, 'blog/post_list.html')
def test_post_detail_view(self):
"""测试文章详情视图"""
response = self.client.get(
reverse('blog:post_detail', kwargs={'pk': self.post.pk})
)
self.assertEqual(response.status_code, 200)
self.assertContains(response, '测试文章')
def test_post_create_view_login_required(self):
"""测试创建文章需要登录"""
response = self.client.get(reverse('blog:post_create'))
self.assertNotEqual(response.status_code, 200)
def test_post_create_view_authenticated(self):
"""测试登录后可以创建文章"""
self.client.login(username='testuser', password='testpass123')
response = self.client.post(
reverse('blog:post_create'),
{
'title': '新文章',
'slug': 'new-post',
'content': '新内容',
'category': self.category.pk,
'status': 'published',
}
)
self.assertEqual(response.status_code, 302) # 重定向
self.assertEqual(Post.objects.count(), 2)
class PostDetailViewTest(TestCase):
"""文章详情视图测试"""
def setUp(self):
self.user = User.objects.create_user(username='test', password='pass')
self.post = Post.objects.create(
title='测试',
slug='test',
author=self.user,
content='内容',
status='published'
)
def test_view_increments_count(self):
"""测试阅读计数增加"""
initial_views = self.post.views
self.client.get(reverse('blog:post_detail', kwargs={'pk': self.post.pk}))
self.post.refresh_from_db()
self.assertEqual(self.post.views, initial_views + 1)
表单测试
from django.test import TestCase
from .forms import PostForm, ContactForm
class PostFormTest(TestCase):
"""表单测试"""
def test_valid_form(self):
"""测试有效表单"""
data = {
'title': '测试标题',
'slug': 'test-title',
'content': '测试内容',
'status': 'published',
}
form = PostForm(data=data)
self.assertTrue(form.is_valid())
def test_invalid_form_empty_title(self):
"""测试无效表单 - 空标题"""
data = {
'title': '',
'content': '内容',
}
form = PostForm(data=data)
self.assertFalse(form.is_valid())
self.assertIn('title', form.errors)
def test_invalid_form_short_title(self):
"""测试无效表单 - 标题太短"""
data = {
'title': 'ab',
'content': '内容',
}
form = PostForm(data=data)
self.assertFalse(form.is_valid())
class ContactFormTest(TestCase):
def test_email_validation(self):
"""测试邮箱验证"""
data = {
'name': '测试',
'email': 'invalid-email',
'message': '消息',
}
form = ContactForm(data=data)
self.assertFalse(form.is_valid())
self.assertIn('email', form.errors)
测试客户端
Django 的测试客户端可以模拟 HTTP 请求:
from django.test import TestCase, Client
class ClientTest(TestCase):
def setUp(self):
self.client = Client()
def test_get_request(self):
"""GET 请求"""
response = self.client.get('/path/')
self.assertEqual(response.status_code, 200)
def test_post_request(self):
"""POST 请求"""
response = self.client.post('/path/', {'key': 'value'})
self.assertEqual(response.status_code, 201)
def test_with_parameters(self):
"""带参数的请求"""
response = self.client.get('/path/', {'q': 'search'})
self.assertEqual(response.status_code, 200)
def test_with_headers(self):
"""带请求头的请求"""
response = self.client.get(
'/api/posts/',
HTTP_AUTHORIZATION='Token abc123'
)
def test_json_request(self):
"""JSON 请求"""
response = self.client.post(
'/api/posts/',
{'title': '测试'},
content_type='application/json'
)
def test_login(self):
"""登录"""
from django.contrib.auth.models import User
User.objects.create_user(username='test', password='pass')
logged_in = self.client.login(username='test', password='pass')
self.assertTrue(logged_in)
测试工具
TestCase 断言方法
class AssertionTest(TestCase):
def test_assertions(self):
# 相等断言
self.assertEqual(1 + 1, 2)
self.assertNotEqual(1 + 1, 3)
# 布尔断言
self.assertTrue(True)
self.assertFalse(False)
# 包含断言
self.assertIn(1, [1, 2, 3])
self.assertNotIn(4, [1, 2, 3])
# 异常断言
with self.assertRaises(ValueError):
int('abc')
# 近似相等
self.assertAlmostEqual(1.1, 1.10001, places=3)
# 大小比较
self.assertGreater(2, 1)
self.assertLess(1, 2)
# 类型断言
self.assertIsInstance('hello', str)
# None 断言
self.assertIsNone(None)
self.assertIsNotNone('value')
测试固件
from django.test import TestCase
from django.contrib.auth.models import User
class FixtureTest(TestCase):
fixtures = ['users.json', 'posts.json']
def test_with_fixture(self):
"""使用固件测试"""
user = User.objects.get(username='admin')
self.assertIsNotNone(user)
测试标签
from django.test import TestCase, tag
class TaggedTest(TestCase):
@tag('fast')
def test_fast(self):
"""快速测试"""
pass
@tag('slow')
def test_slow(self):
"""慢速测试"""
pass
# 运行特定标签的测试
# python manage.py test --tag=fast
运行测试
命令行
# 运行所有测试
python manage.py test
# 运行特定应用的测试
python manage.py test blog
# 运行特定测试文件
python manage.py test blog.tests.test_models
# 运行特定测试类
python manage.py test blog.tests.test_models.PostModelTest
# 运行特定测试方法
python manage.py test blog.tests.test_models.PostModelTest.test_post_creation
# 保持数据库
python manage.py test --keepdb
# 并行运行
python manage.py test --parallel
# 详细输出
python manage.py test --verbosity=2
测试覆盖率
pip install coverage
coverage run --source='.' manage.py test
coverage report
coverage html
最佳实践
1. 测试命名规范
class PostModelTest(TestCase):
# 好的命名
def test_post_creation_with_valid_data(self):
pass
def test_post_creation_without_title_raises_error(self):
pass
# 不好的命名
def test1(self):
pass
2. 使用 setUp 和 tearDown
class TestWithSetup(TestCase):
def setUp(self):
"""每个测试方法前执行"""
self.user = User.objects.create_user(username='test', password='pass')
def tearDown(self):
"""每个测试方法后执行"""
pass
def test_something(self):
# 可以使用 self.user
pass
3. 测试边界条件
class BoundaryTest(TestCase):
def test_empty_string(self):
"""空字符串"""
pass
def test_max_length(self):
"""最大长度"""
pass
def test_special_characters(self):
"""特殊字符"""
pass
4. 使用工厂模式
# factories.py
import factory
from .models import Post, Category
class UserFactory(factory.django.DjangoModelFactory):
class Meta:
model = 'auth.User'
username = factory.Sequence(lambda n: f'user{n}')
email = factory.LazyAttribute(lambda obj: f'{obj.username}@example.com')
class CategoryFactory(factory.django.DjangoModelFactory):
class Meta:
model = Category
name = factory.Sequence(lambda n: f'分类{n}')
slug = factory.LazyAttribute(lambda obj: f'cat-{obj.name}')
class PostFactory(factory.django.DjangoModelFactory):
class Meta:
model = Post
title = factory.Faker('sentence')
slug = factory.Faker('slug')
author = factory.SubFactory(UserFactory)
content = factory.Faker('text')
category = factory.SubFactory(CategoryFactory)
# tests.py
class PostTest(TestCase):
def test_with_factory(self):
post = PostFactory.create()
self.assertIsNotNone(post.title)