使用django-test-plus库编写单元测试用例

使用django-test-plus库进行单元测试

编码前介绍

django-test-plus库简介

django-test-plus库是对Django框架自带的单元测试模块的封装,提供更加方便的接口供我们使用。

PyPI上的介绍

Django项目需要进行单元测试的地方有:

  1. url
  2. model类
  3. 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从两个方面测试:

  1. url的正向解析是否正确
  2. 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

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

一切如来心秘密

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值