Python全功能测试框架pytest

PyTest

一、 快速开始

1、 介绍

pytest是一个非常成熟的全功能的Python测试框架,主要有以下几个特点:

  • 简单灵活,容易上手
  • 支持参数化
  • 能够支持简单的单元测试和复杂的功能测试,还可以用来做selenium/appnium等自动化测试、接口自动化测试(pytest+requests)
  • pytest具有很多第三方插件,并且可以自定义扩展,比较好用的如pytest-selenium(集成selenium)、pytest-html(完美html测试报告生成)、pytest-rerunfailures(失败case重复执行)、pytest-xdist(多CPU分发)等
  • 测试用例的skip和xfail处理
  • 可以很好的和jenkins集成
  • report框架----allure 也支持了pytest

官方文档位置:https://docs.pytest.org/en/7.1.x/

2、 安装

这里使用的是poetry来进行安装的,poetry使用方法

poetry add pytest  # 添加pytest
poetry add pytest-html  # 添加 html 测试报告生成
pytest --version  # 检测是否安装成功

3、 第一个测试

创建一个test_*.py或者*_test..py的文件,运行的时候,pytest可以自动检测这些测试文件:

# content of test_sample.py
def func(x):
    return x + 1

def test_answer():
    assert func(3) == 5

你可以使用 assert 验证测试期望的声明。皮特试验 Advanced assertion introspection 将智能地报告断言表达式的中间值,以便避免使用多个名称 of JUnit legacy methods .

然后,我们就使用,pytest提供的命令开始进行测试:

pytest tests --html report.html  # --html 指定html输出的位置,tests是存放测试文件的文件夹

pytest 将运行窗体测试的所有文件_ *.py or * _当前目录及其子目录中的test.py

运行结果:

4、 断言引发异常

我们使用 raises 助手来断言某些代码引发异常:

# content of test_sysexit.py
import pytest


def f():
    raise SystemExit(1)


def test_mytest():
    with pytest.raises(SystemExit):
        f()
        
# 运行测试
# pytest -q

以“安静”报告模式执行测试功能: 它不会显示其为程序的异常,测试正常通过。

pytest中的-q参数代表保持输出简短 ,只输出程序异常的日志,其余的不输出。

5、 分组测试

一旦开发了多个测试,您可能需要将它们分组到一个类中。pytest使创建包含多个测试的类变得很容易:

# content of test_class.py
class TestClass:
    def test_one(self):
        x = "this"
        assert "h" in x

    def test_two(self):
        x = "hello"
        assert hasattr(x, "check")

pytest 发现以下所有测试 Conventions for Python test discovery ,所以它发现 test_ 前缀函数。没有必要对任何东西进行子类化,但是要确保在类前面加上 Test 否则将跳过该类。我们只需传递其文件名即可运行该模块:

pytest -q test_class.py

第一次测试通过,第二次失败。您可以很容易地看到断言中的中间值,以帮助您理解失败的原因。

将测试分组在类中是有益的,原因如下:

  • 试验机构
  • 仅在该特定类中共享测试夹具
  • 在类级别应用标记并将其隐式应用于所有测试

在类中对测试分组时需要注意的是,每个测试都有一个唯一的类实例。让每个测试共享同一个类实例将非常不利于测试隔离,并且会导致不良的测试实践。概述如下:

# content of test_class_demo.py
class TestClassDemoInstance:
    value = 0

    def test_one(self):
        self.value = 1  # 只有静态函数才可以修改类属性,这里是修改失败的
        assert self.value == 1

    def test_two(self):
        assert self.value == 1
pytest -k TestClassDemoInstance -q  
  • 在类级别添加的属性是类属性,因此它们将在测试之间共享

6、 添加临时目录

pytest提供内置固定装置/函数参数来请求任意资源,例如唯一的临时目录:

# content of test_tmp_path.py
def test_needsfiles(tmp_path):
    print(tmp_path)
    assert 0
# pytest -q test_tmp_path.py # 运行

列出名字 tmpdir 在测试函数签名和 pytest 将在执行测试函数调用之前查找并调用fixture工厂以创建资源。

使用pytest --fixtures命令查看有哪些ficture工厂。

二、 调用测试

1、 布局规则

这里使用的是poetry进行快速生成目录结构的,poetry使用方法

1.1 规则

pytest实现以下标准测试发现:

  • 如果未指定参数,则收集从testpaths (如果已配置)或当前目录开始。或者,命令行参数可用于目录、文件名或节点 ID 的任意组合。
  • 递归到目录,除非它们匹配norecursedirs
  • 在这些目录中,搜索test_*.py或文件,由它们的测试包名称*_test.py导入。
  • 从这些文件中,收集测试项目:
    • test类外的前缀测试函数或方法
    • test前缀测试类中的前缀测试函数或方法Test(没有__init__方法)

在 Python 模块中,pytest还使用标准 unittest.TestCase子类化技术发现测试。

1.2 测试布局
1.2.1 测试代码分离

如果您有许多功能测试或出于其他原因希望将测试与实际应用程序代码分开(通常是一个好主意),则将测试放入实际应用程序代码之外的额外目录可能会很有用:

pyproject.toml
src/
    mypkg/
        __init__.py
        app.py
        view.py
tests/
    test_app.py
    test_view.py
    ...

快速生成的方法:

poetry new --src mypkg

运行测试的方法:

pytest python -m pytest

通常,但特别是如果您使用默认导入模式prepend强烈建议使用src布局。在这里,您的应用程序根包位于根目录的子目录中,src/mypkg/而不是mypkg

1.2.2 测试代码结合

如果您在测试和应用程序模块之间有直接关系并且希望将它们与您的应用程序一起分发,则将测试目录内联到您的应用程序包中很有用:

pyproject.toml
[src/]mypkg/
    __init__.py
    app.py
    view.py
    test/
        __init__.py
        test_app.py
        test_view.py
        ...

运行方法:

pytest --pyargs mypkg

pytest将发现mypkg安装位置并从那里收集测试,这样可以很轻松的运行测试代码。

2、 运行测试

2.1 命令行调用

Pytest 支持多种从命令行运行和选择测试的方法。

# 在模块中运行测试
pytest test_mod.py

# 在目录中运行测试
pytest testing/

# 通过关键字表达式运行测试
pytest -k "MyClass and not method"

第三种方式:

  • 这将运行包含与给定字符串表达式匹配的名称(不区分大小写)的测试,其中可以包括使用文件名、类名和函数名作为变量的 Python 运算符。上面的例子会运行TestMyClass.test_something ,但不会TestMyClass.test_method_simple

按节点 ID 运行测试

每个收集到的测试都被分配一个唯一的nodeid,它由模块文件名和后面的说明符组成,如类名、函数名和参数化的参数,用::字符分隔。

# 要在模块中运行特定测试:
pytest test_mod.py::test_func

# 在命令行中指定测试方法的另一个示例:
pytest test_mod.py::TestClass::test_method

通过标记运行实例

pytest -m slow

将运行所有用@pytest.mark.slow装饰器装饰的测试。

从包运行测试

pytest --pyargs pkg.testing

这将导入pkg.testing并使用其文件系统位置来查找和运行测试。

获取版本信息等等

pytest --version   # shows where pytest was imported from
pytest --fixtures  # show available builtin function arguments
pytest -h | --help # show help on command line and config file options

分析测试执行持续时间

要获得超过 1.0 秒的最慢 10 个测试持续时间的列表

pytest --durations=10 --durations-min=1.0

通过python解释器调用测试

python -m pytest [...]  # 后面传递的参数和上面的类似
2.2 代码中调用

pytest您可以直接从 Python 代码调用:

retcode = pytest.main()

这就像您从命令行调用“pytest”一样。它不会引发SystemExit而是返回退出代码。您可以传入选项和参数:

retcode = pytest.main(["-x", "mytestdir"])

导入其他插件:

# content of myinvoke.py
import pytest
import sys


class MyPlugin:
    def pytest_sessionfinish(self):
        print("*** test run reporting finishing")


if __name__ == "__main__":
    sys.exit(pytest.main(["-qq"], plugins=[MyPlugin()]))

调用pytest.main()将导致导入您的测试和它们导入的任何模块。由于 python 导入系统的缓存机制,pytest.main()从同一进程进行后续调用不会反映调用之间对这些文件的更改。因此,pytest.main()不建议从同一进程多次调用(例如,为了重新运行测试)。

三、 使用方法

1、 断言

1.1 assert

pytest允许您使用标准 Pythonassert来验证 Python 测试中的期望值和值。例如,您可以编写以下内容:

# content of test_assert1.py
def f():
    return 3


def test_function():
    """如果失败,可以查看函数的返回值"""
    assert f() == 4
    assert a % 2 == 0, "value was odd, should be even"  # 指定自定义的异常断言信息
1.2 异常断言

为了编写有关引发异常的断言,您可以 pytest.raises()像这样使用上下文管理器:

def test_recursion_depth():
    with pytest.raises(RuntimeError) as excinfo:

        def f():
            f()

        f()
    assert "maximum recursion" in str(excinfo.value)  # excinfo保存着异常信息

excinfo是一个ExceptionInfo实例,它是引发的实际异常的包装器。感兴趣的主要属性 .type.value.traceback

您可以将match关键字参数传递给上下文管理器,以测试正则表达式是否匹配异常的字符串表示形式(类似于TestCase.assertRaisesRegex方法 from unittest):

import pytest


def myfunc():
    raise ValueError("Exception 123 raised")


def test_match():
    with pytest.raises(ValueError, match=r".* 123 .*"):
        myfunc()

也可以为pytest.mark.xfail指定一个“raises”参数 ,它以一种更具体的方式检查测试是否失败,而不仅仅是引发任何异常:

@pytest.mark.xfail(raises=IndexError)
def test_f():
    f()

pytest.raises()对于您正在测试自己的代码故意引发的异常的情况, 使用可能会更好,而使用@pytest.mark.xfail检查功能可能更适合记录未修复的错误(测试描述“应该”发生什么)或依赖项中的错误

2、 固定装置

2.1 简单使用

在基本层面上,测试函数通过将它们声明为参数来请求它们所需的fixtures

当 pytest 开始运行测试时,它会查看该测试函数签名中的参数,然后搜索与这些参数具有相同名称的fixtures。一旦 pytest 找到它们,它就会运行这些固定装置,捕获它们返回的内容(如果有的话),并将这些对象作为参数传递给测试函数。

import pytest


class Fruit:
    def __init__(self, name):
        self.name = name
        self.cubed = False

    def cube(self):
        self.cubed = True


class FruitSalad:
    def __init__(self, *fruit_bowl):
        self.fruit = fruit_bowl
        self._cube_fruit()

    def _cube_fruit(self):
        for fruit in self.fruit:
            fruit.cube()


# Arrange
@pytest.fixture
def fruit_bowl():
    return [Fruit("apple"), Fruit("banana")]


def test_fruit_salad(fruit_bowl):
    # Act
    fruit_salad = FruitSalad(*fruit_bowl)

    # Assert
    assert all(fruit.cubed for fruit in fruit_salad.fruit)

在这个例子中,test_fruit_salad需要 fruit_bowl作为依赖,当 pytest 看到这个时,它将执行夹具函数并将它返回的对象 作为参数传递。

注意:

  1. 固定装置可以嵌套调用

    # contents of test_append.py
    import pytest
    
    
    # Arrange
    @pytest.fixture
    def first_entry():
        return "a"
    
    
    # Arrange
    @pytest.fixture
    def order(first_entry):
        return [first_entry]
    
    
    def test_string(order):
        # Act
        order.append("b")
    
        # Assert
        assert order == ["a", "b"]
    
  2. 一个测试函数或者固定装置可以使用多个固定装置

    # contents of test_append.py
    import pytest
    
    
    # Arrange
    @pytest.fixture
    def first_entry():
        return "a"
    
    
    # Arrange
    @pytest.fixture
    def second_entry():
        return 2
    
    
    # Arrange
    @pytest.fixture
    def order(first_entry, second_entry):
        return [first_entry, second_entry]
    
    
    # Arrange
    @pytest.fixture
    def expected_list():
        return ["a", 2, 3.0]
    
    
    def test_string(order, expected_list):
        # Act
        order.append(3.0)
    
        # Assert
        assert order == expected_list
    
  3. 固定装置可以重复使用

    # contents of test_append.py
    import pytest
    
    
    # Arrange
    @pytest.fixture
    def first_entry():
        return "a"
    
    
    # Arrange
    @pytest.fixture
    def order(first_entry):
        return [first_entry]
    
    
    def test_string(order):
        # Act
        order.append("b")
    
        # Assert
        assert order == ["a", "b"]
    
    
    def test_int(order):
        # Act
        order.append(2)
    
        # Assert
        assert order == ["a", 2]
    

    在同一个测试期间,也可以多次请求夹具,并且 pytest 不会为该测试再次执行它们。这意味着我们可以在依赖于它们的多个固定装置中请求 固定装置(甚至在测试本身中再次请求固定装置),而无需多次执行这些固定装置。

  4. 自动使用固定装置

    # contents of test_append.py
    import pytest
    
    
    @pytest.fixture
    def first_entry():
        return "a"
    
    
    @pytest.fixture
    def order(first_entry):
        return []
    
    
    @pytest.fixture(autouse=True)
    def append_first(order, first_entry):
        return order.append(first_entry)
    
    
    def test_string_only(order, first_entry):
        assert order == [first_entry]
    
    
    def test_string_and_int(order, first_entry):
        order.append(2)
        assert order == [first_entry, 2]
    

    在此示例中,append_first夹具是自动使用夹具。因为它是自动发生的,所以两个测试都会受到它的影响,即使两个测试都没有 请求它。这并不意味着他们不能被请求;只是没有必要。

2.2 共享数据

需要网络访问的设备取决于连接性,并且通常创建起来很耗时。我们可以scope="module"在调用中添加一个参数来 @pytest.fixture添加一个smtp_connection固定装置,负责创建到一个预先存在的 SMTP 服务器的连接,每个测试模块只调用一次(默认是每个测试函数调用一次)。因此,测试模块中的多个测试函数将接收相同的smtp_connection夹具实例,从而节省时间。scope的可能值为:functionclassmodulepackagesession

下一个示例将fixture函数放入一个单独的conftest.py文件中,以便目录中多个测试模块的测试可以访问fixture函数:

# content of conftest.py
import pytest
import smtplib


@pytest.fixture(scope="module")
def smtp_connection():
    return smtplib.SMTP("smtp.gmail.com", 587, timeout=5)  # 这里也可以使用yield来返回参数,进行后续的内容
# content of test_module.py


def test_ehlo(smtp_connection):
    response, msg = smtp_connection.ehlo()
    assert response == 250
    assert b"smtp.gmail.com" in msg
    assert 0  # for demo purposes


def test_noop(smtp_connection):
    response, msg = smtp_connection.noop()
    assert response == 250
    assert 0  # for demo purposes

更加详细的使用方式,可以到官网自行查看

3、 标记测试

3.1 标记
# content of test_server.py

import pytest


@pytest.mark.webtest
def test_send_http():
    pass  # perform some webtest test for your app


def test_something_quick():
    pass


def test_another():
    pass


class TestClass:
    def test_method(self):
        pass

然后,您可以将测试运行限制为仅运行标记为的测试webtest

pytest -v -m webtest
pytest -v -m "not webtest"  # 运行除webtest之外的所有测试
3.2 节点
 pytest -v test_server.py::TestClass::test_method
 pytest -v test_server.py::TestClass test_server.py::test_send_http
3.2 名称

根据名称选择

您可以使用-k命令行选项来指定一个表达式,该表达式在测试名称上实现子字符串匹配,而不是在提供的标记上实现完全匹配-m。这使得根据名称选择测试变得容易:

pytest -v -k http
pytest -k "not send_http" -v
pytest -k "http or quick" -v

更高级的用法,可以到官方文档中自己学习,https://docs.pytest.org/en/7.1.x/index.html

  • 2
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

SteveKenny

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值