前言
上一篇文章写了Pytest的基础用法
回顾上一篇《Pytest自动化测试框架》由于最新启动的Django项目,最终选型Pytest作为测试框架,就想着分享下测试代码的最佳实践。
为什么不是unittest
unittest作为官方的测试框架,在测试方面更加基础,并且可以再次基础上进行二次开发,同时在用法上格式会更加复杂;而pytest框架作为第三方框架,方便的地方就在于使用更加灵活,并且能够对原有unittest风格的测试用例有很好的兼容性,同时在扩展上更加丰富,可通过扩展的插件增加使用的场景,比如一些并发测试等;
依赖
这里笔者使用了poetry,主要依赖:
1 | pytest = "6.2.4" |
是pytest、pytest-django(django的插件包)和mock模块
pytest-django is a plugin for pytest that provides a set of useful tools for testing Djangoapplications and projects.
配置
一般在项目根目录创建pytest.ini
文件,对项目的pytest进行配置,更多配置可参考官网
以下是我的配置:
1 | [pytest] |
稍微解释一下:
- DJANGO_SETTINGS_MODULE为指定Django项目的settings文件,这里可以指定dev环境,区分测试、生产的配置,对开发人员友好
- testpaths为该项目的测试代码根目录,好像不写也问题不大,因为上一篇讲到,Pytest框架会自动识别测试文件和代码
- addopts为额外的操作选项,–reuse-db的意思为,重复使用测试的数据库,不然的话每次执行测试时,都会重新创建数据库,然后执行
migrate
操作,比较费时
配置完成后,然后命令行运行,就开始测试啦
1 | pytest |
目录结构
一切就绪,来看一下是测试框架的目录结构:
可以用tree命令生成目录结构
1 | backend/tests |
我们拿km目录当作知识库的测试目录,以下代码围绕这个来讲解
测试代码demo
先来看点测试代码:
以下为笔者改造的知识库的文章接口测试,如有雷同,纯属巧合
test_article.py
1 | # -*- coding: utf-8 -*- |
大体思路就是,请求DRF的接口,然后将返回的数据与期望的数据做对比
一般用assert
关键字来验证数据符合性,不然就抛出异常,表示该单元测试失败
所以我的理解里,测试代码的正确性,应该以能否获取该接口期望的数据格式为判断条件,也就是说,前端、客户端需要什么,测试的代码就应该验证什么。这似乎决定了测试的assert并没有严格的标准。
这里的fixture固件用法疑点重重,听我慢慢分析
固件fixtures
api_client
api_client是我们用来调用接口的client,十分通用,我们把它放在测试根目录的conftest.py里:
1 | # -*- coding: utf-8 -*- |
这里我们定义了api_client这个fixture,同时他依赖test_user这个fixture
-
test_user用来生成一个随机的Django内置用户,并且为其token赋值,这等于是mock了一个用于测试的用户
-
APIClient是
Django REST framework
测试模块中,自带的API请求客户端,继承了DjangoClient,为了更好地适配DRF,下面会写到DRF对测试的支持,client用法和requests包很类似。 -
api_client在拿到test_user这个随机用户后,把用户赋予给这个client,是对token的强制认证,不然通不过Django的认证中间件
然后使用api_client.post("/apis/articles/", params)
就可以请求到restful API风格的接口
1 | <!-- ATTENTION! |
create_article_params和article_id
1 | # -*- coding: utf-8 -*- |
article_id:从Article这张表中,随机一个主键id,我们在测试update、detail等接口会用到
init_article_data:给数据库创建几条数据,提供给list、detail等接口查询
但是这二者的用法也不同:
1 |
|
由此可见,api_client和article_id是直接使用的fixture方法,是有返回值的
而init_article_data,是没有返回值的,我们只是用来初始化数据,它类似于测试代码的前置运行时。
usefixtures与传fixture区别:
如果fixture有返回值,那么usefixture就无法获取到返回值,这个是装饰器usefixture与用例直接传fixture参数的区别。
当fixture需要用到return出来的参数时,只能讲参数名称直接当参数传入,不需要用到return出来的参数时,两种方式都可以。
DRF对测试的支持
REST framework 包含一些扩展 Django 现有测试框架的助手类,并改进对 API 请求的支持。
这里强烈推荐去看源码,以下列举了三种请求接口的方式,重点是APIClient
APIRequestFactory
扩展了 Django 现有的 RequestFactory
类。
APIRequestFactory
类支持与 Django 的标准 RequestFactory
类几乎完全相同的 API。这意味着标准的 .get()
, .post()
, .put()
, .patch()
, .delete()
, .head()
和 .options()
方法都可用。
1 | from rest_framework.test import APIRequestFactory |
APIClient
APIClient继承了APIRequestFactory和DjangoClient,扩展了 Django 现有的 Client
类。
APIClient
类支持与 Django 标准 Client
类相同的请求接口。这意味着标准的 .get()
, .post()
, .put()
, .patch()
, .delete()
, .head()
和 .options()
方法都可用。例如:
1 | from rest_framework.test import APIClient |
认证.login
login
方法的功能与 Django 的常规 Client
类一样。这使你可以对任何包含 SessionAuthentication
的视图进行身份验证。
1 | client = APIClient() |
要登出,请照常调用 logout
方法。
1 | client.logout() |
.credentials(**kwargs)
credentials
方法可用于设置 header,这些 header 将包含在测试客户端的所有后续请求中。
1 | from rest_framework.authtoken.models import Token |
请注意,第二次调用 credentials
会覆盖任何现有凭证。你可以通过调用没有参数的方法来取消任何现有的凭证。
1 | client.credentials() |
credentials
方法适用于测试需要验证 header 的 API,例如 basic 验证,OAuth1 和 OAuth2 验证以及简单令牌验证方案。
.force_authenticate(user=None, token=None)
有时你可能想完全绕过认证,强制测试客户端的所有请求被自动视为已认证。
如果你正在测试 API 但是不想构建有效的身份验证凭据以进行测试请求,则这可能是一个有用的捷径。
1 | user = User.objects.get(username='sapphire') |
要对后续请求进行身份验证,请调用 force_authenticate
将 user 和(或) token 设置为 None
。
1 | client.force_authenticate(user=None) |
CSRF 验证
默认情况下,使用 APIClient
时不应用 CSRF 验证。如果你需要明确启用 CSRF 验证,则可以通过在实例化客户端时设置 enforce_csrf_checks
标志来实现。
1 | client = APIClient(enforce_csrf_checks=True) |
像往常一样,CSRF 验证将仅适用于任何会话验证视图。这意味着 CSRF 验证只有在客户端通过调用 login()
登录后才会发生。
RequestsClient
REST framework 还包含一个客户端,用于使用流行的 Python 库 requests
与应用程序进行交互。 这可能是有用的,如果:
- 你期望主要从另一个 Python 服务与 API 进行交互,并且希望在与客户端相同的级别测试该服务。
- 你希望以这样的方式编写测试,以便它们也可以在分段或实时环境中运行。
它暴露了与直接使用请求会话完全相同的接口。
1 | client = RequestsClient() |
参考资料: