惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
H
Help Net Security
博客园_首页
P
Privacy International News Feed
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
T
Tenable Blog
Latest news
Latest news
D
Darknet – Hacking Tools, Hacker News & Cyber Security
爱范儿
爱范儿
Cyberwarzone
Cyberwarzone
P
Palo Alto Networks Blog
月光博客
月光博客
有赞技术团队
有赞技术团队
Know Your Adversary
Know Your Adversary
博客园 - 叶小钗
P
Proofpoint News Feed
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
S
Schneier on Security
A
About on SuperTechFans
F
Full Disclosure
The Cloudflare Blog
T
The Exploit Database - CXSecurity.com
S
SegmentFault 最新的问题
Apple Machine Learning Research
Apple Machine Learning Research
Microsoft Security Blog
Microsoft Security Blog
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
N
News and Events Feed by Topic
www.infosecurity-magazine.com
www.infosecurity-magazine.com
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
Webroot Blog
Webroot Blog
量子位
大猫的无限游戏
大猫的无限游戏
小众软件
小众软件
WordPress大学
WordPress大学
Last Week in AI
Last Week in AI
美团技术团队
Help Net Security
Help Net Security
Microsoft Azure Blog
Microsoft Azure Blog
GbyAI
GbyAI
云风的 BLOG
云风的 BLOG
Y
Y Combinator Blog
博客园 - 司徒正美
N
Netflix TechBlog - Medium
S
Security @ Cisco Blogs
B
Blog
P
Privacy & Cybersecurity Law Blog
Security Archives - TechRepublic
Security Archives - TechRepublic
Hacker News - Newest:
Hacker News - Newest: "LLM"
V
Visual Studio Blog
NISL@THU
NISL@THU

追梦人物的博客

0x05:Merkle Tree & Patricia Trie 0x04:ECDSA - 以太坊设计与实现 LeetCode 105 从前序与中序遍历构造二叉树 迭代算法原理 + 完整证明 0x03:Address - 以太坊设计与实现 0x02:Secp256k1 - 以太坊设计与实现 0x00:专栏开篇 - 以太坊设计与实现 0x01:RLP 编码 - 以太坊设计与实现 Bellman-Ford 算法原理及其在 DeFi 套利中的应用 - 追梦人物的博客 迪杰斯特拉(Dijkstra)最短路径算法原理、实现与证明 - 追梦人物的博客 实战:CEX-DEX 稳定币套利监控程序开发 - 追梦人物的博客 CEX-DEX 稳定币套利模型 - 追梦人物的博客 Uniswap 手续费和协议费机制剖析 - 追梦人物的博客 Uniswap 流动性机制及相关数学原理分析 - 追梦人物的博客 uv 替代 pyenv + pipx + poetry 环境管理实践 Rust 项目从创建到发布 - 追梦人物的博客 VS Code 调试 Python - 追梦人物的博客 比特币跨市场套利的数学模型 - 追梦人物的博客 ERC-20 相关知识点总结 - 追梦人物的博客 Django 老项目如何从 SQLite 迁到 PostgreSQL 自动生成接口文档 - HelloDjango - django REST framework 教程 单元测试 - HelloDjango - django REST framework 教程 限制接口访问频率 - HelloDjango - django REST framework 教程 API 版本管理 - HelloDjango - django REST framework 教程 如何在 Windows 下搭建高效的 django 开发环境 拓展Python Markdown - 追梦人物的博客 加缓存为接口提速 - HelloDjango - django REST framework 教程 基于 drf-haystack 实现文章搜索接口 - HelloDjango 评论接口 - HelloDjango - django REST framework 教程 实现分类、标签、归档日期接口 - HelloDjango - django REST framework 教程 在接口返回Markdown解析后的内容 - HelloDjango - django REST framework 教程 文章详情接口 - HelloDjango - django REST framework 教程 分页 - HelloDjango - django REST framework 教程 使用视图集简化代码 - HelloDjango - django REST framework 教程 用类视图实现首页 API - HelloDjango - django REST framework 教程 实现博客首页文章列表 API - HelloDjango - django REST framework 教程 初始化 RESTful API 风格的博客系统 - HelloDjango django-rest-framework 是什么鬼? - HelloDjango - django REST framework 教程 结束 or 开始? - HelloDjango Coverage.py 统计测试覆盖率 - HelloDjango - Django博客教程(第二版) 单元测试:测试评论应用 - HelloDjango - Django博客教程(第二版) Django Haystack 全文检索与关键词高亮 - HelloDjango Django 博客实现简单的全文搜索 - HelloDjango - Django博客教程(第二版) 开启 Django 博客的 RSS 功能 - HelloDjango 统计各个分类和标签下的文章数 - HelloDjango - Django博客教程(第二版) 稳定易用的 Django 分页库,完善分页功能 - HelloDjango 通过 Django Pagination 实现简单分页 - HelloDjango 在脚本中使用 ORM:Faker 批量生成测试数据 - HelloDjango Django 官方推荐的姿势:类视图 - HelloDjango - Django博客教程(第二版) Django 使用 union 合并不同模型(Model) 的查询集(QuerySet) 开发博客文章阅读量统计功能 - HelloDjango - Django博客教程(第二版) 使用 Docker 让部署 Django 项目更加轻松 - HelloDjango 使用 Certbot 向 Let's Encrypt 免费申请 HTTPS 证书 - HelloDjango 使用 Fabric 自动化部署 - HelloDjango 博客代码开源啦 - 追梦人物的博客 (赠书)推荐一本django书籍:Django企业开发实战 - 追梦人物的博客 Nginx+Gunicorn+Supervisor 部署 Django 博客应用 - HelloDjango 优化博客功能细节,提升使用体验 - HelloDjango - Django博客教程(第二版) 交流的桥梁:评论功能 - HelloDjango - Django博客教程(第二版) 分类、归档和标签页 - HelloDjango - Django博客教程(第二版) 页面侧边栏:使用自定义模板标签 - HelloDjango - Django博客教程(第二版) 自动生成文章摘要 - HelloDjango - Django博客教程(第二版) Markdown 文章自动生成目录,提升阅读体验 - HelloDjango - Django博客教程(第二版) 让博客支持 Markdown 语法和代码高亮 - HelloDjango 开发博客文章详情页 - HelloDjango - Django博客教程(第二版) 创作后台开启,请开始你的表演 - HelloDjango - Django博客教程(第二版) 博客从“裸奔”到“有皮肤” - HelloDjango - Django博客教程(第二版) Django 的接客之道 - HelloDjango - Django博客教程(第二版) Django 迁移、操作数据库 - HelloDjango - Django博客教程(第二版) 创建 Django 博客的数据库模型 - HelloDjango 开始进入 django 开发之旅 - HelloDjango "空空如也"的博客应用 - HelloDjango - Django博客教程(第二版) 一种自顶而下的Python装饰器设计方法 - 追梦人物的博客 Python提取支付宝和微信支付二维码 - 追梦人物的博客 2018 - 我的学生生涯最后一年回顾 - 追梦人物的博客 批量清除todo练习参考答案 - Vue 2.x Todo 教程练习参考答案 筛选练习参考答案 - Vue 2.x Todo 教程练习参考答案 删除todo练习参考答案 - Vue 2.x Todo 教程练习参考答案 编辑todo练习参考答案 - Vue 2.x Todo 教程练习参考答案 添加todo练习参考答案 - Vue 2.x Todo 教程练习参考答案 标为完成练习参考答案 - Vue 2.x Todo 教程练习参考答案 入门仪式_Hello_Vue练习参考答案 - Vue 2.x Todo 教程练习参考答案 组件化todo应用 - Vue 2.x Todo 教程 批量清除todo - Vue 2.x Todo 教程 本地存储 - Vue 2.x Todo 教程 筛选 - Vue 2.x Todo 教程 还剩多少todo未完成 - Vue 2.x Todo 教程 自定义指令实现自动聚焦 - Vue 2.x Todo 教程 全部标为完成 - Vue 2.x Todo 教程 删除todo - Vue 2.x Todo 教程 编辑todo - Vue 2.x Todo 教程 添加todo - Vue 2.x Todo 教程 标为完成 - Vue 2.x Todo 教程 UI - Vue 2.x Todo 教程 显示todo列表 - Vue 2.x Todo 教程 入门仪式:Hello Vue - Vue 2.x Todo 教程 在学习django-rest-framework时收集的学习资料推荐 - 追梦人物的博客 区块链理论与应用研究小组成员招募书 - 追梦人物的博客 Python界网红,豆瓣工程师董伟明加了我的QQ后 - 追梦人物的博客 招募Django学习小组项目组核心成员 - 追梦人物的博客
单元测试:测试 blog 应用 - HelloDjango
2020-02-27 · via 追梦人物的博客

我们博客功能越来越来完善了,但这也带来了一个问题,我们不敢轻易地修改已有功能的代码了!

我们怎么知道代码修改后带来了预期的效果?万一改错了,不仅新功能没有用,原来已有的功能都可能被破坏。此前我们开发一个新的功能,都是手工运行开发服务器去验证,不仅费时,而且极有可能验证不充分。

如何不用每次开发了新的功能或者修改了已有代码都得去人工验证呢?解决方案就是编写自动化测试,将人工验证的逻辑编写成脚本,每次新增或修改代码后运行一遍测试脚本,脚本自动帮我们完成全部测试工作。

接下来我们将进行两种类型的测试,一种是单元测试,一种是集成测试。

单元测试是一种比较底层的测试,它将一个功能逻辑的代码块视为一个单元(例如一个函数、方法、或者一个 if 语句块等,单元应该尽可能小,这样测试就会更加充分),程序员编写测试代码去测试这个单元,确保这个单元的逻辑代码按照预期的方式执行了。通常来说我们一般将一个函数或者方法视为一个单元,对其进行测试。

集成测试则是一种更加高层的测试,它站在系统角度,测试由各个已经经过充分的单元测试的模块组成的系统,其功能是否符合预期。

我们首先来进行单元测试,确保各个单元的逻辑都没问题后,然后进行集成测试,测试整个博客系统的可用性。

Python 一般使用标准库 unittest 提供单元测试,django 拓展了单元测试,提供了一系列类,用于不同的测试场合。其中最常用到的就是 django.test.TestCase 类,这个类和 Python 标准库的 unittest.TestCase 类似,只是拓展了以下功能:

  • 提供了一个 client 属性,这个 client 是 Client 的实例。可以把 Client 看做一个发起 HTTP 请求的功能库(类似于 requests),这样我们可以方便地使用这个类测试视图函数。
  • 运行测试前自动创建数据库,测试运行完毕后自动销毁数据库。我们肯定不希望自动生成的测试数据影响到真实的数据。

博客应用的单元测试,主要就是和这个类打交道。

django 应用的单元测试包括:

  • 测试 model,model 的方法是否返回了预期的数据,对数据库的操作是否正确。

  • 测试表单,数据验证逻辑是否符合预期

  • 测试视图,针对特定类型的请求,是否返回了预期的响应

  • 其它的一些辅助方法或者类等

接下来我们就逐一地来测试上述内容。

搭建测试环境

测试写在 tests.py 里(应用创建时就会自动创建这个文件),首先来个冒烟测试,用于验证测试功能是否正常,在 blog\tests.py 文件写入如下代码:

from django.test import TestCase


class SmokeTestCase(TestCase):
    def test_smoke(self):
        self.assertEqual(1 + 1, 2)

使用 manage.py 的 test 命令将自动发现 django 应用下的 tests 文件或者模块,并且自动执行以 test_ 开头的方法。运行:pipenv run python manage.py test

Creating test database for alias 'default'...
System check identified no issues (0 silenced).

.

--------------------------------------------------------------------------------------------------

Ran 1 test in 0.002s

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

OK 表明我们的测试运行成功。

不过,如果需要测试的代码比较多,把全部测试逻辑一股脑塞入 tests.py,这个模块就会变得十分臃肿,不利于维护,所以我们把 tests.py 文件升级为一个包,不同的单元测试写到包下对应的模块中,这样便于模块化地维护和管理。

删除 blog\tests.py 文件,然后在 blog 应用下创建一个 tests 包,再创建各个单元测试模块:

blog\
    tests\
        __init__.py
        test_smoke.py
        test_models.py
        test_views.py
        test_templatetags.py
        test_utils.py
  • test_models.py 存放和模型有关的单元测试
  • test_views.py 测试视图函数
  • test_templatetags.py 测试自定义的模板标签
  • test_utils.py 测试一些辅助方法和类等

注意

tests 包中的各个模块必须以 test_ 开头,否则 django 无法发现这些测试文件的存在,从而不会运行里面的测试用例。

测试模型

模型需要测试的不多,因为基本上都是使用了 django 基类 models.Model 的特性,自己的逻辑很少。拿最为复杂的 Post 模型举例,它包括的逻辑功能主要有:

  • __str__ 方法返回 title 用于模型实例的字符表示
  • save 方法中设置文章创建时间(created_time)和摘要(exerpt)
  • get_absolute_url 返回文章详情视图对应的 url 路径
  • increase_views 将 views 字段的值 +1

单元测试就是要测试这些方法执行后的确返回了上面预期的结果,我们在 test_models.py 中新增一个类,叫做 PostModelTestCase,在这个类中编写上述单元测试的用例。

from django.apps import apps

class PostModelTestCase(TestCase):
    def setUp(self):
        # 断开 haystack 的 signal,测试生成的文章无需生成索引
        apps.get_app_config('haystack').signal_processor.teardown()
        user = User.objects.create_superuser(
            username='admin', 
            email='admin@hellogithub.com', 
            password='admin')
        cate = Category.objects.create(name='测试')
        self.post = Post.objects.create(
            title='测试标题',
            body='测试内容',
            category=cate,
            author=user,
        )

    def test_str_representation(self):
        self.assertEqual(self.post.__str__(), self.post.title)

    def test_auto_populate_modified_time(self):
        self.assertIsNotNone(self.post.modified_time)

        old_post_modified_time = self.post.modified_time
        self.post.body = '新的测试内容'
        self.post.save()
        self.post.refresh_from_db()
        self.assertTrue(self.post.modified_time > old_post_modified_time)

    def test_auto_populate_excerpt(self):
        self.assertIsNotNone(self.post.excerpt)
        self.assertTrue(0 < len(self.post.excerpt) <= 54)

    def test_get_absolute_url(self):
        expected_url = reverse('blog:detail', kwargs={'pk': self.post.pk})
        self.assertEqual(self.post.get_absolute_url(), expected_url)

    def test_increase_views(self):
        self.post.increase_views()
        self.post.refresh_from_db()
        self.assertEqual(self.post.views, 1)

        self.post.increase_views()
        self.post.refresh_from_db()
        self.assertEqual(self.post.views, 2)

这里代码虽然比较多,但做的事情很明确。setUp 方法会在每一个测试案例运行前执行,这里做的事情是在数据库中创建一篇文章,用于测试。

接下来的各个 test_* 方法就是对于各个功能单元的测试,以 test_auto_populate_modified_time 为例,这里我们要测试文章保存到数据库后,modifited_time 被正确设置了值(期待的值应该是文章保存时的时间)。

self.assertIsNotNone(self.post.modified_time) 断言文章的 modified_time 不为空,说明的确设置了值。TestCase 类提供了系列 assert* 方法用于断言测试单元的逻辑结果是否和预期相符,一般从方法的命名中就可以读出其功能,比如这里 assertIsNotNone 就是断言被测试的变量值不为 None。

接着我们尝试通过

self.post.body = '新的测试内容'
self.post.save()

修改文章内容,并重新保存数据库。预期的结果应该是,文章保存后,modifited_time 的值也被更新为修改文章时的时间,接下来的代码就是对这个预期结果的断言:

self.post.refresh_from_db()
self.assertTrue(self.post.modified_time > old_post_modified_time)

这个 refresh_from_db 方法将刷新对象 self.post 的值为数据库中的最新值,然后我们断言数据库中 modified_time 记录的最新时间比原来的时间晚,如果断言通过,说明我们更新文章后,modified_time 的值也进行了相应更新来记录修改时间,结果符合预期,测试通过。

其它的测试方法都是做着类似的事情,这里不再一一讲解,请自行看代码分析。

测试视图

视图函数测试的基本思路是,向某个视图对应的 URL 发起请求,视图函数被调用并返回预期的响应,包括正确的 HTTP 响应码和 HTML 内容。

我们的博客应用包括以下类型的视图需要进行测试:

  • 首页视图 IndexView,访问它将返回全部文章列表。
  • 标签视图,访问它将返回某个标签下的文章列表。如果访问的标签不存在,返回 404 响应。
  • 分类视图,访问它将返回某个分类下的文章列表。如果访问的分类不存在,返回 404 响应。
  • 归档视图,访问它将返回某个月份下的全部文章列表。
  • 详情视图,访问它将返回某篇文章的详情,如果访问的文章不存在,返回 404。
  • 自定义的 admin,添加文章后自动填充 author 字段的值。
  • RSS,返回全部文章的 RSS 内容。

首页视图、标签视图、分类视图、归档视图都是同一类型的视图,他们预期的行为应该是:

  • 返回正确的响应码,成功返回200,不存在则返回404。
  • 没有文章时正确地提示暂无文章。
  • 渲染了正确的 html 模板。
  • 包含关键的模板变量,例如文章列表,分页变量等。

我们首先来测试这几个视图。为了给测试用例生成合适的数据,我们首先定义一个基类,预先定义好博客的数据内容,其它视图函数测试用例继承这个基类,就不需要每次测试时都创建数据了。我们创建的测试数据如下:

  • 分类一、分类二
  • 标签一、标签二
  • 文章一,属于分类一和标签一,文章二,属于分类二,没有标签
class BlogDataTestCase(TestCase):
    def setUp(self):
        apps.get_app_config('haystack').signal_processor.teardown()

        # User
        self.user = User.objects.create_superuser(
            username='admin',
            email='admin@hellogithub.com',
            password='admin'
        )

        # 分类
        self.cate1 = Category.objects.create(name='测试分类一')
        self.cate2 = Category.objects.create(name='测试分类二')

        # 标签
        self.tag1 = Tag.objects.create(name='测试标签一')
        self.tag2 = Tag.objects.create(name='测试标签二')

        # 文章
        self.post1 = Post.objects.create(
            title='测试标题一',
            body='测试内容一',
            category=self.cate1,
            author=self.user,
        )
        self.post1.tags.add(self.tag1)
        self.post1.save()

        self.post2 = Post.objects.create(
            title='测试标题二',
            body='测试内容二',
            category=self.cate2,
            author=self.user,
            created_time=timezone.now() - timedelta(days=100)
        )

CategoryViewTestCase 为例:

class CategoryViewTestCase(BlogDataTestCase):
    def setUp(self):
        super().setUp()
        self.url = reverse('blog:category', kwargs={'pk': self.cate1.pk})
        self.url2 = reverse('blog:category', kwargs={'pk': self.cate2.pk})

    def test_visit_a_nonexistent_category(self):
        url = reverse('blog:category', kwargs={'pk': 100})
        response = self.client.get(url)
        self.assertEqual(response.status_code, 404)

    def test_without_any_post(self):
        Post.objects.all().delete()
        response = self.client.get(self.url2)
        self.assertEqual(response.status_code, 200)
        self.assertTemplateUsed('blog/index.html')
        self.assertContains(response, '暂时还没有发布的文章!')

    def test_with_posts(self):
        response = self.client.get(self.url)
        self.assertEqual(response.status_code, 200)
        self.assertTemplateUsed('blog/index.html')
        self.assertContains(response, self.post1.title)
        self.assertIn('post_list', response.context)
        self.assertIn('is_paginated', response.context)
        self.assertIn('page_obj', response.context)
        self.assertEqual(response.context['post_list'].count(), 1)
        expected_qs = self.cate1.post_set.all().order_by('-created_time')
        self.assertQuerysetEqual(response.context['post_list'], [repr(p) for p in expected_qs])

这个类首先继承自 BlogDataTestCasesetUp 方法别忘了调用父类的 stepUp 方法,以便在每个测试案例运行时,设置好博客测试数据。

然后就是进行了3个案例测试:

  • 访问一个不存在的分类,预期返回 404 响应码。

  • 没有文章的分类,返回200,但提示暂时还没有发布的文章!渲染的模板为 index.html

  • 访问的分类有文章,则响应中应该包含系列关键的模板变量,post_listis_paginatedpage_objpost_list 文章数量为1,因为我们的测试数据中这个分类下只有一篇文章,post_list 是一个 queryset,预期是该分类下的全部文章,时间倒序排序。

其它的 TagViewTestCase 等测试类似,请自行参照代码分析。

博客文章详情视图的逻辑更加复杂一点,所以测试用例也更多,主要需要测试的点有:

  • 访问不存在文章,返回404。
  • 文章每被访问一次,访问量 views 加一。
  • 文章内容被 markdown 渲染,并生成了目录。

测试代码如下:

class PostDetailViewTestCase(BlogDataTestCase):
    def setUp(self):
        super().setUp()
        self.md_post = Post.objects.create(
            title='Markdown 测试标题',
            body='# 标题',
            category=self.cate1,
            author=self.user,
        )
        self.url = reverse('blog:detail', kwargs={'pk': self.md_post.pk})

    def test_good_view(self):
        response = self.client.get(self.url)
        self.assertEqual(response.status_code, 200)
        self.assertTemplateUsed('blog/detail.html')
        self.assertContains(response, self.md_post.title)
        self.assertIn('post', response.context)

    def test_visit_a_nonexistent_post(self):
        url = reverse('blog:detail', kwargs={'pk': 100})
        response = self.client.get(url)
        self.assertEqual(response.status_code, 404)

    def test_increase_views(self):
        self.client.get(self.url)
        self.md_post.refresh_from_db()
        self.assertEqual(self.md_post.views, 1)

        self.client.get(self.url)
        self.md_post.refresh_from_db()
        self.assertEqual(self.md_post.views, 2)

    def test_markdownify_post_body_and_set_toc(self):
        response = self.client.get(self.url)
        self.assertContains(response, '文章目录')
        self.assertContains(response, self.md_post.title)

        post_template_var = response.context['post']
        self.assertHTMLEqual(post_template_var.body_html, "<h1 id='标题'>标题</h1>")
        self.assertHTMLEqual(post_template_var.toc, '<li><a href="#标题">标题</li>')

接下来是测试 admin 添加文章和 rss 订阅内容,这一块比较简单,因为大部分都是 django 的逻辑,django 已经为我们进行了测试,我们需要测试的只是自定义的部分,确保自定义的逻辑按照预期的定义运行,并且得到了预期的结果。

对于 admin,预期的结果就是发布文章后,的确自动填充了 author:

class AdminTestCase(BlogDataTestCase):
    def setUp(self):
        super().setUp()
        self.url = reverse('admin:blog_post_add')

    def test_set_author_after_publishing_the_post(self):
        data = {
            'title': '测试标题',
            'body': '测试内容',
            'category': self.cate1.pk,
        }
        self.client.login(username=self.user.username, password='admin')
        response = self.client.post(self.url, data=data)
        self.assertEqual(response.status_code, 302)

        post = Post.objects.all().latest('created_time')
        self.assertEqual(post.author, self.user)
        self.assertEqual(post.title, data.get('title'))
        self.assertEqual(post.category, self.cate1)
  • reverse('admin:blog_post_add') 获取 admin 管理添加博客文章的 URL,django admin 添加文章的视图函数名为 admin:blog_post_add,一般 admin 后台操作模型的视图函数命名规则是 <app_label>_<model_name>_<action>
  • self.client.login(username=self.user.username, password='admin') 登录用户,相当于后台登录管理员账户。
  • self.client.post(self.url, data=data) ,向添加文章的 url 发起 post 请求,post 的数据为需要发布的文章内容,只指定了 title,body和分类。

接着我们进行一系列断言,确认是否正确创建了文章。

RSS 测试也类似,我们期待的是,它返回的内容中的确包含了全部文章的内容:

class RSSTestCase(BlogDataTestCase):

    def setUp(self):
        super().setUp()
        self.url = reverse('rss')

    def test_rss_subscription_content(self):
        response = self.client.get(self.url)
        self.assertContains(response, AllPostsRssFeed.title)
        self.assertContains(response, AllPostsRssFeed.description)
        self.assertContains(response, self.post1.title)
        self.assertContains(response, self.post2.title)
        self.assertContains(response, '[%s] %s' % (self.post1.category, self.post1.title))
        self.assertContains(response, '[%s] %s' % (self.post2.category, self.post2.title))
        self.assertContains(response, self.post1.body)
        self.assertContains(response, self.post2.body)

测试模板标签

这里测试的核心内容是,模板中 {% templatetag %} 被渲染成了正确的 HTML 内容。你可以看到测试代码中对应的代码:

context = Context(show_recent_posts(self.ctx))
template = Template(
    '{% load blog_extras %}'
    '{% show_recent_posts %}'
)
expected_html = template.render(context)

注意模板标签本质上是一个 Python 函数,第一句代码中我们直接调用了这个函数,由于它需要接受一个 Context 类型的标量,因此我们构造了一个空的 context 给它,调用它将返回需要的上下文变量,然后我们构造了一个需要的上下文变量。

接着我们构造了一个模板对象。

最后我们使用构造的上下文去渲染了这个模板。

我们调用了模板引擎的底层 API 来渲染模板,视图函数会渲染模板,返回响应,但是我们没有看到这个过程,是因为 django 帮我们在背后的调用了这个过程。

全部模板引擎的测试套路都是一样,构造需要的上下文,构造模板,使用上下文渲染模板,断言渲染的模板内容符合预期。以为例:

def test_show_recent_posts_with_posts(self):
    post = Post.objects.create(
        title='测试标题',
        body='测试内容',
        category=self.cate,
        author=self.user,
    )
    context = Context(show_recent_posts(self.ctx))
    template = Template(
        '{% load blog_extras %}'
        '{% show_recent_posts %}'
    )
    expected_html = template.render(context)
    self.assertInHTML('<h3 class="widget-title">最新文章</h3>', expected_html)
    self.assertInHTML('<a href="{}">{}</a>'.format(post.get_absolute_url(), post.title), expected_html)

这个模板标签对应侧边栏的最新文章版块。我们进行了2处关键性的内容断言。一个是包含最新文章版块标题,一个是内容中含有文章标题的超链接。

测试辅助方法和类

我们的博客中只自定义了关键词高亮的一个逻辑。

class HighlighterTestCase(TestCase):
    def test_highlight(self):
        document = "这是一个比较长的标题,用于测试关键词高亮但不被截断。"
        highlighter = Highlighter("标题")
        expected = '这是一个比较长的<span class="highlighted">标题</span>,用于测试关键词高亮但不被截断。'
        self.assertEqual(highlighter.highlight(document), expected)

        highlighter = Highlighter("关键词高亮")
        expected = '这是一个比较长的标题,用于测试<span class="highlighted">关键词高亮</span>但不被截断。'
        self.assertEqual(highlighter.highlight(document), expected)

这里 Highlighter 实例化时接收搜索关键词作为参数,然后 highlight 将搜索结果中关键词包裹上 span 标签。

Highlighter 事实上 haystack 为我们提供的类,我们只是定义了 highlight 方法的逻辑。我们又是如何知道 highlight 方法的逻辑呢?如何进行测试呢?

我是看源码,大致了解了 Highlighter 类的实现逻辑,然后我从 haystack 的测试用例中找到了 highlight 的测试方法。

所以,有时候不要惧怕去看源代码,Python 世界里一切都是开源的,源代码也没有什么神秘的地方,都是人写的,别人能写出来,你学习后也一样能写出来。单元测试的代码一般比较冗长重复,但目的也十分明确,而且大都以顺序逻辑组织,代码自成文档,非常好读。

单纯看文章中的讲解你可能仍有迷惑,但是好好读一遍示例项目中测试部分的源代码,你一定会对单元测试有一个更加清晰的认识,然后依葫芦画瓢,写出对自己项目代码的单元测试。

-- EOF --