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

14 阅读8分钟

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

你终于完成了那个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,更接近生产环境
    DEBUGFalse
    
    # 添加其他测试专用配置
    TESTINGTrue

第三步:编写你的第一个测试

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

# 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.png

第四步:视图和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

第八步:实用技巧和最佳实践

  1. 测试隔离:每个测试都应该是独立的。使用setUptearDown确保测试之间不相互影响。

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

    # 不好
    def test_1(self):
    
    # 好
    def test_user_cannot_login_with_wrong_password(self):
    
  3. 测试失败信息:提供有用的失败信息:

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

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

    # 运行所有测试
    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
    

遇到问题怎么办?

当你运行测试时,可能会遇到一些常见问题:

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

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

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

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

    pip install coverage
    coverage run --source='.' manage.py test
    coverage report
    coverage html  # 生成HTML报告
    

结语

搭建测试环境看似是额外的工作,但实际上它为你节省的是未来数小时甚至数天的调试时间。当你修复一个bug时,测试能确保你不会引入新的bug;当你重构代码时,测试给你信心;当你添加新功能时,测试文档化了代码的预期行为。

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

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

推荐学习

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

image.png