构建基于 Python 的单元测试
这篇文章记录了如何基于 Python 构建一个 Web 系统的单元测试,涉及一些基本和高级用法。
测试分类
- 单元测试:单个模块的测试
- 集成测试:多个模块的测试
- 功能测试:项目的功能测试
其实就是范围不同,单元测试仅是系统特定一部分的测试,功能测试是将系统作为整体进行测试,集成测试介于两者之间。
单元测试库
最常用的是 unittest 和 pytest
- 继承 unittest 的 TestCase 类来组织单元测试
- assert 语句用来检测是否符合预期,而 pytest 提供了一些更强大的 assert 方法
- pytest 用来运行测试,它可以使用加强版的 assert,并且它完全支持 unittest
一个简单的单元测试
1 | import unittest |
运行:
1 | (venv) $ pytest |
pytest命令比较智能,它会自动识别单元测试,它假定以这样的名字:test_[something].py
或者 [something]_test.py
命名的模块都包含单元测试。同时它也会搜索子目录。
一般来说,单元测试统一放到 tests 目录下,和应用目录隔离开。
测试覆盖率
安装:pip install pytest-cov
运行 pytest --cov=fizzbuzz
,可以针对 fizzbuzz
模块运行单元测试以及覆盖率
1 | (venv) $ pytest --cov=fizzbuzz |
还有以下参数:
--cov-branch
针对分支处理,有多少个分支就统计多少次--cov-report=term-missing
表示以何种方式展示报告,term-missing
表示在terminal上展示,并且会额外加上缺少测试覆盖的代码行数,另外一个常用选项是html
在html上展示报告,很清晰,常用。
可以添加注释 pragma: no cover
来跳过该块代码的覆盖率检测
测试参数化
使用库 parameterized: pip install parameterized
1 | from parameterized import parameterized |
也可以使用列表推导式:
1 | class TestLife(unittest.TestCase): |
支持多参数:
1 | import itertools |
测试异常
1 | import pytest |
Mocking
mocking 就是劫持函数或者功能,可以控制返回值或者其他东西的一种功能。在测试中如果对某个函数已经有了详尽的测试,那么在这个函数被调用的地方,就可以用mocking功能,节约资源。
unittest 里的 mock 模块,可以使用 mock.patch_object()
来替换函数或者方法
1 | from unittest import mock |
测试 Web 应用
最好将测试归集到一个继承 unittest.TestCase 的类里,这样可以公用 setUp 和 tearDown 方法,会有更好的性能,以及更方便。
WSGI 和 ASGI 都有特定的规则用于服务器如何传递到应用的请求。所以我们可以注入假的请求到应用上来模拟,而不用启动真正的服务器。这些 Web 框架都有所谓的测试客户端(test clients)来帮助实现单元测试,不需要任何网络,会向应用传递假的请求。如果 Web 框架没有提供的话,WSGI 应用可以使用 Werkzeug
库,ASGI 应用可以使用 async-asgi-testclient
。
比如,Flask 框架可以直接使用自带的 test client:
1 | class TestWebApp(unittest.TestCase): |
Tornado 框架可以继承 HTTPTestCase or AsyncHTTPTestCase 类来实现,其中它自带了 HTTPClient 和 AsyncHTTPClient,可以直接使用:
1 |
|
测试 html 内容
没必要全部 match 去做测试,而是可以检查一部分内容是否存在,比如提交按钮是否存在于 html 中,而忽略其顺序等无关信息。
1 | def test_registration_form(self): |
这样的方式也适合于其他数据量比较大的测试,只需要测试关键部分即可。
提交表单
主要问题在于 CSRF token 怎么处理,可以先发一个 GET 请求,然后拿到 token,再去提交表单,这是一种方法。另一种方法就是在测试中禁掉 CSRF 的保护。
1 | def setUp(self): |
测试表单验证
根据表单验证失败返回的语句进行判断
1 | def test_register_user_mismatched_passwords(self): |
测试需要登陆验证的页面
有以下几点:
- setUp 方法初始化用户
- login 方法
- 完成对应测试
Example:
1 |
|
测试 API 服务器
比较简单,因为 API 接口第一涉及范围小,第二返回基本上都是 JSON,容易解析。
1 | def test_api_register_user(self): |