文章目錄
- 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 的測試。現在,我們想知道的是,到底我們的測試效果怎么樣呢?測試充分嗎?測試全面嗎?還有沒有沒有測到的地方呢?
單憑肉眼觀察難以回答上面的問題,接下來我們就借助一個工具,從代碼覆蓋率的角度來檢測一下我們的測試效果究竟如何。