Skip to content

第15章:测试

15.1 单元测试

TestCase类

Django提供了强大的测试框架,基于Python的unittest模块:

python
# tests.py
from django.test import TestCase
from django.contrib.auth.models import User
from django.urls import reverse
from .models import Article, Category, Tag

class ArticleModelTest(TestCase):
    """文章模型测试"""
    
    def setUp(self):
        """测试前的准备工作"""
        self.user = User.objects.create_user(
            username='testuser',
            email='test@example.com',
            password='testpass123'
        )
        self.category = Category.objects.create(
            name='测试分类',
            slug='test-category'
        )
        self.tag = Tag.objects.create(
            name='测试标签',
            slug='test-tag'
        )
    
    def test_article_creation(self):
        """测试文章创建"""
        article = Article.objects.create(
            title='测试文章',
            slug='test-article',
            author=self.user,
            category=self.category,
            content='这是测试文章的内容',
            excerpt='这是测试文章的摘要',
            status='published'
        )
        article.tags.add(self.tag)
        
        # 验证文章是否正确创建
        self.assertEqual(article.title, '测试文章')
        self.assertEqual(article.author, self.user)
        self.assertEqual(article.category, self.category)
        self.assertIn(self.tag, article.tags.all())
        self.assertTrue(article.is_published)
    
    def test_article_string_representation(self):
        """测试文章字符串表示"""
        article = Article.objects.create(
            title='测试文章标题',
            slug='test-article',
            author=self.user,
            content='内容'
        )
        self.assertEqual(str(article), '测试文章标题')
    
    def test_article_get_absolute_url(self):
        """测试文章绝对URL"""
        article = Article.objects.create(
            title='测试文章',
            slug='test-article',
            author=self.user,
            content='内容'
        )
        expected_url = reverse('blog:article_detail', kwargs={'slug': 'test-article'})
        self.assertEqual(article.get_absolute_url(), expected_url)

class CategoryModelTest(TestCase):
    """分类模型测试"""
    
    def test_category_creation(self):
        """测试分类创建"""
        category = Category.objects.create(
            name='技术',
            description='技术相关文章'
        )
        self.assertEqual(category.name, '技术')
        self.assertEqual(category.slug, 'ji-zhu')  # 自动创建的slug
        self.assertEqual(str(category), '技术')
    
    def test_category_slug_generation(self):
        """测试分类slug自动生成"""
        category = Category.objects.create(name='Python编程')
        self.assertEqual(category.slug, 'pythonbian-cheng')

class ArticleViewTest(TestCase):
    """文章视图测试"""
    
    def setUp(self):
        self.user = User.objects.create_user(
            username='testuser',
            password='testpass123'
        )
        self.category = Category.objects.create(name='测试分类')
        self.article = Article.objects.create(
            title='测试文章',
            author=self.user,
            category=self.category,
            content='测试内容',
            status='published'
        )
    
    def test_article_list_view(self):
        """测试文章列表视图"""
        response = self.client.get(reverse('blog:article_list'))
        self.assertEqual(response.status_code, 200)
        self.assertContains(response, '测试文章')
        self.assertTemplateUsed(response, 'blog/article_list.html')
    
    def test_article_detail_view(self):
        """测试文章详情视图"""
        response = self.client.get(
            reverse('blog:article_detail', kwargs={'slug': self.article.slug})
        )
        self.assertEqual(response.status_code, 200)
        self.assertContains(response, '测试文章')
        self.assertContains(response, '测试内容')
        self.assertTemplateUsed(response, 'blog/article_detail.html')
    
    def test_article_detail_view_404(self):
        """测试不存在的文章详情视图"""
        response = self.client.get(
            reverse('blog:article_detail', kwargs={'slug': 'non-existent'})
        )
        self.assertEqual(response.status_code, 404)

# 使用工厂模式简化测试数据创建
# factories.py
import factory
from django.contrib.auth.models import User
from .models import Article, Category, Tag

class UserFactory(factory.django.DjangoModelFactory):
    class Meta:
        model = User
    
    username = factory.Sequence(lambda n: f"user{n}")
    email = factory.LazyAttribute(lambda obj: f"{obj.username}@example.com")
    password = factory.PostGenerationMethodCall('set_password', 'defaultpass123')

class CategoryFactory(factory.django.DjangoModelFactory):
    class Meta:
        model = Category
    
    name = factory.Sequence(lambda n: f"分类{n}")
    description = factory.Faker('text', max_nb_chars=200)

class TagFactory(factory.django.DjangoModelFactory):
    class Meta:
        model = Tag
    
    name = factory.Sequence(lambda n: f"标签{n}")

class ArticleFactory(factory.django.DjangoModelFactory):
    class Meta:
        model = Article
    
    title = factory.Faker('sentence', nb_words=6)
    content = factory.Faker('text', max_nb_chars=1000)
    excerpt = factory.Faker('text', max_nb_chars=200)
    author = factory.SubFactory(UserFactory)
    category = factory.SubFactory(CategoryFactory)
    status = 'published'

# 使用工厂的测试示例
class ArticleFactoryTest(TestCase):
    """使用工厂的测试示例"""
    
    def test_create_multiple_articles(self):
        """测试创建多个文章"""
        # 创建5篇文章
        articles = ArticleFactory.create_batch(5)
        self.assertEqual(len(articles), 5)
        self.assertEqual(Article.objects.count(), 5)
    
    def test_create_article_with_tags(self):
        """测试创建带标签的文章"""
        tags = TagFactory.create_batch(3)
        article = ArticleFactory(tags=tags)
        self.assertEqual(article.tags.count(), 3)

数据库测试

数据库相关的测试:

python
# test_models.py
from django.test import TestCase
from django.db import IntegrityError
from django.core.exceptions import ValidationError
from .models import Article, Category, Tag

class ModelDatabaseTest(TestCase):
    """模型数据库测试"""
    
    def setUp(self):
        self.user = UserFactory()
        self.category = CategoryFactory()
    
    def test_unique_slug_constraint(self):
        """测试唯一slug约束"""
        ArticleFactory(slug='unique-slug')
        with self.assertRaises(IntegrityError):
            ArticleFactory(slug='unique-slug')
    
    def test_category_name_unique(self):
        """测试分类名称唯一性"""
        CategoryFactory(name='唯一分类')
        with self.assertRaises(IntegrityError):
            CategoryFactory(name='唯一分类')
    
    def test_article_validation(self):
        """测试文章验证"""
        article = Article(
            title='',  # 空标题应该引发验证错误
            author=self.user,
            content='内容'
        )
        with self.assertRaises(ValidationError):
            article.full_clean()
    
    def test_article_save_method(self):
        """测试文章保存方法"""
        article = ArticleFactory(
            title='测试文章',
            excerpt=''  # 空摘要应该自动生成
        )
        self.assertTrue(len(article.excerpt) > 0)
    
    def test_foreign_key_cascade(self):
        """测试外键级联删除"""
        article = ArticleFactory(category=self.category)
        category_id = self.category.id
        
        # 删除分类
        self.category.delete()
        
        # 文章应该仍然存在,但分类为空
        article.refresh_from_db()
        self.assertIsNone(article.category)
        self.assertTrue(Article.objects.filter(id=article.id).exists())

表单测试

表单测试确保表单验证和处理逻辑正确:

python
# test_forms.py
from django.test import TestCase
from .forms import ArticleForm, CommentForm
from .models import Article, Category, Tag

class FormTest(TestCase):
    """表单测试"""
    
    def setUp(self):
        self.user = UserFactory()
        self.category = CategoryFactory()
        self.tag = TagFactory()
    
    def test_article_form_valid_data(self):
        """测试文章表单有效数据"""
        form_data = {
            'title': '测试文章',
            'category': self.category.id,
            'content': '文章内容',
            'excerpt': '文章摘要',
            'status': 'published'
        }
        form = ArticleForm(data=form_data)
        self.assertTrue(form.is_valid())
    
    def test_article_form_invalid_data(self):
        """测试文章表单无效数据"""
        form_data = {
            'title': '',  # 标题为空
            'content': '',  # 内容为空
        }
        form = ArticleForm(data=form_data)
        self.assertFalse(form.is_valid())
        self.assertIn('title', form.errors)
        self.assertIn('content', form.errors)
    
    def test_article_form_save(self):
        """测试文章表单保存"""
        form_data = {
            'title': '测试文章',
            'category': self.category.id,
            'content': '文章内容',
            'excerpt': '文章摘要',
            'status': 'published'
        }
        form = ArticleForm(data=form_data)
        self.assertTrue(form.is_valid())
        
        article = form.save(commit=False)
        article.author = self.user
        article.save()
        
        self.assertEqual(article.title, '测试文章')
        self.assertEqual(article.author, self.user)
    
    def test_comment_form_valid_data(self):
        """测试评论表单有效数据"""
        form_data = {
            'content': '这是一条评论'
        }
        form = CommentForm(data=form_data)
        self.assertTrue(form.is_valid())
    
    def test_comment_form_empty_content(self):
        """测试评论表单空内容"""
        form_data = {
            'content': ''
        }
        form = CommentForm(data=form_data)
        self.assertFalse(form.is_valid())
        self.assertIn('content', form.errors)

视图测试

视图测试确保HTTP请求处理正确:

python
# test_views.py
from django.test import TestCase
from django.urls import reverse
from django.contrib.auth.models import User
from .models import Article, Category

class ViewTest(TestCase):
    """视图测试"""
    
    def setUp(self):
        self.user = User.objects.create_user(
            username='testuser',
            password='testpass123'
        )
        self.staff_user = User.objects.create_user(
            username='staffuser',
            password='staffpass123',
            is_staff=True
        )
        self.category = Category.objects.create(name='测试分类')
        self.article = Article.objects.create(
            title='测试文章',
            author=self.user,
            category=self.category,
            content='测试内容',
            status='published'
        )
    
    def test_home_page_status_code(self):
        """测试首页状态码"""
        response = self.client.get(reverse('blog:home'))
        self.assertEqual(response.status_code, 200)
    
    def test_home_page_contains_correct_html(self):
        """测试首页包含正确HTML"""
        response = self.client.get(reverse('blog:home'))
        self.assertContains(response, '测试文章')
        self.assertContains(response, '<title>')
    
    def test_home_page_uses_correct_template(self):
        """测试首页使用正确模板"""
        response = self.client.get(reverse('blog:home'))
        self.assertTemplateUsed(response, 'blog/article_list.html')
    
    def test_article_detail_view_with_existing_article(self):
        """测试存在的文章详情视图"""
        response = self.client.get(
            reverse('blog:article_detail', kwargs={'slug': self.article.slug})
        )
        self.assertEqual(response.status_code, 200)
        self.assertContains(response, '测试文章')
    
    def test_article_detail_view_with_nonexistent_article(self):
        """测试不存在的文章详情视图"""
        response = self.client.get(
            reverse('blog:article_detail', kwargs={'slug': 'nonexistent'})
        )
        self.assertEqual(response.status_code, 404)
    
    def test_create_article_view_requires_login(self):
        """测试创建文章视图需要登录"""
        response = self.client.get(reverse('blog:article_create'))
        # 应该重定向到登录页面
        self.assertEqual(response.status_code, 302)
    
    def test_create_article_view_with_authenticated_user(self):
        """测试已认证用户创建文章视图"""
        self.client.login(username='testuser', password='testpass123')
        response = self.client.get(reverse('blog:article_create'))
        self.assertEqual(response.status_code, 200)
    
    def test_search_view(self):
        """测试搜索视图"""
        response = self.client.get(reverse('blog:search'), {'q': '测试'})
        self.assertEqual(response.status_code, 200)
        self.assertContains(response, '测试文章')
    
    def test_category_detail_view(self):
        """测试分类详情视图"""
        response = self.client.get(
            reverse('blog:category_detail', kwargs={'slug': self.category.slug})
        )
        self.assertEqual(response.status_code, 200)
        self.assertContains(response, '测试文章')

通过这些单元测试,可以确保应用程序的各个组件按预期工作,并在代码更改时快速发现问题。

小结

Django单元测试的核心要点:

  1. ✅ 使用TestCase类进行模型、视图和表单测试
  2. ✅ 实施数据库测试验证数据完整性和约束
  3. ✅ 编写表单测试确保验证逻辑正确
  4. ✅ 执行视图测试验证HTTP请求处理
  5. ✅ 使用工厂模式简化测试数据创建

完善的测试体系是保证代码质量和应用稳定性的关键。

下一篇

我们将学习集成测试。

15.2 集成测试 →

目录

返回课程目录

Released under the Apache 2.0 License.