写点什么

5 分钟快速上手 pytest 测试框架!

  • 2024-08-31
    湖南
  • 本文字数:3845 字

    阅读完需:约 13 分钟

  • 当 x=3.3 且 y=3 时,结果是否为 result=6.3 的情况

  • ……


我们也可以将参数堆叠起来进行组合,但效果也是类似:


import pytest
@pytest.mark.parametrize("x", [0, 1])@pytest.mark.parametrize("y", [2, 3])@pytest.mark.parametrize("result", [2, 4])def test_add(x, y, result): assert add(x,y) == result
复制代码


当然如果我们有足够多的参数,只要写进了 parametrize 中,pytest 依旧能帮我们把所有情况都给测试一遍。这样我们就再也不用写多余的代码。


但需要注意的是,parametrize 和我们后面将要讲到的一个重要的概念 fixture 会有一些差异:前者主要是模拟不同参数下时待测对象会输出怎样的结果,而后者是在固定参数或数据的情况下,去测试会得到怎样的结果。

跳过测试

有些情况下我们的代码包含了针对不同情况、版本或兼容性的部分,那么这些代码通常只有在符合了特定条件下可能才适用,否则执行就会有问题,但产生的这个问题的原因不在于代码逻辑,而是因为系统或版本信息所导致,那如果此时作为用例测试或测试失败显然不合理。比如我针对 Python 3.3 版本写了一个兼容性的函数,add(),但当版本大于 Python 3.3 时使用必然会出现问题。


因此为了适应这种情况 pytest 就提供了 mark.skip 和 mark.skipif 两个标记,当然后者用的更多一些。


import pytestimport sys
@pytest.mark.skipif(sys.version_info >= (3,3))def test_add(x, y, result): assert add(x,y) == result
复制代码


所以当我们加上这一标记之后,每次在测试用例之前使用 sys 模块判断 Python 解释器的版本是否大于 3.3,大于则会自动跳过。

预期异常

代码只要是人写的必然会存在不可避免的 BUG,当然有一些 BUG 我们作为写代码的人是可以预期得到的,这类特殊的 BUG 通常也叫异常(Exception)。比如我们有一个除法函数:


def div(x, y):    return x / y
复制代码


但根据我们的运算法则可以知道,除数不能为 0;因此如果我们传递 y=0 时,必然会引发 ZeroDivisionError 异常。所以通常的做法要么就用 try...exception 来捕获异常,并且抛出对应的报错信息(我们也可以使用 if 语句进行条件判断,最后也同样是抛出报错):


def div(x, y):    try:        return x/y    except ZeroDivisionError:        raise ValueError("y 不能为 0")
复制代码


因此,此时在测试过程中,如果我们想测试异常断言是否能被正确抛出,此时就可以使用 pytest 提供的 raises() 方法:


import pytest
@pytest.mark.parametrize("x", [1])@pytest.mark.parametrize("y", [0])def test_div(x, y): with pytest.raises(ValueError): div(x, y)
复制代码


这里需要注意,我们需要断言捕获的是引发 ZeroDivisionError 后我们自己指定抛出的 ValueError,而非前者。当然我们可以使用另外一个标记化的方法(pytest.mark.xfail)来和 pytest.mark.parametrize 相结合:


@pytest.mark.parametrize(    "x,y,result",     [        pytest.param(1,0, None, marks=pytest.mark.xfail(raises=(ValueError))),    ])def test_div_with_xfail(x, y, result):    assert div(x,y) == result
复制代码


这样测试过程中会直接标记出失败的部分。

Fixture

在 pytest 的众多特性中,最令人感到惊艳的就是 fixture。关于 fixture 的翻译大部分人都直接将其直译为了「夹具」一词,但如果你有了解过 Java Spring 框架的 那么你在实际使用中你就会更容易将其理解为 IoC 容器类似的东西,但我自己认为它叫「载具」或许更合适。


因为通常情况下都是 fixture 的作用往往就是为我们的测试用例提供一个固定的、可被自由拆装的通用对象,本身就像容器一样承载了一些东西在里面;让我们使用它进行我们的单元测试时,pytest 会自动向载具中注入对应的对象。


这里我稍微模拟了一下我们在使用使用数据库时的情况。通常我们会通过一个数据库类创建一下数据库对象,然后使用前先进行连接 connect(),接着进行操作,最后使用完之后断开连接 close() 以释放资源。


# test_fixture.py
import pytest
class Database(object):
def __init__(self, database): self.database = database def connect(self): print(f"\n{self.database} database has been connected\n")
def close(self): print(f"\n{self.database} database has been closed\n")
def add(self, data): print(f"`{data}` has been add to database.") return True
@pytest.fixturedef myclient(): db = Database("mysql") db.connect() yield db db.close()
def test_foo(myclient): assert myclient.add(1) == True
复制代码


在这段代码中,实现载具的关键是 @pytest.fixture 这一行装饰器代码,通过该装饰器我们可以直接使用一个带有资源的函数将其作为我们的载具,在使用时将函数的签名(即命名)作为参数传入到我们的测试用例中,在运行测试时 pytest 则会自动帮助我们进行注入。



在注入的过程中 pytest 会帮我们执行 myclient() 中 db 对象的 connect() 方法调用模拟数据库连接的方法,在测试完成之后会再次帮我们调用 close() 方法释放资源。


pytest 的 fixture 机制是一个让我们能实现复杂测试的关键,试想我们以后只需要写好一个带有测试数据的 fixture,就可以在不同的模块、函数或者方法中多次使用,真正做到「一次生成,处处使用」。


当然 pytest 给我们提供了可调节载具作用域(scope)的情况,从小到大依次是:


  • function:函数作用域(默认)

  • class:类作用域

  • module:模块作用域

  • package:包作用域

  • session:会话作用域


载具会随着作用域的生命周期而诞生、销毁。所以如果我们希望创建的载具作用域范围增加,就可以在 @pytest.fixture() 中多增加一个 scope 参数,从而提升载具作用的范围。


虽然 pytest 官方为我们提供了一些内置的通用载具,但通常情况下我们自己自定义的载具会更多一些。所以我们都可以将其放到一个名为 conftest.py 文件中进行统一管理:


# conftest.py
import pytest
class Database: def __init__(self, database): self.database:str = database def connect(self): print(f"\n{self.database} database has been connected\n")
def close(self): print(f"\n{self.database} database has been closed\n")
def add(self, data): print(f"\n`{data}` has been add to database.") return True
@pytest.fixture(scope="package")def myclient(): db = Database("mysql") db.connect() yield db db.close()
复制代码


因为我们声明了作用域为同一个包,那么在同一个包下我们再将前面的 test_add() 测试部分稍微修改一下,无需显式导入 myclient 载具就可以直接注入并使用:


from typing import Union
import pytest
def add( x: Union[int, float], y: Union[int, float],) -> Union[int, float]: return x + y
@pytest.mark.parametrize( argnames="x,y,result", argvalues=[ (1,1,2), (2,4,6), ])def test_add( x: Union[int, float], y: Union[int, float], result: Union[int, float], myclient): assert myclient.add(x) == True assert add(x, y) == result
复制代码


之后运行 pytest -vs 即可看到输出的结果:


Pytest 扩展

对于每个使用框架的人都知道,框架生态的好坏会间接影响框架的发展(比如 Django 和 Flask)。而 pytest 预留了足够多的扩展空间,加之许多易用的特性,也让使用 pytest 存在了众多插件或第三方扩展的可能。


根据官方插件列表所统计,目前 pytest 有多大 850 个左右的插件或第三方扩展,我们可以在 pytest 官方的 Reference 中找到 Plugin List 这一页面查看,这里我主要只挑两个和我们下一章实践相关的插件:


相关插件我们可以根据需要然后通过 pip 命令安装即可,最后使用只需要简单的参照插件的使用文档编写相应的部分,最后启动 pytest 测试即可。

pytest-xdist

pytest-xdist 是一个由 pytest 团队维护,并能让我们进行并行测试以提高我们测试效率的 pytest 插件,因为如果我们的项目是有一定规模,那么测试的部分必然会很多。而由于 pytest 收集测试用例时是以一种同步的方式进行,因此无法充分利用到多核。


因此通过 pytest-xdist 我们就能大大加快每轮测试的速度。当然我们只需要在启动 pytest 测试时加上 -n <CPU_NUMBER> 参数即可,其中的 CPU 数量可以直接用 auto 代替,它会自动帮你调整 pytest 测试所使用的 CPU 核心数:


pytest-asyncio

pytest-asycnio 是一个让 pytest 能够测试异步函数或方法的扩展插件,同样是由 pytest 官方维护。由于目前大部分的异步框架或库往往都是会基于 Python 官方的 asyncio 来实现,因此 pytest-asyncio 可以进一步在测试用例中集成异步测试和异步载具。


我们直接在测试的函数或方法中直接使用 @pytest.mark.asyncio 标记装饰异步函数或方法,然后进行测试即可:


import asyncio
import pytest

async def foo(): await asyncio.sleep(1) return 1
@pytest.mark.asyncioasync def test_foo(): r = await foo() assert r == 1
复制代码

结语



网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。


参考:docs.qq.com/doc/DSlVlZExWQ0FRSE9H


用户头像

还未添加个人签名 2024-02-27 加入

还未添加个人简介

评论

发布
暂无评论
5 分钟快速上手 pytest 测试框架!_程序员_程序猿忙什么_InfoQ写作社区