使用django-test-plus库进行单元测试
编码前介绍
django-test-plus库简介
django-test-plus库是对Django框架自带的单元测试模块的封装,提供更加方便的接口供我们使用。
Django项目需要进行单元测试的地方有:
- url
- model类
- view视图函数或者类视图函数
Django项目是以APP的形式构建的,所以我们一般在写完一个APP后就要及时的进行单元测试保证该模块的质量。
在每个APP模块下都建立tests包
注意是包(文件夹下有__init__
文件)不是tests文件夹
该包下包括测试urls的模块,测试models的模块,测试views的模块,需要注意的是这些模块都要以test_
开头:
编写url测试
url需要测试地方包括url的正向解析和反向解析
我news模块的url如下:
步骤一:
在该模块的tests包(自己建立的)下建立test_urls.py文件:
步骤二:
导入需要的模块:
from django.urls import reverse, resolve
from test_plus.test import TestCase
注:确保先安装了django-test-plus包
步骤三:
可以开始编写测试类了,一般测试urls从两个方面测试:
- url的正向解析是否正确
- url的反向解析是否正确
单元测试代码如下:
测试结果如下:
编写model类单元测试
步骤一:
在该模块的tests包(自己建立的)下建立test_models.py文件:
步骤二:
导入所需模块和models:
from test_plus.test import TestCase
from app01.news.models import News
步骤三:
可以编写模型类的测试用例了,一般根据模型类的业务内容进行编写,比如说我的是动态模型类,model代码如下,字段包括用户发表的动态以及对该动态的评论和点赞,其中还有自定义的方法,在编写模型类的时候就需要发挥自己的想象力根据实际情况来定了:
#!/usr/bin/python3
# -*- coding:utf-8 -*-
# __author__ = 'zzy'
from __future__ import unicode_literals
import uuid
from django.db import models
from django.utils.encoding import python_2_unicode_compatible
from django.conf import settings
from asgiref.sync import async_to_sync
from channels.layers import get_channel_layer
from app01.notifications.views import notification_handler
@python_2_unicode_compatible
class News(models.Model):
uuid_id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
user = models.ForeignKey(settings.AUTH_USER_MODEL, blank=True, null=True, on_delete=models.SET_NULL,
related_name='publisher', verbose_name='用户')
parent = models.ForeignKey("self", blank=True, null=True, on_delete=models.CASCADE,
related_name='thread', verbose_name='自关联')
content = models.TextField(verbose_name='动态内容')
liked = models.ManyToManyField(settings.AUTH_USER_MODEL, related_name='liked_news', verbose_name='点赞')
reply = models.BooleanField(default=False, verbose_name='是否为评论')
created_at = models.DateTimeField(db_index=True, auto_now_add=True, verbose_name='创建时间')
updated_at = models.DateTimeField(auto_now=True, verbose_name='更新时间')
class Meta:
verbose_name = '首页'
verbose_name_plural = verbose_name
ordering = ("-created_at",)
def __str__(self):
return self.content
def save(self, *args, **kwargs):
super(News, self).save(*args, **kwargs)
if not self.reply:
channel_layer = get_channel_layer()
payload = {
"type": "receive",
"key": "additional_news",
"actor_name": self.user.username
}
async_to_sync(channel_layer.group_send)('notifications', payload)
def switch_like(self, user):
"""点赞或取消"""
# 如果用户已经赞过,则取消赞
if user in self.liked.all():
self.liked.remove(user)
else:
# 如果用户没有赞过,则添加赞
self.liked.add(user)
# # 通知楼主,给自己点赞时不通知
if user.username != self.user.username:
notification_handler(user, self.user, 'L', self, id_value=str(self.uuid_id), key='social_update')
def get_parent(self):
"""返回自关联中的上级记录或者本身"""
if self.parent:
return self.parent
else:
return self
def reply_this(self, user, text):
"""
回复首页的动态
:param user: 登录的用户
:param text: 回复的内容
:return: None
"""
parent = self.get_parent()
News.objects.create(
user=user,
content=text,
reply=True,
parent=parent
)
# 通知楼主
notification_handler(user, parent.user, 'R', parent, id_value=str(parent.uuid_id), key='social_update')
def get_thread(self):
"""关联到当前记录的所有记录"""
parent = self.get_parent()
return parent.thread.all()
def comment_count(self):
"""评论数"""
return self.get_thread().count()
def count_likers(self):
"""点赞数"""
return self.liked.count()
def get_likers(self):
"""所有点赞用户"""
return self.liked.all()
关于我这个models类的测试内容就可以设计为这样的:
- 首先创建两个用户
- 一个用户发表了两个动态,就需要创建两条动态数据
- 另一个用户对该动态进行了评论,就需要创建一条评论
- 对第一条动态点赞后,点赞数量是否为1,该点赞用户是否在所有点赞用户中
测试代码如下:
class NewsModelsTest(TestCase):
def setUp(self) -> None:
self.user = self.make_user("user01")
self.other_user = self.make_user("user02")
self.first_news = News.objects.create(
user=self.user,
content="第一条动态",
)
self.second_news = News.objects.create(
user=self.user,
content="第二条动态"
)
self.third_news = News.objects.create(
user=self.other_user,
content="评论第一条动态",
reply=True,
parent=self.first_news
)
def test__str__(self):
self.assertEqual(self.first_news.__str__(), "第一条动态")
def test_reply_this(self):
"""测试回复功能"""
initial_count = News.objects.count()
self.first_news.reply_this(self.other_user, "A second answer.")
assert News.objects.count() == initial_count + 1
assert self.first_news.comment_count() == 2
assert self.third_news in self.first_news.get_thread()
def test_switch_like(self):
"""测试点赞和取消的功能"""
self.first_news.switch_like(self.user)
assert self.first_news.count_likers() == 1
assert self.user in self.first_news.get_likers()
编写views单元测试
步骤一:
在该模块的tests包(自己建立的)下建立test_views.py文件:
步骤二:
导入所需模块和models:
from django.test import Client
from django.urls import reverse
from test_plus import TestCase
from app01.news.models import News
步骤三:
可以编写视图类的测试用例了,对于views的单元测试使用的方法是模拟客户端登录的方式,所以需要先在setUp()
方法中编写创建用户、模拟登录的代码。
同样的对于views的单元测试也需要根据实际的业务场景设计,比如说我的views模块中包括动态首页、删除动态、发表动态、点赞、发布评论、返回动态的评论、更新互动的功能,在设计单元测试的时候就要考虑到这些方面:
views.py代码如下:
#!/usr/bin/python3
# -*- coding:utf-8 -*-
# __author__ = 'zzy'
from django.contrib.auth.decorators import login_required
from django.contrib.auth.mixins import LoginRequiredMixin
from django.http import HttpResponse, HttpResponseBadRequest, JsonResponse
from django.template.loader import render_to_string
from django.urls import reverse_lazy
from django.views.decorators.http import require_http_methods
from django.views.generic import ListView, DeleteView
from app01.news.models import News
from app01.helper import ajax_required, AuthorRequiredMixin
class NewsListView(LoginRequiredMixin, ListView):
"""首页动态"""
model = News
paginate_by = 20
template_name = 'news/news_list.html'
def get_queryset(self, *args, **kwargs):
return News.objects.filter(reply=False)
class NewsDeleteView(LoginRequiredMixin, AuthorRequiredMixin, DeleteView):
"""继承DeleteView重写delete方法, 使用ajax请求"""
model = News
template_name = 'news/news_confirm_delete.html'
success_url = reverse_lazy("news:list")
@login_required
@ajax_required
@require_http_methods(["POST"])
def post_news(request):
"""发送动态,AJAX POST 请求"""
post = request.POST['post'].strip()
if post:
posted = News.objects.create(user=request.user, content=post)
html = render_to_string('news/news_single.html', {'news': posted, 'request': request})
return HttpResponse(html)
else:
return HttpResponseBadRequest("内容为空!")
@login_required
@ajax_required
@require_http_methods(["POST"])
def like(request):
"""点赞, AJAX POST请求"""
news_id = request.POST['news']
news = News.objects.get(pk=news_id)
# 取消或者添加赞
news.switch_like(request.user)
# 返回赞的数量
return JsonResponse({'likes': news.count_likers()})
@login_required
@ajax_required
@require_http_methods(["GET"])
def get_thread(request):
"""
返回动态的评论,AJAX GET请求
1.
"""
news_id = request.GET['news']
news = News.objects.select_related('user').get(pk=news_id)
news_html = render_to_string("news/news_single.html", {"news": news})
thread_html = render_to_string("news/news_thread.html", {"thread": news.get_thread()})
return JsonResponse({
"uuid": news_id,
"news": news_html,
"thread": thread_html,
})
@login_required
@ajax_required
@require_http_methods(["POST"])
def post_comment(request):
"""
发布评论, AJAX POST 请求
1. 获取评论的动态id,评论内容
2. 使用News模型类的reply_this方法更新评论
3. 返回评论数量
"""
post = request.POST['reply'].strip()
parent_id = request.POST['parent']
parent = News.objects.get(pk=parent_id)
if post:
parent.reply_this(request.user, post)
return JsonResponse({'comments': parent.comment_count()})
else:
return HttpResponseBadRequest("内容不能为空")
@login_required
@ajax_required
@require_http_methods(['POST'])
def update_interactions(request):
"""更新互动信息"""
data_point = request.POST['id_value']
news = News.objects.get(pk=data_point)
return JsonResponse({'likes': news.count_likers(), 'comments': news.comment_count()})
所以我的视图类的单元测试的设计如下:
- 测试前需要准备的
- 创建两个用户
- 创建两个客户端
- 第一个用户发表了一条动态,第二个用户也发表了一条动态
- 第二个用户评论了第一个用户的动态
- 测试动态列表页功能
- 第一个客户端访问动态首页接口
- 判断状态码是否200
- 判断两条动态和一条评论是否在response中
- 测试删除动态
- 获取动态的总数
- 客户端模拟post请求访问删除接口
- 判断状态码是否为302
- 动态数量是否减一
- 测试发送动态
- 获取动态总数
- 客户端模拟ajax的post请求访问发送接口(注意的是要通过ajax请求,因为在views的接口中进行了限制)
- 判断状态码是否为200
- 动态数量是否加一
- 测试点赞
- 客户端模拟ajax的post请求访问点赞接口
- 判断状态码是否为200
- 点赞数量是否为1
- 该用户是否为点赞用户
- 服务端响应的json数据中的点赞数是否为1
- 测试获取动态的评论
- 客户端模拟ajax的post请求访问获取动态的评论的接口
- 判断状态码是否为200
- 返回的uuid是否为该动态的主键
- 动态内容是否在返回的json数据中
- 评论内容是否在返回的json数据中
- 测试发表评论
- 客户端模拟ajax的post请求访问发表评论的接口
- 状态码是否为200
- 返回的json数据中的评论是否为1
单元测试代码如下:
#!/usr/bin/python3
# -*- coding:utf-8 -*-
# __author__ = 'zzy'
from django.test import Client
from django.urls import reverse
from test_plus import TestCase
from app01.news.models import News
class NewsViewTest(TestCase):
def setUp(self) -> None:
self.user = self.make_user("user01")
self.other_user = self.make_user("user02")
self.client = Client()
self.other_client = Client()
self.client.login(username="user01", password="password")
self.other_client.login(username="user02", password="password")
self.first_news = News.objects.create(
user=self.user,
content="第一条动态"
)
self.second_news = News.objects.create(
user=self.other_user,
content="第二条动态"
)
self.third_news = News.objects.create(
user=self.other_user,
content="第一条动态的评论",
reply=True,
parent=self.first_news
)
def test_news_list(self):
"""测试动态列表页功能"""
response = self.client.get(reverse("news:list"))
assert response.status_code == 200
assert self.first_news in response.context["news_list"]
assert self.second_news in response.context["news_list"]
assert self.third_news not in response.context["news_list"]
def test_delete_news(self):
"""测试删除动态"""
initial_count = News.objects.count()
response = self.client.post(reverse("news:delete_news", kwargs={"pk": self.second_news.pk}))
assert response.status_code == 302
assert News.objects.count() == initial_count - 1
def test_post_news(self):
"""发送动态"""
initial_count = News.objects.count()
response = self.client.post(
reverse("news:post_news"), {"post": "zzy"},
HTTP_X_REQUESTED_WITH="XMLHttpRequest" # 表示发送Ajax request请求
)
assert response.status_code == 200
assert News.objects.count() == initial_count + 1
def test_like_news(self):
"""测试点赞"""
response = self.client.post(
reverse("news:like_post"), {"news": self.first_news.pk},
HTTP_X_REQUESTED_WITH="XMLHttpRequest"
)
assert response.status_code == 200
assert self.first_news.count_likers() == 1
assert self.user in self.first_news.get_likers()
assert response.json()["likes"] == 1
def test_get_thread(self):
"""测试获取动态的评论"""
response = self.client.get(
reverse("news:get_thread"), {"news": self.first_news.pk},
HTTP_X_REQUESTED_WITH="XMLHttpRequest"
)
assert response.status_code == 200
assert response.json()['uuid'] == str(self.first_news.pk)
assert "第一条动态" in response.json()['news']
assert "第一条动态的评论" in response.json()["thread"]
def test_post_comments(self):
"""测试发表评论"""
response = self.client.post(
reverse("news:post_comments"),
{
"reply": "第二条动态的评论",
"parent": self.second_news.pk
},
HTTP_X_REQUESTED_WITH="XMLHttpRequest"
)
assert response.status_code == 200
assert response.json()["comments"] == 1