霍格沃兹测试开发学社

《Python测试开发进阶训练营》(随到随学!)
2023年第2期《Python全栈开发与自动化测试班》(开班在即)
报名联系weixin/qq:2314507862

从零开始:为你的第一个Django项目搭建测试环境

关注 霍格沃兹测试学院公众号,回复「资料」, 领取人工智能测试开发技术合集

你终于完成了那个Django博客应用的核心功能——文章发布、用户评论、标签分类,一切都运行得很完美。你兴奋地将代码部署到服务器,然后安心入睡。但第二天早上,你收到了一封紧急邮件:某个用户发现,当他尝试删除自己的账户时,系统意外删除了所有其他用户的评论。

这就是没有测试环境要付出的代价。

测试不是“有更好”的奢侈品,而是现代Web开发的必需品。今天,我将带你一步步为你的Django项目搭建一个完整的测试环境,让你能安心地部署代码,睡个安稳觉。

第一步:理解Django的测试框架
Django自带了一个强大的测试框架,基于Python的unittest模块。但在我们深入之前,先确保你的项目结构合理:

myproject/
├── manage.py
├── myproject/
│ ├── init.py
│ ├── settings.py
│ ├── urls.py
│ └── wsgi.py
└── myapp/
├── init.py
├── models.py
├── views.py
├── tests/
│ ├── init.py
│ ├── test_models.py
│ ├── test_views.py
│ └── test_forms.py
└── ...
注意那个tests/目录——这就是我们测试代码的家。Django会自动发现这个目录下以test_开头的文件。

第二步:配置测试设置
开发环境和测试环境的需求不同。我们不想在测试时发送真实的邮件,或者弄脏生产数据库。在settings.py中添加:

settings.py

import sys

在文件底部添加

if'test'in sys.argv:
# 使用更快的密码哈希器加速测试
PASSWORD_HASHERS = [
'django.contrib.auth.hashers.MD5PasswordHasher',
]

# 禁用邮件发送
EMAIL_BACKEND = 'django.core.mail.backends.locmem.EmailBackend'

# 使用SQLite内存数据库加速测试
DATABASES['default'] = {
    'ENGINE': 'django.db.backends.sqlite3',
    'NAME': ':memory:',
}

# 关闭DEBUG,更接近生产环境
DEBUG = False

# 添加其他测试专用配置
TESTING = True

第三步:编写你的第一个测试
让我们从一个简单的模型测试开始。假设你有一个博客应用:

blog/tests/test_models.py

from django.test import TestCase
from django.contrib.auth.models import User
from blog.models import Post
from django.utils import timezone
from django.core.exceptions import ValidationError

class PostModelTest(TestCase):
"""测试Post模型"""

def setUp(self):
    """每个测试方法前运行"""
    self.user = User.objects.create_user(
        username='testuser',
        password='testpass123',
        email='test@example.com'
    )
    
    self.post = Post.objects.create(
        title='测试文章标题',
        content='这里是文章内容',
        author=self.user,
        status='published'
    )

def test_post_creation(self):
    """测试文章创建"""
    self.assertEqual(self.post.title, '测试文章标题')
    self.assertEqual(self.post.author.username, 'testuser')
    self.assertTrue(isinstance(self.post.created_at, timezone.datetime))

def test_post_slug_auto_generation(self):
    """测试slug自动生成"""
    self.assertEqual(self.post.slug, 'ce-shi-wen-zhang-biao-ti')

def test_get_absolute_url(self):
    """测试获取文章URL"""
    expected_url = f'/blog/{self.post.slug}/'
    self.assertEqual(self.post.get_absolute_url(), expected_url)

def test_string_representation(self):
    """测试字符串表示"""
    self.assertEqual(str(self.post), '测试文章标题')

def test_invalid_status(self):
    """测试无效状态值"""
    with self.assertRaises(ValidationError):
        post = Post(
            title='无效状态测试',
            content='内容',
            author=self.user,
            status='invalid_status'# 无效值
        )
        post.full_clean()  # 这会触发验证

运行这个测试:

python manage.py test blog.tests.test_models.PostModelTest
如果一切正常,你会看到:

.....

Ran 5 tests in 0.023s
OK
Django技术学习交流群
伙伴们,对Django感兴趣吗?我们建了一个 「Django技术学习交流群」,专门用来探讨相关技术、分享资料、互通有无。无论你是正在实践还是好奇探索,都欢迎扫码加入,一起抱团成长!期待与你交流!👇

image

第四步:视图和API测试
模型测试很重要,但视图测试确保用户真正看到的内容是正确的:

blog/tests/test_views.py

from django.test import TestCase, Client
from django.urls import reverse
from django.contrib.auth.models import User
from blog.models import Post

class PostViewTest(TestCase):

def setUp(self):
    self.client = Client()
    self.user = User.objects.create_user(
        username='viewtestuser',
        password='testpass123'
    )
    
    # 创建一些测试文章
    self.published_post = Post.objects.create(
        title='已发布文章',
        content='内容',
        author=self.user,
        status='published'
    )
    
    self.draft_post = Post.objects.create(
        title='草稿文章',
        content='内容',
        author=self.user,
        status='draft'
    )

def test_post_list_view(self):
    """测试文章列表页"""
    response = self.client.get(reverse('post_list'))
    
    self.assertEqual(response.status_code, 200)
    self.assertTemplateUsed(response, 'blog/post_list.html')
    self.assertContains(response, '已发布文章')
    self.assertNotContains(response, '草稿文章')  # 草稿不应显示

def test_post_detail_view(self):
    """测试文章详情页"""
    url = reverse('post_detail', args=[self.published_post.slug])
    response = self.client.get(url)
    
    self.assertEqual(response.status_code, 200)
    self.assertContains(response, '已发布文章')
    self.assertContains(response, '内容')

def test_draft_post_not_public(self):
    """测试草稿文章不可公开访问"""
    url = reverse('post_detail', args=[self.draft_post.slug])
    response = self.client.get(url)
    
    self.assertEqual(response.status_code, 404)  # 应该返回404

def test_create_post_requires_login(self):
    """测试创建文章需要登录"""
    url = reverse('post_create')
    response = self.client.get(url)
    
    # 未登录用户应该被重定向到登录页
    self.assertEqual(response.status_code, 302)
    self.assertTrue(response.url.startswith('/accounts/login/'))

def test_authenticated_user_can_create_post(self):
    """测试已登录用户可以创建文章"""
    self.client.login(username='viewtestuser', password='testpass123')
    
    url = reverse('post_create')
    response = self.client.post(url, {
        'title': '新测试文章',
        'content': '新内容',
        'status': 'published'
    })
    
    # 成功后应该重定向
    self.assertEqual(response.status_code, 302)
    
    # 验证文章已创建
    self.assertTrue(Post.objects.filter(title='新测试文章').exists())

第五步:测试API端点
如果你的项目有API,也需要测试:

api/tests/test_views.py

from rest_framework.test import APITestCase, APIClient
from rest_framework import status
from django.contrib.auth.models import User
from blog.models import Post

class PostAPITest(APITestCase):

def setUp(self):
    self.client = APIClient()
    self.user = User.objects.create_user(
        username='apitestuser',
        password='testpass123'
    )
    
    self.post = Post.objects.create(
        title='API测试文章',
        content='API测试内容',
        author=self.user,
        status='published'
    )
    
    # 获取认证token(如果你的API使用Token认证)
    from rest_framework.authtoken.models import Token
    self.token = Token.objects.create(user=self.user)
    self.client.credentials(HTTP_AUTHORIZATION='Token ' + self.token.key)

def test_get_post_list(self):
    """测试获取文章列表API"""
    response = self.client.get('/api/posts/')
    
    self.assertEqual(response.status_code, status.HTTP_200_OK)
    self.assertEqual(len(response.data), 1)
    self.assertEqual(response.data[0]['title'], 'API测试文章')

def test_create_post_with_token_auth(self):
    """测试使用Token认证创建文章"""
    data = {
        'title': '通过API创建的文章',
        'content': 'API创建的内容',
        'status': 'published'
    }
    
    response = self.client.post('/api/posts/', data, format='json')
    
    self.assertEqual(response.status_code, status.HTTP_201_CREATED)
    self.assertEqual(Post.objects.count(), 2)

def test_unauthenticated_access_denied(self):
    """测试未认证访问被拒绝"""
    # 移除认证信息
    self.client.credentials()
    
    response = self.client.get('/api/posts/')
    self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)

第六步:配置测试数据库和工厂
随着测试增多,每次在setUp中创建对象会变得很繁琐。使用工厂模式可以解决这个问题:

blog/tests/factories.py

import factory
from django.contrib.auth.models import User
from blog.models import Post

class UserFactory(factory.django.DjangoModelFactory):
class Meta:
model = User

username = factory.Sequence(lambda n: f'user{n}')
email = factory.Sequence(lambda n: f'user{n}@example.com')

@classmethod
def _create(cls, model_class, *args, **kwargs):
    """重写创建方法以设置密码"""
    password = kwargs.pop('password', 'defaultpassword')
    user = super()._create(model_class, *args, **kwargs)
    user.set_password(password)
    user.save()
    return user

class PostFactory(factory.django.DjangoModelFactory):
class Meta:
model = Post

title = factory.Sequence(lambda n: f'测试文章标题 {n}')
content = factory.Faker('paragraph', nb_sentences=5)
author = factory.SubFactory(UserFactory)
status = 'published'

然后在测试中使用:

blog/tests/test_with_factories.py

from django.test import TestCase
from blog.tests.factories import PostFactory, UserFactory

class FactoryBasedTest(TestCase):

def test_multiple_posts(self):
    """使用工厂创建多个测试对象"""
    # 创建5篇文章
    posts = PostFactory.create_batch(5)
    
    self.assertEqual(len(posts), 5)
    
    # 每篇文章都有不同的标题
    titles = [post.title for post in posts]
    self.assertEqual(len(set(titles)), 5)

def test_custom_factory_attributes(self):
    """使用自定义属性创建对象"""
    post = PostFactory(
        title='自定义标题',
        status='draft'
    )
    
    self.assertEqual(post.title, '自定义标题')
    self.assertEqual(post.status, 'draft')

第七步:配置持续集成
测试只有定期运行才有价值。在项目根目录创建.github/workflows/test.yml:

name: DjangoTests

on:
push:
branches:[main,develop]
pull_request:
branches:[main]

jobs:
test:
runs-on:ubuntu-latest

services:
  postgres:
    image:postgres:13
    env:
      POSTGRES_PASSWORD:postgres
    options:>-
      --health-cmd pg_isready
      --health-interval 10s
      --health-timeout 5s
      --health-retries 5
    ports:
      -5432:5432

steps:
-uses:actions/checkout@v2

-name:SetupPython
  uses:actions/setup-python@v2
  with:
    python-version:'3.9'

-name:Installdependencies
  run:|
    python -m pip install --upgrade pip
    pip install -r requirements.txt

-name:Runmigrations
  env:
    DATABASE_URL:postgres://postgres:postgres@localhost:5432/postgres
    SECRET_KEY:test-secret-key
  run:|
    python manage.py migrate

-name:Runtests
  env:
    DATABASE_URL:postgres://postgres:postgres@localhost:5432/postgres
    SECRET_KEY:test-secret-key
  run:|
    python manage.py test --parallel=4

-name:Generatecoveragereport
  run: |
    pip install coverage
    coverage run --source='.' manage.py test
    coverage report
    coverage html

第八步:实用技巧和最佳实践
测试隔离:每个测试都应该是独立的。使用setUp和tearDown确保测试之间不相互影响。

有意义的测试名称:测试名称应该清晰地说明测试的目的:

不好

def test_1(self):

def test_user_cannot_login_with_wrong_password(self):
测试失败信息:提供有用的失败信息:

不好

self.assertEqual(response.status_code, 200)

self.assertEqual(
response.status_code,
200,
f"Expected status 200, got {response.status_code}. Response: {response.content}"
)
不要过度测试:测试重要的业务逻辑,而不是Django或第三方库已经测试过的功能。

定期运行测试:在本地开发时,养成经常运行测试的习惯:

运行所有测试

python manage.py test

运行特定app的测试

python manage.py test blog

运行特定测试类

python manage.py test blog.tests.test_views.PostViewTest

运行单个测试方法

python manage.py test blog.tests.test_views.PostViewTest.test_post_list_view

并行运行测试(加速)

python manage.py test --parallel=4
遇到问题怎么办?
当你运行测试时,可能会遇到一些常见问题:

数据库问题:确保测试数据库正确配置。Django默认会创建一个测试数据库,测试结束后会自动销毁。

静态文件:测试可能不加载静态文件。如果需要,使用django.contrib.staticfiles.testing.StaticLiveServerTestCase。

耗时太长:如果测试运行太慢:

使用SQLite内存数据库
使用--parallel选项
避免在测试中使用真实的外部API
测试覆盖率:了解你的测试覆盖了多少代码:

pip install coverage
coverage run --source='.' manage.py test
coverage report
coverage html # 生成HTML报告
结语
搭建测试环境看似是额外的工作,但实际上它为你节省的是未来数小时甚至数天的调试时间。当你修复一个bug时,测试能确保你不会引入新的bug;当你重构代码时,测试给你信心;当你添加新功能时,测试文档化了代码的预期行为。

记住,好的测试不是100%的覆盖率,而是测试了正确的东西。从今天开始,为你写的每一段新代码都加上测试。几个月后,当你的项目变得复杂时,你会感谢现在开始测试的自己。

现在,去运行你的测试吧。如果所有测试都通过,今晚你应该能睡个好觉了。

推荐学习
Ai自动化智能体与工作流平台课程,限时免费,机会难得。扫码报名,参与直播,希望您在这场课程中收获满满,开启智能自动化测试的新篇章!

image

posted @ 2026-01-23 16:33  霍格沃兹测试开发学社  阅读(1)  评论(0)    收藏  举报