单元测试:Testing leads to failure, and failure leads to understanding

单元测试的概念可能多数读者都有接触过。作为开发人员,我们编写一个个测试用例,测试框架发现这些测试用例,将它们组装成测试 suite 并运行,收集测试报告,并且提供测试基础设施(断言、mock、setup 和 teardown 等)。Python 当中最主流的单元测试框架有三种,Pytest, nose 和 Unittest,其中 Unittest 是标准库,其它两种是第三方工具。在 ppw 向导生成的项目中,就使用了 Pytest 来驱动测试。

这里主要比较一下 pytest 和 unittest。多数情况下,当我们选择单元测试框架时,选择二者之一就好了。unitttest 基于类来组织测试用例,而 pytest 则是函数式的,基于模块来组织测试用例,同时它也提供了 group 概念来组织测试用例。pytest 的 mock 是基于第三方的 pytest-mock,而 pytest-mock 实际上只是对标准库中的 mock 的简单封装。单元测试都会有 setup 和 teardown 的概念,unittest 直接使用了 setUp 和 tearDown 作为测试入口和结束的 API,在 pytest 中,则是通过 fixture 来实现,这方面学习曲线可能稍微陡峭一点。在断言方面,pytest 使用 python 的关键字 assert 进行断言,比 unittest 更为简洁,不过断言类型上没有 unittest 丰富。

另外一个值得一提的区别是,unittest 从 python 3.8 起就内在地支持 asyncio,而在 pytest 中,则需要插件 pytest-asyncio 来支持。但两者在测试的兼容性上并没有大的不同。

pytest 的主要优势是有:

  1. pytest 的测试用例更简洁。由于测试用例并不是正式代码,开发者当然希望少花时间在这些代码上,因此代码的简洁程度很重要。
  2. 提供了命令行工具。如果我们仅使用 unittest,则执行单元测试必须要使用python -m unittest来执行;而通过 pytest 来执行单元测试,我们只需要调用pytest .即可。
  3. pytest 提供了 marker,可以更方便地决定哪些用例执行或者不执行。
  4. pytest 提供了参数化测试。

这里我们简要地举例说明一下什么是参数化测试,以便读者理解为什么参数化测试是一个值得一提的优点。

# 示例 7 - 1
import pytest
from datetime import datetime
from src.example import get_time_of_day@pytest.mark.parametrize("datetime_obj, expect",[(datetime(2016, 5, 20, 0, 0, 0), "Night"),(datetime(2016, 5, 20, 1, 10, 0), "Night"),(datetime(2016, 5, 20, 6, 10, 0), "Morning"),(datetime(2016, 5, 20, 12, 0, 0), "Afternoon"),(datetime(2016, 5, 20, 14, 10, 0), "Afternoon"),(datetime(2016, 5, 20, 18, 0, 0), "Evening"),(datetime(2016, 5, 20, 19, 10, 0), "Evening"),],
)
def test_get_time_of_day(datetime_obj, expect, mocker):mock_now = mocker.patch("src.example.datetime")mock_now.now.return_value = datetime_objassert get_time_of_day() == expect​

在这个示例中,我们希望用不同的时间参数,来测试 get_time_of_day 这个方法。如果使用 unittest,我们需要写一个循环,依次调用 get_time_of_day(),然后对比结果。而在 pytest 中,我们只需要使用 parametrize 这个注解,就可以传入参数数组(包括期望的结果),进行多次测试,不仅代码量要少不少,更重要的是,这种写法更加清晰。

基于以上原因,在后面的内容中,我们将以 pytest 为例进行介绍。

1. 测试代码的组织

我们一般将所有的测试代码都归类在项目根目录下的 tests 文件夹中。每个测试文件的名字,要么使用 test_.py,要么使用_test.py。这是测试框架的要求。如此以来,当我们执行命令如pytest tests时,测试框架就能从这些文件中发现测试用例,并组合成一个个待执行的 suite。

在 test_*.py 中,函数名一样要遵循一定的模式,比如使用 test_xxx。不遵循规则的测试函数,不会被执行。

一般来说,测试文件应该与功能模块文件一一对应。如果被测代码有多重文件夹,对应的测试代码也应该按同样的目录来组织。这样做的目的,是为了将商业逻辑与其测试代码对应起来,方便我们添加新的测试用例和对测试用例进行重构。

比如在 ppw 生成的示例工程中,我们有:


sample
├── sample
│   ├── __init__.py
│   ├── app.py
│   └── cli.py
├── tests
│   ├── __init__.py
│   ├── test_app.py
│   └── test_cli.py

注意这里面的__init__.py 文件,如果缺少这个文件的话,tests 就不会成为一个合法的包,从而导致 pytest 无法正确导入测试用例。

2. PYTEST

使用 pytest 写测试用例很简单。假设 sample\app.py 如下所示:

# 示例 7 - 2
def inc(x:int)->int:return x + 1

则我们的 test_app.py 只需要有以下代码即可完成测试:

# 示例 7 - 3
import pytest
from sample.app import incdef test_inc():assert inc(3) == 4

这比 unittest 下的代码要简洁很多。

2.1. 测试用例的组装

在 pytest 中,pytest 会按传入的文件(或者文件夹),搜索其中的测试用例并组装成测试集合 (suite)。除此之外,它还能通过 pytest.mark 来标记哪些测试用例是需要执行的,哪些测试用例是需要跳过的。

# 示例 7 - 4
import pytest@pytest.mark.webtest
def test_send_http():pass  # perform some webtest test for your appdef test_something_quick():passdef test_another():passclass TestClass:def test_method(self):pass

然后我们就可以选择只执行标记为 webtest 的测试用例:

$ pytest -v -m webtest=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y -- $PYTHON_PREFIX/bin/python
cachedir: .pytest_cache
rootdir: /home/sweet/project
collecting ... collected 4 items / 3 deselected / 1 selectedtest_server.py::test_send_http PASSED                                [100%]===================== 1 passed, 3 deselected in 0.12s ======================

从输出可以看出,只有 test_send_http 被执行了。

这里的 webtest 是自定义的标记。pytest 还内置了这些标记,有的也可以用来筛选用例:

  1. pytest.mark.filterwarnings, 给测试用例添加 filterwarnings 标记,可以忽略警告信息。
  2. pytest.mark.skip,给测试用例添加 skip 标记,可以跳过测试用例。
  3. pytest.mark.skipif, 给测试用例添加 skipif 标记,可以根据条件跳过测试用例。
  4. pytest.mark.xfail, 在某些条件下(比如运行在某个 os 上),用例本应该失败,此时就应使用此标记,以便在测试报告中标记出来。
  5. pytest.mark.parametrize, 给测试用例添加参数化标记,可以根据参数化的参数执行多次测试用例。

这些标记可以用 pytest --markers 命令查看。

2.2. pytest 断言

在测试时,当我们调用一个方法之后,会希望将其返回结果与期望结果进行比较,以决定该测试是否通过。这被称之为测试断言。

pytest 中的断言巧妙地拦截并复用了 python 内置的函数 assert,由于您很可能已经接触过 assert 了,因而使得这一部分的学习成本变得非常低。

# 示例 7 - 5
def test_assertion():# 判断基本变量相等assert "loud noises".upper() == "LOUD NOISES"# 判断列表相等assert [1, 2, 3] == list((1, 2, 3))# 判断集合相等assert set([1, 2, 3]) == {1, 3, 2}# 判断字典相等assert dict({"one": 1,"two": 2}) == {"one": 1,"two": 2}# 判断浮点数相等# 缺省地, ORIGIN  ± 1E-06assert 2.2 == pytest.approx(2.2 + 1e-6)assert 2.2 == pytest.approx(2.3, 0.1)# 如果要判断两个浮点数组是否相等,我们需要借助 NUMPY.TESTINGimport numpyarr1 = numpy.array([1., 2., 3.])arr2 = arr1 + 1e-6numpy.testing.assert_array_almost_equal(arr1, arr2)# 异常断言:有些用例要求能抛出异常with pytest.raises(ValueError) as e:raise ValueError("some error")msg = e.value.args[0]assert msg == "some error"

上面的代码分别演示了如何判断内置类型、列表、集合、字典、浮点数和浮点数组是否相等。这部分语法跟标准 python 语法并无二致。pytest 与 unittest 一样,都没有提供如何判断两个浮点数数组是否相等的断言,如果有这个需求,我们可以求助于 numpy.testing,正如例子中第 25~30 行所示。

有时候我们需要测试错误处理,看函数是否正确地抛出了异常,代码 32~37 演示了异常断言的使用。注意这里我们不应该这么写:

# 示例 7 - 6try:# CALL SOME_FUNC WILL RAISE VALUEERRORexcept ValueError as e:assert str(e) == "some error":else:assert False

上述代码看上去逻辑正确,但它混淆了异常处理和断言,使得他人一时难以分清这段代码究竟是在处理测试代码中的异常呢,还是在测试被调用函数能否正确抛出异常,明显不如异常断言那样清晰。

2.3. pytest fixture

一般而言,我们的测试用例很可能需要依赖于一些外部资源,比如数据库、缓存、第三方微服务等。这些外部资源的初始化和销毁,我们希望能够在测试用例执行前后自动完成,即自动完成 setup 和 teardown 的操作。这时候,我们就需要用到 pytest 的 fixture。

在这里插入图片描述

假定我们有一个测试用例,它需要连接数据库,代码如下(参见 code/chap07/sample/app.py)

# 示例 7 - 7
import asyncpg
import datetimeasync def add_user(conn: asyncpg.Connection, name: str, date_of_birth: datetime.date)->int:# INSERT A RECORD INTO THE CREATED TABLE.await conn.execute('''INSERT INTO users(name, dob) VALUES($1, $2)''', name, date_of_birth)# SELECT A ROW FROM THE TABLE.row: asyncpg.Record = await conn.fetchrow('SELECT * FROM users WHERE name = $1', 'Bob')# *ROW* NOW CONTAINS# ASYNCPG.RECORD(ID=1, NAME='BOB', DOB=DATETIME.DATE(1984, 3, 1))return row["id"]

我们先展示测试代码(参见 code/chap07/sample/test_app.py),再结合代码讲解 fixture 的使用:

# 示例 7 - 8
import pytest
from sample.app import add_user
import pytest_asyncio
import asyncio# PYTEST-ASYNCIO 已经提供了一个 EVENT_LOOP 的 FIXTURE, 但它是 FUNCTION 级别的
# 这里我们需要一个 SESSION 级别的 FIXTURE,所以我们需要重新实现
@pytest.fixture(scope="session")
def event_loop():policy = asyncio.get_event_loop_policy()loop = policy.new_event_loop()yield looploop.close()@pytest_asyncio.fixture(scope='session')
async def db():import asyncpgconn = await asyncpg.connect('postgresql://zillionare:123456@localhost/bpp')yield connawait conn.close()@pytest.mark.asyncio
async def test_add_user(db):import datetimeuser_id = await add_user(db, 'Bob', datetime.date(2022, 1, 1))assert user_id == 1

我们的功能代码很简单,就是往 users 表里插入一条记录,并返回它在表中的 id。测试代码调用 add_user 这个函数,然后检测返回值是否为 1(如果每次测试前都新建数据库或者清空表的话,那么返回的 ID 就应该是 1)。

这个测试显然需要连接数据库,因此我们需要在测试前创建一个数据库连接,然后在测试结束后关闭连接。并且,我们还会有多个测试用例需要连接数据库,因此我们希望数据库连接是一个全局的资源,可以在多个测试用例中共享。这就是 fixture 的用武之地。

fixture 是一些函数,pytest 会在执行测试函数之前(或之后)加载运行它们。但与 unitest 中的 setup 和 teardown 不同,pytest 中的 fixture 依赖是显式声明的。比如,在上面的 test_add_user 显式依赖了 db 这个 fixture(通过在函数声明中传入 db 作为参数),而 db 则又显示依赖 event_loop 这个 fixture。即使文件中还存在其它 fixture, test_add_user 也不会依赖到这些 fixture,因为依赖必须显式声明。

上面的代码中,我们演示的是对异步函数 add_user 的测试。显然,异步函数必须在某个 event loop 中执行,并且相关的初始化 (setup) 和退出操作 (teardown) 也必须在同一个 loop 中执行。这里是分别通过 pytest.mark.asyncio, pytest_asyncio 等 fixture 来实现的:

首先,我们需要将测试用例标注为异步执行,即上面的代码第 21 行。其次,test_add_user 需要一个数据库连接,该连接由 fixture db来提供。这个连接的获得也是异步的,因此,我们不能使用 pytest.fixutre 来声明该函数,而必须使用@pytest_asyncio.fixture 来声明该函数。

最后,我们还必须提供一个 event_loop 的 fixture,它是一切的关键。当某个函数被 pytest.mark.asyncio 装饰时,该函数将在 event_loop 提供的 event loop 中执行。

我们还要介绍一下出现在第 6 行和第 13 行中的 scope=‘session’。这个参数表示 fixture 的作用域,它有四个可选值:function, class, module 和 session。默认值是 function,表示 fixture 只在当前测试函数中有效。在上面的示例中,我们希望这个 event loop 在一次测试中都有效,所以将 scope 设置为 session。

上面的例子是关于异步模式下的测试的。对普通函数的测试更简单一些。我们不需要 pytest.mark.asynio 这个装饰器,也不需要 event_loop 这个 fixture。所有的 pytest_asyncio.fixture 都换成 pytest.fixture 即可(显然,它必须、也只能装饰普通函数,而非由 async 定义的函数)。

在这里插入图片描述

我们通过上面的例子演示了 fixture。与 markers 类似,要想知道我们的测试环境中存在哪些 fixtures,可以通过 pytest --fixtures 来显示当前环境中所有的 fixture。

$ pytest --fixtures------------- fixtures defined from faker.contrib.pytest.plugin --------------
faker -- .../faker/contrib/pytest/plugin.py:24Fixture that returns a seeded and suitable ``Faker`` instance.------------- fixtures defined from pytest_asyncio.plugin -----------------
event_loop -- .../pytest_asyncio/plugin.py:511Create an instance of the default event loop for each test case....------------- fixtures defined from tests.test_app ----------------
event_loop [session scope] -- tests/test_app.py:45db [session scope] -- tests/test_app.py:52

这里我们看到 faker.contrib 提供了一个名为 faker 的 fixture, 我们之前安装的、支持异步测试的 pytest_asyncio 也提供了名为 event_loop 的 fixture(为节省篇幅,其它几个省略了),以及我们自己测试代码中定义的 event_loop 和 db 这两个 fixture。

Pytest 还提供了一类特别的 fixture,即 pytest-mock。为了讲解方便,我们先安装 pytest-mock 这个插件,看看它提供的 fixture。

$ pip install pytest-mock
pytest --fixture------- fixtures defined from pytest_mock.plugin --------
class_mocker [class scope] -- .../pytest_mock/plugin.py:419Return an object that has the same interface to the `mock` module, buttakes care of automatically undoing all patches after each test method.mocker -- .../pytest_mock/plugin.py:419Return an object that has the same interface to the `mock` module, buttakes care of automatically undoing all patches after each test method.module_mocker [module scope] -- .../pytest_mock/plugin.py:419Return an object that has the same interface to the `mock` module, buttakes care of automatically undoing all patches after each test method.package_mocker [package scope] -- .../pytest_mock/plugin.py:419Return an object that has the same interface to the `mock` module, buttakes care of automatically undoing all patches after each test method.session_mocker [session scope] -- .../pytest_mock/plugin.py:419Return an object that has the same interface to the `mock` module, buttakes care of automatically undoing all patches after each test method.

可以看到 pytest-mock 提供了 5 个不同级别的 fixture。关于什么是 mock,这是下一节的内容。


本文摘自《Python能做大项目》,将由机械工业出版社出版。全书已发表在大富翁量化上,欢迎提前阅读!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/628065.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

JAVAEE初阶 文件IO(一)

这里写目录标题 一. 计算机中存储数据的设备1.1 CPU1.2 内存1.3 硬盘1.4 三种存储的区别 二.文件系统2.1 相对路径2.2 绝对路径2.3 .和..的含义2.4 例子2.5 everything工具 三.文件3.1 文本文件3.2 二进制文件 四. JAVA对于文件的API4.1 getParent getName getPath getAbsolute…

Jest单元测试:玩转代码的小捉迷藏!

Jest Jest 是什么? Jest 是一个流行的 JavaScript 测试框架,专注于简化和改进代码的测试流程。它由 Facebook 开发并维护,具有以下特点: 1、易用性:Jest 提供了一个简单而强大的测试框架,使得编写和运行测…

uniapp h5 发行后 微信第二次打开网址 页面白屏

发行后把网址给客户,第一次可以正常登录打开,第二次打开白屏 原因:第一次打开时没有token,所以跳转登录页,可以正常访问 第二次打开时有token,但是网址根目录没有配置默认页面,所以白屏 解决…

Windows Server调整策略实现999999个远程用户用时登录

正文共:1234 字 23 图,预估阅读时间:2 分钟 上篇文章中(Windows Server 2019配置多用户远程桌面登录服务器),我们主要介绍了Windows Server 2019在配置远程桌面时,如何通过3种方式创建本地用户账…

使用Qt连接scrcpy-server控制手机

Qt连接scrcpy-server 测试环境如何启动scrcpy-server1. 连接设备2. 推送scrcpy-server到手机上3. 建立Adb隧道连接4. 启动服务5. 关闭服务 使用QTcpServer与scrcpy-server建立连接建立连接并视频推流完整流程1. 开启视频推流过程2. 关闭视频推流过程 视频流的解码1. 数据包协议…

NVMe系统内存结构 - Meta Data

NVMe系统内存结构 - Meta Data 1 为什么需要数据保护2 Meta Data定义3 Meta Data传输方式4 常见Meta Data使用场景4.1 不带数据保护信息4.2 带数据保护信息“数据写”流程4.3 带数据保护信息“数据读”流程4.4 SSD内部加入数据保护信息4.5 SSD内部根据数据保护信息验证数据 本文…

如何在你的网站接入QQ登录?

文章目录 准备阶段申请QQ登录的权限创建应用最后上传qqlogin.php代码 准备阶段 国内服务器和备案域名需要你有张独一无二本人的身份证你正面手持身份证的图片一张100px*100px的网站图标 申请QQ登录的权限 首先访问qq互联,点击我直接访问 登陆完成后我们点击面的…

bash shell基础命令(一)

1.shell启动 shell提供了对Linux系统的交互式访问,通常在用户登录终端时启动。系统启动的shell程序取决于用户账户的配置。 /etc/passwd/文件包含了所有用户的基本信息配置, $ cat /etc/passwd root:x:0:0:root:/root:/bin/bash ...例如上述root账户信…

Python新年文字烟花简单代码

简单的Python新年烟花代码示例: import random import timedef create_firework():colors [红色, 橙色, 黄色, 绿色, 蓝色, 紫色]flashes [爆裂, 闪光, 旋转, 流星, 喷射]color random.choice(colors)flash random.choice(flashes)print(f"发射一枚{color…

redis之单线程和多线程

目录 1、redis的发展史 2、redis为什么选择单线程? 3、主线程和Io线程是怎么协作完成请求处理的? 4、IO多路复用 5、开启redis多线程 1、redis的发展史 Redis4.0之前是用的单线程,4.0以后逐渐支持多线程 Redis4.0之前一直采用单线程的主…

GUI编程(函数解析以及使用)

1.介绍 AWT(Abstract Window Toolkit)和Swing 是 Java 提供的用于创建图形用户界面(GUI)的类库。 AWT:AWT 是 Java 最早提供的 GUI 类库,它基于本地平台的窗口系统,使用操作系统的原生组件进行…

文件的创建时间可以修改吗,怎么改?

文件的创建时间可以修改吗,怎么改?文件的创建时间是由操作系统自动生成并记录的,通常情况下无法直接修改。创建时间是文件的属性之一,它反映了文件在文件系统中的生成时间。一旦文件被创建,其创建时间就被确定下来&…

Vulnhub-tr0ll-1

一、信息收集 端口收集 PORT STATE SERVICE VERSION 21/tcp open ftp vsftpd 3.0.2 | ftp-anon: Anonymous FTP login allowed (FTP code 230) |_-rwxrwxrwx 1 1000 0 8068 Aug 09 2014 lol.pcap [NSE: writeable] | ftp-syst: | STAT: | FTP …

分布式搜索——Elasticsearch

Elasticsearch 文章目录 Elasticsearch简介ELK技术栈Elasticsearch和Lucene 倒排索引正向索引倒排索引正向和倒排 ES概念文档和字段索引和映射Mysql与Elasticsearch 安装ES、Kibana安装单点ES创建网络拉取镜像运行 部署kibana拉取镜像部署 安装Ik插件扩展词词典停用词词典 索引…

Linux 内核大转变:是否将迈入现代 C++ 的时代?

Linux开发者 H. Peter Anvin 在邮件列表中重启了关于 Linux内核C代码转换为C的讨论,并陈述了自己的观点。说之前先看一下这个话题的历史背景。 早在2018年4月1日,Andrew Pinski提议将 Linux 内核源码转为 C,在文中写道之所以引入是由于以下优…

centos7配置时间同步网络时间

centos7配置时间同步网络时间 1、安装 NTP 工具。 sudo yum install -y ntp2启动 NTP 服务。 sudo systemctl start ntpd3、将 NTP 服务设置为开机自启动。 sudo systemctl enable ntpd4、验证 date

Xmind 网页端登录及多端同步

好久没用 Xmind 了,前几天登录网页端突然发现没办法登录了,总是跳转到 Xmind AI 页面。本以为他们不再支持网页端了,后来看提示才知道只是迁移到了新的网址,由原来的 xmind.works 现在改成了的 xmind.ai。又花费好长时间才重新登录…

JAVAEE——request对象(三)

1. request对象 1.1 知识点 &#xff08;1&#xff09;乱码问题的两种解决方式 &#xff08;2&#xff09;post和get提交的区别 &#xff08;3&#xff09;request接收同名参数的问题 1.2 具体内容 使用request接收参数 <%page contentType"text/html; charsetut…

探索2023年大模型与AIGC峰会:程序员的学习之旅与未来展望

在2023年的技术前沿&#xff0c;大模型与AIGC峰会无疑是一个备受瞩目的盛会。 作为程序员&#xff0c;你将从这次大会中学到什么&#xff1f;这次峰会将为你揭示哪些前沿科技趋势&#xff1f;让我们一起来探讨这个问题。 一、理解大模型与AIGC 大模型和AIGC是人工智能领域中两…

离线数据仓库-关于增量和全量

数据同步策略 数据仓库同步策略概述一、数据的全量同步二、数据的增量同步三、数据同步策略的选择 数据仓库同步策略概述 应用系统所产生的业务数据是数据仓库的重要数据来源&#xff0c;我们需要每日定时从业务数据库中抽取数据&#xff0c;传输到数据仓库中&#xff0c;之后…