文章目录
- 1. 前言
- 2. 数据基类
- 3.测试 Comment Model
- 4. 测试视图函数
- 5. 测试模板标签
1. 前言
comments应用的测试和blog应用测试的套路是一样的。
先来建立测试文件的目录结构。首先在 comments 应用的目录下建立一个名为 tests 的 Python 包,然后删除 comments 应用下 django 自动生成的 tests.py 文件,防止和 tests 包冲突,再根据需要测试的内容,创建相应的 Python 模块。最终 tests 目录结构如下,其中 base.py 用于存放各个测试用例的公共的数据初始化基类。
2. 数据基类
由于评论必须和文章关联,因此我们首先来写一个数据基类,用于初始化生成文章数据,其它测试类继承这个数据基类,从而不用在每个测试类里都写一遍创建文章数据的代码了。数据基类写在 base.py 模块里:
文件位置:comments/tests/base.py
from django.apps import apps
from django.contrib.auth.models import User
from django.test import TestCasefrom blog.models import Category, Postclass CommentDataTestCase(TestCase):def setUp(self):apps.get_app_config('haystack').signal_processor.teardown()self.user = User.objects.create_superuser(username='admin',email='admin@hellogithub.com',password='admin')self.cate = Category.objects.create(name='测试')self.post = Post.objects.create(title='测试标题',body='测试内容',category=self.cate,author=self.user,)
要注意创建文章数据时,使用 apps.get_app_config(‘haystack’).signal_processor.teardown() 断开创建索引的信号
3.测试 Comment Model
先回顾一下comments应用的models.py
from django.db import models
from django.utils import timezoneclass Comment(models.Model):name = models.CharField('名字', max_length=50)email = models.EmailField('邮箱')url = models.URLField('网址', blank=True)text = models.TextField('内容')created_time = models.DateTimeField('创建时间', default=timezone.now)post = models.ForeignKey('blog.Post', verbose_name='文章', on_delete=models.CASCADE)class Meta:verbose_name = '评论'verbose_name_plural = verbose_nameordering = ['-created_time']def __str__(self):return '{}: {}'.format(self.name, self.text[:20])
Comment Model 的代码逻辑比较简单,测试起来也很简单:
文件位置:comments/tests/test_models.py
from .base import CommentDataTestCase
from ..models import Commentclass CommentModelTestCase(CommentDataTestCase):def setUp(self):super().setUp()self.comment = Comment.objects.create(name='评论者',email='a@a.com',text='评论内容',post=self.post,)def test_str_representation(self):self.assertEqual(self.comment.__str__(), '评论者: 评论内容')
4. 测试视图函数
我们只有一个发表评论的视图函数,首先回顾一下:
from blog.models import Post
from django.shortcuts import get_object_or_404, redirect, render
from django.views.decorators.http import require_POSTfrom .forms import CommentForm
from django.contrib import messages@require_POST
def comment(request, post_pk):# 先获取被评论的文章,因为后面需要把评论和被评论的文章关联起来。# 这里我们使用了 Django 提供的一个快捷函数 get_object_or_404,# 这个函数的作用是当获取的文章(Post)存在时,则获取;否则返回 404 页面给用户。post = get_object_or_404(Post, pk=post_pk)# django 将用户提交的数据封装在 request.POST 中,这是一个类字典对象。# 我们利用这些数据构造了 CommentForm 的实例,这样就生成了一个绑定了用户提交数据的表单。form = CommentForm(request.POST)# 当调用 form.is_valid() 方法时,Django 自动帮我们检查表单的数据是否符合格式要求。if form.is_valid():# 检查到数据是合法的,调用表单的 save 方法保存数据到数据库,# commit=False 的作用是仅仅利用表单的数据生成 Comment 模型类的实例,但还不保存评论数据到数据库。comment = form.save(commit=False)# 将评论和被评论的文章关联起来。comment.post = post# 最终将评论数据保存进数据库,调用模型实例的 save 方法comment.save()messages.add_message(request, messages.SUCCESS, '评论发表成功!', extra_tags='success')# 重定向到 post 的详情页,实际上当 redirect 函数接收一个模型的实例时,它会调用这个模型实例的 get_absolute_url 方法,# 然后重定向到 get_absolute_url 方法返回的 URL。return redirect(post)# 检查到数据不合法,我们渲染一个预览页面,用于展示表单的错误。# 注意这里被评论的文章 post 也传给了模板,因为我们需要根据 post 来生成表单的提交地址。context = {'post': post,'form': form,}messages.add_message(request, messages.ERROR, '评论发表失败!请修改表单中的错误后重新提交。', extra_tags='danger')return render(request, 'comments/preview.html', context=context)
根据视图函数的逻辑,需要测试以下几点:
- 只处理 POST 请求,其它请求将返回 405 Method Not Allowed 错误码。
- 如果评论的文章不存在,返回 404 错误码。
- 如果提交的评论内容有错误(例如 email 格式不正确),将渲染 preview.html 预览页面,并且预览页面显示评论出错的消息提醒和评论表单中包含的错误。
- 提交的内容合法,则创建评论,用户被重定向回被评论文章的详情页,页面中包含评论成功的消息提醒。
具体代码如下:
文件位置:comments/tests/test_views.py
from django.apps import apps
from django.contrib.auth.models import User
from django.urls import reversefrom blog.models import Category, Postfrom ..models import Comment
from .base import CommentDataTestCaseclass CommentViewTestCase(CommentDataTestCase):def setUp(self) -> None:super().setUp()self.url = reverse("comments:comment", kwargs={"post_pk": self.post.pk})def test_comment_a_nonexistent_post(self):url = reverse("comments:comment", kwargs={"post_pk": 100})response = self.client.post(url, {})self.assertEqual(response.status_code, 404)def test_invalid_comment_data(self):invalid_data = {"email": "invalid_email",}response = self.client.post(self.url, invalid_data)self.assertTemplateUsed(response, "comments/preview.html")self.assertIn("post", response.context)self.assertIn("form", response.context)form = response.context["form"]for field_name, errors in form.errors.items():for err in errors:self.assertContains(response, err)self.assertContains(response, "评论发表失败!请修改表单中的错误后重新提交。")def test_valid_comment_data(self):valid_data = {"name": "评论者","email": "a@a.com","text": "评论内容",}response = self.client.post(self.url, valid_data, follow=True)self.assertRedirects(response, self.post.get_absolute_url())self.assertContains(response, "评论发表成功!")self.assertEqual(Comment.objects.count(), 1)comment = Comment.objects.first()self.assertEqual(comment.name, valid_data["name"])self.assertEqual(comment.text, valid_data["text"])
- 在 test_invalid_comment_data 测试用例。这个测试用例中,我们构造了一个缺失评论内容、评论人名字且邮箱格式不正确的数据,然后将其提交了评论。接着就是对预期结果的断言。这里关键的一点是,渲染的预览页面应该包含提示用户的表单错误。所以我们从响应的上下文变量中取得表单 form 这个模板变量。接着使用如下代码获取表单的错误并断言响应中是否包含了这些错误:
for field_name, errors in form.errors.items():for err in errors:self.assertContains(response, err)
一旦表单绑定了数据,并且 is_valid 方法被调用,就会有一个 errors 属性(参考评论视图函数中表单的处理逻辑)。errors 属性是一个类字典对象,如果表单数据不包含错误,则为空;如果包含错误数据,则其键为包含错误数据的字段名称,值为该字段错误提示构成的列表(一个字段可能包含多个错误,所以是一个列表)。例如这里的 form.errors,如果将其打印出来(使用 print(repr(form.errors)),str 方法返回的内容是经渲染的 ul 列表),可以看到它的内容如下:
{'name': ['这个字段是必填项。'], 'email': ['输入一个有效的 Email 地址。'], 'text': ['这个字段是必填项。']}
- test_valid_comment_data 中,我们构造合法的评论内容并提交,预期结果是评论提交成功后重定向到被评论文章的详情页,所以使用了 assertRedirects 进行断言。
注意 self.client.post(self.url, valid_data, follow=True) 传入的 follow=True 参数。由于评论成功后需要重定向,因此传入 follow=True,表示跟踪重定向,因此返回的响应,是最终重定向之后返回的响应(即被评论文章的详情页),如果传入 False,则不会追踪重定向,返回的响应就是一个响应码为 302 的重定向前响应。
对于重定向响应,使用 assertRedirects 进行断言,这个断言方法会对重定向的整个响应的过程进行检测,默认检测的是响应码从 302 变为 200。
5. 测试模板标签
上一篇中介绍过模板标签的测试方法。基本套路就是代替 django 视图函数自动渲染模板内容的过程,手工构造一个包含待测试模板标签的模板,然后手工渲染其内容,断言渲染后的内容是否包含预期的内容。具体代码请看源代码,这里不再一一讲解,只将涉及的几个新的表单操作进行一个简单介绍。
class CommentExtraTestCase(CommentDataTestCase):# ...省略其它测试用例的代码def test_show_comment_form_with_invalid_bound_form(self):template = Template('{% load comments_extras %}''{% show_comment_form post form %}')invalid_data = {'email': 'invalid_email',}form = CommentForm(data=invalid_data)self.assertFalse(form.is_valid())context = Context(show_comment_form(self.ctx, self.post, form=form))expected_html = template.render(context)for field in form:label = '<label for="{}">{}:</label>'.format(field.id_for_label, field.label)self.assertInHTML(label, expected_html)self.assertInHTML(str(field), expected_html)self.assertInHTML(str(field.errors), expected_html)
看到循环表单 form 的语句:
for field in form:label = '<label for="{}">{}:</label>'.format(field.id_for_label, field.label)
我们这里使用了 field 的两个属性,id_for_label 和 id_for_label,分别是 django 表单自动生成的表单字段 label 的 id 和 label 名。别的就没什么好说的了,就是不停地断言页面包含预期的 HTML 内容。
至此,我们完成了对 blog 应用和 comment 应用这两个核心 app 的测试。现在,我们想知道的是,到底我们的测试效果怎么样呢?测试充分吗?测试全面吗?还有没有没有测到的地方呢?
单凭肉眼观察难以回答上面的问题,接下来我们就借助一个工具,从代码覆盖率的角度来检测一下我们的测试效果究竟如何。