大家好,我是玉米君,本篇文章将从基础、断言、夹具、标记、配置、插件和布局几个方面带大家了解pytest测试框架,并配置脚本案例,让你熟悉起来更方便。

1. pytest特点和基本用法

Python内置了测试框架unit test,但是了解units同学知道它是一个拥有浓烈的Java风格,比如说类名、方法名字会使用驼峰,而且必须要继承父类才能的定义测试用例等等。

那有一些Python开发者,他觉得这种方式这种风格不太适应,所以做了一个更加pythonic的测试框架,最开始只是工具箱的一部分(py.test),后来这个测试框架独立出来的就成为了大名鼎鼎的pytest。

1.1 安装pytest

使用pip进行安装

pip install pytest -U

验证安装

pytest
pytest --version
pytest -h

1.2 创建测试用例

  1. 创建test_开头的python文件
  2. 编写test_开头的函数
  3. 在函数中使用assert 关键字
# test_main.py

def test_sanmu():
    a = 1
    b = 2
    assert a == b

1.3 执行测试用例

  • 自动执行所有的用例

    • pytest
  • 执行指定文件中所有用例
    • pytest filename.py
  • 执行指定文件夹中的所有文件中的所有用例
    • pyest dirname
  • 执行指定的用例
    • pytest test_a.py::test_b

测试发现:搜集用例

一般规则:

  1. 从当前目录开始,遍历每一个子目录 (不论这个目录是不是包)
  2. 在目录搜索test_*.py*_test.py,并导入(测试文件中的代码会自动执行)
  3. 在导入的模块手机以下特征的对象,作为测试用例
    1. test开头的函数
    2. Test开头类及其test开头方法 (这个类不应该有__init__
    3. unittest框架的测试用例

惯例(约定):和测试相关的一切,用test或者test_开头

1.4 读懂测试结果

import pytest

def test_ok():
    print("ok")

def test_fail():
    a, b = 1, 2
    assert a == b

def test_error(something):
    pass

@pytest.mark.xfail(reason="always xfail")
def test_xpass():
    pass

@pytest.mark.xfail(reason="always xfail")
def test_xfail():
    assert False

@pytest.mark.skip(reason="skip is case")
def test_skip():
    pass

pytest报告分为几个基本部分:

  1. 报告头
  2. 用例收集情况
  3. 执行状态
    1. 用例的结果
    2. 进度
  4. 信息
    1. 错误信息
    2. 统计信息
    3. 耗时信息

报告中的结果缩写符合是什么含义

符号 含义
. 测试通过
F 测试失败
E 出错
X XPass 预期外的通过
x xfailed 预期失败
s 跳过用例

如果要展示更加详细的结果,可以通过参数的方式设置

pytest -vrA

2. 断言

2.1 普通断言

pytest使用python内置关键字assert验证预期值和实际值

def test_b():
    a = 1
    b = 2

    assert a == b

pytest 和python处理方式不一样:

  1. 数值比较:会显示具体数值

2.2 容器型数据断言

如果是两个容器型数据(字符串、元组、列表、字典、数组),断言失败,会将两个数据进行diff比较,找出不用

def test_b():
    a = [1, 1, 1, 1, 1, 1, 1, 1, 1, 0]
    b = [1, 1, 1, 1, 1, 1, 1, 1, 1, 2]

    assert a == b, "a和b不相等"
>       assert a == b, "a和b不相等"
E       AssertionError: a和b不相等
E       assert [1, 1, 1, 1, 1, 1, ...] == [1, 1, 1, 1, 1, 1, ...]
E         At index 9 diff: 0 != 2
E         Full diff:
E         - [1, 1, 1, 1, 1, 1, 1, 1, 1, 2]
E         ?                             ^
E         + [1, 1, 1, 1, 1, 1, 1, 1, 1, 0]
E         ?   

2.3 断言出现异常

一般情况,:

  • 执行测试用例出现了异常,认为失败

  • 如果没有出现异常,认为通过。

“断言出现异常” :

  • 出现了异常,认为通过
  • 没有出现异常,认为失败
def test_b():

    with pytest.raises(ZeroDivisionError):
        1 / 0

不仅可以断言出现了异常,还可以断言出现什么异常,更可以断言谁引发的异常

def test_b():
    d = dict()
    with pytest.raises(KeyError) as exc_info:
        print(d["a"])  # 这行代码,预期不发生异常
        print(d["b"])  # 这行代码,预期异常

    assert "b" in str(exc_info.value)

2.4 断言出现警告

警告(Warning)是Exception的子类,但是它不是有raise关键字抛出,而是通过warnings.warn函数进行执行。

def f():
    pass
    warnings.warn("再过几天,就要放假了", DeprecationWarning)

def test_b():
    with pytest.warns(DeprecationWarning):
        f()

3. 夹具

单元代码?

创建测试用例:

  1. 创建test_开头的函数
  2. 在函数内使用断言关键字

一个测试用例的执行分为四个步骤:

  1. 预置条件
  2. 执行动作
  3. 断言结果
  4. 清理现场

为了重复测试结果不会异常,也为了不会干扰其他用例。

在理想情况,为了突出用例重点,用例中应该只有2(执行动作)和3(断言结果)

  • 1 和4 应当封装起来
  • 1 和4 能够自动执行

夹具(Fixture)是软件测试装置,作用和目的:

  • 在测试开始前,准备好相关的测试环境
  • 在测试结束后,销毁相关的内容

以socket聊天服务器作为例子,演示夹具的用法

socket服务的测试步骤:

  1. 建立socket连接
  2. 利用socket执行测试动作
  3. 对结果进行断言
  4. 断开socket

3.1 创建夹具

3.1.1 快速上手

夹具的特性:

  1. 在测试用例之前执行
  2. 具体重复性和必要性

夹具client:自动和server建立socket连接,并供用例使用

创建一个函数,并使用@pytest.fixture()装饰器即可

@pytest.fixture()
def client():
    client = socket.create_connection(server_address, 1)
    return client

3.1.2 setup 和 teardwon

pytest 有2种方式实现teardwon,这里只推荐一种: 使用yield关键字

函数中有了yield关键字之后,成了生成器,可以多次调用

@pytest.fixture()
def server():
    p = Process(target=run_server, args=(server_address,))
    p.start()  # 启动服务端
    print("启动服务端")
    yield p
    p.kill()

yield关键字 使夹具执行分为2个部分:

  1. yield之前的代码,在测试前执行,对应xUnit中setUP
  2. yield 之后的代码,在测试后执行,对应xUnit中yeadDown

3.1.3 夹具范围

夹具生命周期:

  1. 被需要用的时候创建
  2. 在结束范围的时候销毁
  3. 如果夹具存在,不会重复创建

pytest夹具范围有5种:

  • function:默认的范围,夹具在单个用例结束的时候被销毁
  • class: 夹具在类的最后一个用例结束的时候被销毁
  • module:夹具在模块的最后一个用例结束的时候被销毁
  • package:夹具在包的最后一个用例结束的时候被销毁
  • session:夹具在整个测试活动的最后一个用例结束的时候被销毁

使用Python,如果完全不会class,是没有任何问题的。

@pytest.fixture(scope="session")
def server():
    p = Process(target=run_server, args=(server_address,))
    p.start()  # 启动服务端
    print("启动服务端")
    yield p
    p.kill()

3.1.4 夹具参数化

夹具的参数,可以通过参数化的方式,为夹具产生多个结果 (产生了多个夹具)

如果测试用例要使用的夹具被参数化了,那么测试用例得到的夹具结果会有多个,每个夹具都会被使用

测试用例也会执行多次

测试用例,不知道自己被执行了多次,正如它不知道夹具被参数一样

@pytest.fixture(scope="session", params=[9001, 9002, 9003])
def server(request):
    port = request.param
    p = Process(target=run_server, args=(("127.0.0.1", port),))
    p.start()  # 启动服务端
    print("启动服务端")  # *3
    yield p
    p.kill()

3.2 使用夹具

3.2.1 在用例中使用

3.2.2 在夹具中使用

注意:夹具中使用夹具,必须确保范围是兼容的

例子:夹具A 和夹具B,A范围是function,B的范围是session,A可以使用B ,B不可用使用A

  • A在第一个用例结束的时候,被销毁
  • B在所有的用例结束的时候,被销毁
  • A比B先被销毁

使用实际上依赖的关系:

假设:

  • A使用B
    • B的setup
    • A
    • B的tearDown
  • B使用A (不可以的)
    • 第一个用例结束的时候 A被销毁,B该怎么办?
    • A的setUP
    • B
    • A的tearDown

生命周期短的夹具,才可用使用声明周期长的夹具

3.2.4 自动使用夹具

在一些代码质量工具中,未被使用的变量和参数,会被评为低质量。

pytest中,夹具可以声明自动执行,不需要写在用例参数列表中了。

@pytest.fixture(scope="function", autouse=True)
def server(request):
    port = 9001
    p = Process(target=run_server, args=(("127.0.0.1", port),))
    p.start()  # 启动服务端
    print("启动服务端")  # *3
    yield p
    p.kill()

4. 标记

默认情况下,pytest执行逻辑:

  1. 运行所有的测试用例
  2. 执行用例的时候,出现异常,判断为测试失败
  3. 执行用例的时候,没有出现异常,判断为测试通过

标记是给测试用例用的

标记的作用,就是为了改变默认行为:

  • userfixtures :在测试用例中使用夹具
  • skip:跳过测试用例
  • xfail: 预期失败
  • parametrize: 参数化测试,反复,多次执行测试用例
  • 自定义标记:提供筛选用例的条件,pytest只执行部分用例

4.1 userfixtures

@pytest.mark.usefixtures("server",)  # 只能给用例,使用夹具
class TestSocket:
    def test_create_client(self, client):
        print("客户端的地址", client.getsockname())
        print("服务端的地址", client.getpeername())

    def test_send_and_recv(self, client):
        data = "hello world\n"

        client.sendall(data.encode())  # 将字符串转为字节,然后发生

        f = client.makefile()

        msg = f.readline()

        assert data == msg

def test_():
    pass

4.2 skip 和 skipif

  • skip 无条件跳过
  • skipif 有条件跳过
class TestSocket:
    @pytest.mark.skip(reason="心情不美丽,不想执行这个测试")
    def test_create_client(self, client):
        print("客户端的地址", client.getsockname())
        print("服务端的地址", client.getpeername())

    def test_send_and_recv(self, client):
        data = "hello world\n"

        client.sendall(data.encode())  # 将字符串转为字节,然后发生

        f = client.makefile()

        msg = f.readline()

        assert data == msg
class TestSocket:
    @pytest.mark.skipif(sys.platform.startswith("win"), reason="心情不美丽,不想执行这个测试")
    def test_create_client(self, client):
        print("客户端的地址", client.getsockname())
        print("服务端的地址", client.getpeername())

4.3 xfail

无参数:无条件预期失败

有参数condition:有条件预期失败

有参数run: 预期失败的时候,不执行测试用例

有参数strict:预期外通过时,认为测试失败

@pytest.mark.xfail(1 != 1, reason="意料中的失败", run=False, strict=True)
def test_server_not_run():
    """当服务端未启动的时候,客户端应该连接失败"""

    my_socket = socket.create_connection(server_address, 1)

4.4 参数化

好处:

  1. 提供测试覆盖率 1,1 => 2, 1,0=>1, 9999999999,1=>100000000
  2. 反复测试,验证测试结果稳定性 1,1 => 2 1,1 => 2 1,1 => 2

本质:同一个测试代码可以执行多个测试用例

@pytest.mark.parametrize("n", [1, "x"])
def test_server_can_re_content(n):
    """测试服务器可以被多个客户端反复连接和断开"""
    print(n)
    my_socket = socket.create_connection(server_address)

4.5 自定义标记

提供筛选用例的条件,使pytest只执行部分用例

  • 选择简单的标记

    • pytest -m 标记
  • 选择复杂的标记
    • pytest -m "标记A and 标记B" 同时具有标记A 和标记B的用例
    • pytest -m "标记A or 标记B" 具有标记A 或标记B 的用例
    • pytest -m "not 标记A " 不具有标记A 的B用例
@pytest.mark.mmm
@pytest.mark.yumi
def test_sanmu():
    pass

@pytest.mark.mmm
@pytest.mark.danny
def test_yiran():
    pass

注册自定义标记:pytest知道哪些自定义标记是正确的,就不会发出警告

# pytest.ini
[pytest]
markers =
 mmm
 yumi
 danny

5. 配置

5.1 配置方法

  1. 命令行
    • 灵活
    • 如果有多个选项的话,不方便
  2. 配置文件
    • 特别适合大量,或者不常修改的选项
    • pytest.ini
    • pyproject.toml
      • pytest 6.0+ 支持
      • 是PEP标准
      • 是未来
  3. python代码动态配置
    • 太灵活, 意味着容易出错
    • 优先级是最高的
# conftest.py 会被pytest自动加载,适合写配置信息

def pytest_configure(config):  # 钩子:pytest会自动发现并运行这个函数

    config.addinivalue_line("markers", "mmm")
    config.addinivalue_line("markers", "yumi")
    config.addinivalue_line("markers", "danny")

5.2 配置项

  1. 查询帮助信息 pytest -h
  2. 查看pytest参考文档 https://docs.pytest.org/en/stable/reference.html#id90

约定大于配置

6. 插件

一般情况,插件是一个python的包,在pypi,使用pytest-开头

不一般的情况,需要把插件的在confgtest.py进行启用

6.1 安装插件

pip install pytest-html
pip install pytest-httpx  # mock httpx
pip install pytest-django  # test  django

6.2 使用插件

各个插件的使用方法 ,各不相同

参考各插件自己的问题

有些插件时自动启用的,不需要任何操作

6.3 禁用插件

添加参数

pytest -p no:插件名称
  • 包名称:pytest-html
  • 插件名称 :html

7. 布局

特性:

  1. 如果一个测试文件,存放在目录中,那么执行时,这个目录成为顶级目录
  2. 如果一个测试文件,存放在包中,那么执行时,根目录成为顶级目录
  3. python -m pytest ,将当前目录加入到sys.path ,当前目录中的模块可以被导入