资讯专栏INFORMATION COLUMN

Django搭建个人博客:自动化测试

smallStone / 3048人阅读

摘要:修改某一个组件可能会导致另一个组件出现意想不到的,但是在人工测试时却很难检查出来,总不能每写几行代码就把整个网站统统检查一遍吧。比如说有个功能,限制每个用户每天发表评论不能超过条,人工测试就显得比较麻烦,特别是需要反复调试的时候。

测试是伴随着开发进行的,开发有多久,测试就要多久。本教程已经进行了30多章了,都是如何测试的?当然是runserver啦!每当开发新功能后,都需要运行服务器,假装自己就是用户,测试是否运行正常。

这样的人工测试优点是非常直观,你看到的和用户看到的是完全相同的。但是缺点也很明显:

效率低。在开发时可能你需要反复的修改代码、测试功能,这样重复查看几十次甚至几百次网页时会相当的让人烦躁。

容易遗漏bug。随着你的项目越来越复杂,组件之间的交互也更加复杂。修改某一个组件可能会导致另一个组件出现意想不到的bug,但是在人工测试时却很难检查出来,总不能每写几行代码就把整个网站统统检查一遍吧。过了很久之后你终于发现了这个bug,但此时你已经搞不清它来源于什么地方了。

有的测试不方便进行。比如说有个功能,限制每个用户每天发表评论不能超过10条,人工测试就显得比较麻烦,特别是需要反复调试的时候。

为了解决人工测试的种种问题,Django引入了Python标准库的单元测试模块,也就是自动化测试了:你可以写一段代码,让代码帮你测试!(程序员是最会偷懒的职业..)代码会忠实的完成测试任务,帮助你从繁重的测试工作中解脱出来。除此之外,自动化测试还有以下优点:

预防错误。当应用过于复杂时,代码的意图会变得非常不清晰,甚至你都看不懂自己写的代码,这是很常见的。而测试就好像是从内部审查代码一样,可以帮助你发现微小的错误。

有利于团队协作。良好的测试保证其他人不会不小心破坏了你的代码(也保证你不会不小心弄坏别人的..)。现在已经不是单打独斗出英雄的年代了,想要成为优秀的Django程序员,你必须擅长编写测试!

虽然学习自动化测试不会让你的博客增加一丝丝的功能,但是可以让代码更加强壮,所以我觉得很有必要拿出一章来专门讲讲。

Django官方文档的第5部分讲测试讲得非常的好,并且有中文版本。本章节就大量借鉴了官方文档,也非常非常推荐读者去拜读。
第一个测试 给我bug!

为了演示测试是如何工作的,让我们首先在文章模型中写个有bug的方法:

article/models.py

from django.utils import timezone

class ArticlePost(models.Model):
    ...

    def was_created_recently(self):
        # 若文章是"最近"发表的,则返回 True
        diff = timezone.now() - self.created
        if diff.days <= 0 and diff.seconds < 60:
            return True
        else:
            return False

这个方法用于检测当前文章是否是最近发表的。

这个方法稍微扩展一下就会变得非常实用。比如可以将博文的发表日期显示为“刚刚”、“3分钟前”、“5小时前”等相对时间,用户体验将大有提升。

仔细看看,它是没办法正确判断“未来”的文章的:

>>> import datetime
>>> from django.utils import timezone
>>> from article.models import ArticlePost
>>> from django.contrib.auth.models import User

# 创建一篇"未来"的文章
>>> future_article = ArticlePost(author=User(username="user"), title="test",body="test", created=timezone.now() + datetime.timedelta(days=30))

# 是否是“最近”发表的?
>>> future_article.was_created_recently()
True

未来发生的肯定不是最近发生的,因此代码是错误的。

写个测试暴露bug

接下来就要写测试用例,将测试转为自动化。

还记得最初生成文章app时候的目录结构吗?

article
 │  admin.py
 │  apps.py
 │  models.py
 │  tests.py
 │  views.py
 │  __init__.py
 │
 └─migrations
       └─ __init__.py

这个tests.py就是留给你写测试用例的地方了:

article/tests.py

from django.test import TestCase

import datetime
from django.utils import timezone
from article.models import ArticlePost
from django.contrib.auth.models import User


class ArticlePostModelTests(TestCase):

    def test_was_created_recently_with_future_article(self):
        # 若文章创建时间为未来,返回 False
        author = User(username="user", password="test_password")
        author.save()

        future_article = ArticlePost(
            author=author,
            title="test",
            body="test",
            created=timezone.now() + datetime.timedelta(days=30)
            )

        self.assertIs(future_article.was_created_recently(), False)

基本就是把刚才在Shell中的测试代码抄了过来。有点不同的是末尾这个assertIs方法,了解“断言”的同学会对它很熟悉:它的作用是检测方法内的两个参数是否完全一致,如果不是则抛出异常,提醒你这个地方是有问题滴。

接下来运行测试:

(env) > python manage.py test

运行结果如下:

Creating test database for alias "default"...
System check identified no issues (0 silenced).
F
======================================================================
FAIL: test_was_created_recently_with_future_article (article.tests.ArticlePostModelTests)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "E:django_projectmy_blogarticle	ests.py", line 19, in test_was_created_recently_with_future_article
    self.assertIs(future_article.was_created_recently(), False)
AssertionError: True is not False

----------------------------------------------------------------------
Ran 1 test in 0.000s

FAILED (failures=1)
Destroying test database for alias "default"...

这里面名堂就很多了:

首先测试系统会在所有以tests开头的文件中寻找测试代码

所有TestCase的子类都被认为是测试代码

系统创建了一个特殊的数据库供测试使用,即所有测试产生的数据不会对你自己的数据库造成影响

类中所有以test开头的方法会被认为是测试用例

在运行测试用例时,assertIs抛出异常,因为True is not False

完成测试后,自动销毁测试数据库

测试系统明确指明了错误的数量、位置和种类等信息,请读者细细品尝。

修正bug

既然通过测试找到了bug,那接下来就要把代码进行修正:

article/models.py

from django.utils import timezone

class ArticlePost(models.Model):
    ...

    def was_created_recently(self):
        diff = timezone.now() - self.created
        
        # if diff.days <= 0 and diff.seconds < 60:
        if diff.days == 0 and diff.seconds >= 0 and diff.seconds < 60:
            return True
        else:
            return False

重新运行测试:

(env) > python manage.py test

Creating test database for alias "default"...
System check identified no issues (0 silenced).
.
----------------------------------------------------------------------
Ran 1 test in 0.000s

OK
Destroying test database for alias "default"...

这次代码顺利通过了测试。

可以肯定的是,在往后的开发中,这个bug不会再出现了,因为你只需要运行一遍测试,就会立即得到警告。可以认为项目的这一小部分代码永远是安全的

更全面的测试

既然一个测试用例就可以保证一小段代码永远安全,那我写一堆测试岂不是可以保证整个项目永远安全吗?确实如此,这个买卖绝对是不亏的。

因此我们继续再增加几个测试,全面强化代码:

article/tests.py

...

from django.test import TestCase

import datetime
from django.utils import timezone
from article.models import ArticlePost
from django.contrib.auth.models import User


class ArticlePostModelTests(TestCase):

    def test_was_created_recently_with_future_article(self):
        # 若文章创建时间为未来,返回 False
        ...

    def test_was_created_recently_with_seconds_before_article(self):
        # 若文章创建时间为 1 分钟内,返回 True
        author = User(username="user1", password="test_password")
        author.save()
        seconds_before_article = ArticlePost(
            author=author,
            title="test1",
            body="test1",
            created=timezone.now() - datetime.timedelta(seconds=45)
            )
        self.assertIs(seconds_before_article.was_created_recently(), True)

    def test_was_created_recently_with_hours_before_article(self):
        # 若文章创建时间为几小时前,返回 False
        author = User(username="user2", password="test_password")
        author.save()
        hours_before_article = ArticlePost(
            author=author,
            title="test2",
            body="test2",
            created=timezone.now() - datetime.timedelta(hours=3)
            )
        self.assertIs(hours_before_article.was_created_recently(), False)

    def test_was_created_recently_with_days_before_article(self):
        # 若文章创建时间为几天前,返回 False
        author = User(username="user3", password="test_password")
        author.save()
        months_before_article = ArticlePost(
            author=author,
            title="test3",
            body="test3",
            created=timezone.now() - datetime.timedelta(days=5)
            )
        self.assertIs(months_before_article.was_created_recently(), False)

现在我们拥有了4个测试,来保证was_created_recently()方法对于过去最近未来中的4种情况都返回正确的值。你还可以继续扩展,直到你觉得完全没有任何bug藏匿的可能性为止。

在实际的开发中,有些难缠的bug会把自己伪装得非常的好,而不是像教程这样明确的知道它就在那里。有了自动化测试,无论以后你的项目怎么变化、app交互多么的复杂,只要在测试中写好的逻辑就一定是符合预期的,而你所需要做的只是运行一条测试指令而已。

虽然教程中仅使用了assertIs,但实际上Django中的断言有大概几十种之多,比如assertEqualassertContains等,并且还在不断更新。详见Python标准断言和Django扩展断言
测试视图

上面的测试都是针对模型的。视图该怎么测试?如何通过测试系统模拟出用户的请求呢?

答案是TestCase类提供了一个供测试使用的Client来模拟用户通过请求和视图层代码的交互。

文章详情视图浏览量统计为例,比较容易出现的潜在bug有:

增加的浏览量未能正常保存进数据库(即每次请求则浏览量+1)

增加浏览量的同时,updated字段也错误的一并更新

所以有针对的写2条测试。新写一个专门测试视图的类,与前面的测试模型的类区分开:

article/tests.py

...
from time import sleep
from django.urls import reverse


class ArticlePostModelTests(TestCase):
    ...


class ArtitclePostViewTests(TestCase):

    def test_increase_views(self):
        # 请求详情视图时,阅读量 +1
        author = User(username="user4", password="test_password")
        author.save()
        article = ArticlePost(
            author=author,
            title="test4",
            body="test4",
            )
        article.save()
        self.assertIs(article.total_views, 0)

        url = reverse("article:article_detail", args=(article.id,))
        response = self.client.get(url)

        viewed_article = ArticlePost.objects.get(id=article.id)
        self.assertIs(viewed_article.total_views, 1)

    def test_increase_views_but_not_change_updated_field(self):
        # 请求详情视图时,不改变 updated 字段
        author = User(username="user5", password="test_password")
        author.save()
        article = ArticlePost(
            author=author,
            title="test5",
            body="test5",
            )
        article.save()

        sleep(0.5)

        url = reverse("article:article_detail", args=(article.id,))
        response = self.client.get(url)

        viewed_article = ArticlePost.objects.get(id=article.id)
        self.assertIs(viewed_article.updated - viewed_article.created < timezone.timedelta(seconds=0.1), True)

注意看代码是如何与视图层交互的:response = self.client.get(url)向视图发起请求并获得了响应,剩下的就是从数据库中取出更新后的数据,并用断言语句来判断代码是否符合预期了。

运行测试:

(env) > python manage.py test

Creating test database for alias "default"...
System check identified no issues (0 silenced).
......
----------------------------------------------------------------------
Ran 6 tests in 0.617s

OK
Destroying test database for alias "default"...

6条测试用例全部通过。

越多越好的测试

仅仅是app中的两个非常小的功能,就已经写了6条测试用例了,并且还可以继续扩展。除此之外,其他的每个模型、视图都可以扩展出几十甚至上百条测试,这样下去代码总量很快就要失去控制了,并且相对于业务代码来说,测试代码显得繁琐且不够优雅。

但是没关系!就让测试代码继续肆意增长吧。大部分情况下,你写完一个测试之后就可以忘掉它了。在你继续开发的过程中,它会一直默默无闻地为你做贡献的。最坏的情况是当你继续开发的时候,发现之前的一些测试现在看来是多余的。但是这也不是什么问题,多做些测试也不错。

深入代码测试

在前面的测试中,我们已经从模型层和视图层的角度检查了应用的输入输出,但是模板呢?虽然可以用assertInHTMLassertJSONEqual等断言大致检查模板中的某些内容,但更加近似于浏览器的检查就要使用Selenium等测试工具(毕竟Django的重点是后端而不是前端)。

Selenium不仅可以测试 Django 框架里的代码,甚至还可以检查 JavaScript代码。它假装成是一个正在和你站点进行交互的浏览器,就好像有个真人在访问网站一样。Django 提供了LiveServerTestCase来和Selenium这样的工具进行交互。

关于测试的话题这里只是开了个头,读者可以继续阅读下面的内容进一步了解:

Django: Writing and running tests

Django: Testing tools

Django: Advanced testing topics

Selenium官方文档

总结

有一帮崇尚“测试驱动”的开发者,他们开发时先写测试代码,然后才写业务代码。而普通开发者通常是先写业务代码,再写测试代码,这也是没问题的。但如果你已经写了很多业务代码了,再回头写测试确实有些无从下手,那么至少在以后写新功能时,记得加上测试。测试写得好不好,甚至比功能本身更能看出编程水平。

测试可以让代码更加强壮。项目没出bug时,皆大欢喜,有没有测试都一样;一旦出现难缠的bug,你就会无比想念一套完善的测试代码了。

博主写自己的网站时就没有对测试给与足够的重视,回想起来走了很多弯路。希望读者以前车之鉴,培养良好的编程习惯。


有疑问请在杜赛的个人网站留言,我会尽快回复。

或Email私信我:dusaiphoto@foxmail.com

项目完整代码:Django_blog_tutorial

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/45096.html

相关文章

  • Django搭建个人博客:View视图初探

    摘要:比如,在一个博客应用中,你可能会创建如下几个视图博客首页展示最近的几项内容。这些需求都靠视图来完成。首先写一个最简单的视图函数,在浏览器中打印出字符串。调用函数时会返回一个含字符串的对象。换句话说,的作用是将映射到视图中。 Django 中的视图的概念是「一类具有相同功能和模板的网页的集合」。比如,在一个博客应用中,你可能会创建如下几个视图: 博客首页:展示最近的几项内容。 内容详情...

    Turbo 评论0 收藏0
  • Django搭建个人博客:结束和开始

    摘要:教程看到这里,你已经学会如下内容搭建开发环境博文管理用户管理发表评论若干小功能搭建简单的小博客,以上的功能够用了。教程为了起步平缓,没有展开这方面的内容。陌生人,祝你学业进步事业有成欢迎常到杜赛的个人网站做客 教程看到这里,你已经学会如下内容: 搭建开发环境 博文管理 用户管理 发表评论 若干小功能 搭建简单的小博客,以上的功能够用了。 相信你的志向不止于此。毕竟程序员面试个个造火...

    zqhxuyuan 评论0 收藏0
  • Django搭建个人博客:用户的注册

    摘要:既然有登录登出,那么用户的注册肯定也是少不了的。用户在注册成功后会自动登录并返回博客列表页面。总结本章用到了表单类对数据进行验证清洗等知识,完成了用户的注册功能。 既然有登录登出,那么用户的注册肯定也是少不了的。 注册表单类 用户注册时会用到表单来提交账号、密码等数据,所以需要写注册用的表单/userprofile/forms.py: /userprofile/forms.py .....

    leanote 评论0 收藏0
  • Django搭建个人博客:在博文中发表评论

    摘要:确认创建成功后,记得在中注册因为我们想显示发表评论的时间,修改时区设置为上海的时区。处理错误请求发表评论仅接受请求。返回到一个适当的中即用户发送评论后,重新定向到文章详情页面。总结本章实现了发表评论展示评论的功能。 在没有互联网的年代,我们用日记来记录每天的心得体会。小的时候我有一个带锁的日记本,生怕被别人看见里面写了啥,钥匙藏得那叫一个绝。 现在时代变了,网络版的日记本:博客,却巴不...

    Jinkey 评论0 收藏0
  • 使用 django-blog-zinnia 搭建个人博客

    摘要:语法支持再次打开文件,在文件的最后添加指明了使用语法标记,做了两个拓展,其中表示支持语法高亮,包含的特性请参见相关文档。语法高亮支持注意这一步必须在安装完主题之后。 目前网上搭建个人博客的方案很多,虽然使用诸如 Wordpress ( PHP )、Hexo ( Node.js ) 等可以方便快速地搭建一款功能齐全的高性能个人博客,但是本文将尝试一种更为小众化的方案 —— 一款基于 dj...

    褰辩话 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<