pytest

安装

pip install pytest

sample

test_sample.py

def inc(x):
    return x + 1


def test_answer():
    # 断言
    assert inc(1) == 2

执行测试

pytest test_sample.py

常用执行参数

  • 执行当前目录下名称中包含“关键字”的 py 文件: pytest -k "关键字"

  • 输出执行过程中打印的信息: pytest test_sample.py -vs

  • 执行添加了标签的测试用例

    • 给测试用例添加标签 @pytest.mark.标签名

    • 把标签添加到配置文件中:

      • 新建文件:pytest.ini

      • 添加:

        [pytest]
        markers = 标签名
        
    • 执行 pytest -m 标签名

  • 显示 fixture 的执行过程 --setupshow

setup, teardown

  • setup_module , teardown_module
  • setup_class, teardown_class
  • setup_function, teardown_function
  • setup , teardown

参数化

@pytest.mark.parametrize(key, value)

@pytest.mark.parametrize("a, b, result", [[1, 1, 2], [1, 99, 100], [1, 0, 1]])
    def test_add(self, a, b, result):
        assert self.calculator.add(a, b) == result

数据驱动

可以使用 Json , yaml ,这里选择 yaml

安装 yaml

pip install pyyaml

新建 yaml 文件 test.yaml

# [a, b , result]
#- a
#- b
#- result

# {a:1, b:2, result:3}
#a:
#  1
#b:
#  2
#result:
#  3

# [[1, 1, 2], [1, 99, 100], [1, 0, 1]]
#  - [1, 1, 2]
#  - [1, 99, 100]
#  - [1, 0, 1]

add:
  datas:
    - [ 1, 1, 2 ]
    - [ 1, 99, 100 ]
    - [ 1, 0, 1 ]
  # 说明/备注
  ids: ["case1", "case2", "case3"]

读取 yaml 文件

test_yaml.py

import yaml


def test_yaml():
    with open("../datas/test.yml") as f:
        # 读取 yml 文件中的内容
        data = yaml.safe_load(f)
        print(type(data))
        print(data)

执行:pytest test_yaml.py -vs

执行结果:

test_yaml.py::test_yaml 
{'add': {'datas': [[1, 1, 2], [1, 99, 100], [1, 0, 1]], 'ids': ['case1', 'case2', 'case3']}}

测试计算器

被测试的文件 calculator.py

# 计算器
class Calculator:
  # 加法
    def add(self, a, b):
        return a + b
    # 除法
    def div(self, a, b):
        return a / b

Yaml 文件 calc.yml

# 加法
add:
  datas:
    - [ 1, 1, 2 ]
    - [ 1, 99, 100 ]
    - [ 1, 0, 1 ]
  # 说明/备注
  ids: ["case1", "case2", "case3"]

# 除法
div:
  datas:
    - [10, 5, 2]
    - [9, 3, 3]
  ids: ["case1", "case2"]

# 除法,除数为0
div_error:
  datas:
    - [1, 0, 0]
  ids: ["ZeroDivisionError"]

测试文件 test_calc.py

import sys
import pytest
import yaml

sys.path.append("..")


from ..dev.calculator import Calculator


# 读取 yml 文件中的数据
def get_datas():
    with open("../datas/calc.yml") as f:
        datas = yaml.safe_load(f)
        return datas


class TestCalc:
    # 从 yml 文件中读取到的数据
    datas = get_datas()

    # setup_class 在类被调用的时候,在所有测试方法执行之前,执行一次
    def setup_class(self):
        print("我是 setup_class")
        self.calculator = Calculator()

    # setup 在每个测试方法执行之前,执行一次
    def setup(self):
        print("我是 setup")
        
    # 测试加法,使用从 yml 文件中读取到的数据进行参数化 (key, values, 描述)
    @pytest.mark.parametrize("a, b, result", datas["add"]["datas"], ids=datas["add"]["ids"])
    def test_add(self, a, b, result):
        assert self.calculator.add(a, b) == result

    # 测试除法
    @pytest.mark.parametrize("a, b, result", datas["div"]["datas"], ids=datas["div"]["ids"])
    def test_div(self, a, b, result):
            assert self.calculator.div(a, b) == result

    # 测试除法,除数为 0 的情况
    @pytest.mark.parametrize("a, b, result", datas["div_error"]["datas"], ids=datas["div_error"]["ids"])
    def test_div_error(self, a, b, result):
        # 使用 pytest 自带的异常捕获功能,捕获除数为 0 的异常
        with pytest.raises(ZeroDivisionError):
            result = a / b

    # 在每个测试方法执行之后,执行一次
    def teardown(self):
        print("我是 teardown")

    # 在所有测试方法执行之后,执行一次
    def teardown_class(self):
        print("我是 teardown_class")


执行结果:

collected 6 items                                                                                                                                                                                                         

test_calc.py::TestCalc::test_add[case1] 我是 setup_class
我是 setup
PASSED我是 teardown

test_calc.py::TestCalc::test_add[case2] 我是 setup
PASSED我是 teardown

test_calc.py::TestCalc::test_add[case3] 我是 setup
PASSED我是 teardown

test_calc.py::TestCalc::test_div[case1] 我是 setup
PASSED我是 teardown

test_calc.py::TestCalc::test_div[case2] 我是 setup
PASSED我是 teardown

test_calc.py::TestCalc::test_div_error[ZeroDivisionError] 我是 setup
PASSED我是 teardown
我是 teardown_class

Fixture

Fixture 的功能类似于 setup, teardown ,前者比后者更灵活。

比如测试搜索和购物车功能不需要登录,测试订单功能需要登录:

test_fixture.py

import pytest


# 给 login 函数添加 fixture
@pytest.fixture()
def login():
    print("登录")


def test_search():
    print("测试搜索功能")


def test_cart():
    print("测试购物车功能")


# 测试订单功能需要登录,把 login 函数作为参数传递进来
def test_order(login):
    print("测试订单功能")

查看 fixture 的执行过程:pytest test_fixture --setupshow

执行结果:

collected 3 items                                                                                                                                                                                                         

test_fixture.py 
        test_fixture.py::test_search.
        test_fixture.py::test_cart.
        SETUP    F login
        test_fixture.py::test_order (fixtures used: login).
        TEARDOWN F login

Fixture 的执行过程显示:会在执行测试订单功能之前和之后各执行一次登陆功能。

除了作为参数传递,还可以通过装饰器 @pytest.mark.usefixtures 调用:

@pytest.mark.usefixtures("login")
def test_order():
    print("测试订单功能")

添加多个 fixture

import pytest


# 给 login 函数添加 fixture
@pytest.fixture()
def login():
    print("登录")


@pytest.fixture()
def get_username():
    print("获取用户名")


def test_search():
    print("测试搜索功能")


def test_cart():
    print("测试购物车功能")


# 通过 usefixtures 调用 fixture
@pytest.mark.usefixtures("get_username")
@pytest.mark.usefixtures("login")
def test_order():
    print("测试订单功能")

执行测试:pytest test_fixture -vs

执行结果:

collected 3 items                                                                                                                                                                                                         

test_fixture.py::test_search 测试搜索功能
PASSED
test_fixture.py::test_cart 测试购物车功能
PASSED
test_fixture.py::test_order 登录
获取用户名
测试订单功能
PASSED

执行结果显示会先调用 login 再调用 get_username

@pytest.fixture(autouse=True)

如果设置了 autouse=True 那么所有的测试方法都会调用该 fixture

@pytest.fixture(autouse=True)
def login():
    print("登录")

......

执行结果:

collected 3 items                                                                                                                                                                                                         

test_fixture.py::test_search 登录
测试搜索功能
PASSED
test_fixture.py::test_cart 登录
测试购物车功能
PASSED
test_fixture.py::test_order 登录
获取用户名
测试订单功能
PASSED

fixture 调用 fixture

@pytest.fixture()
def login():
    print("登录")

# fixture get_username 调用 fixture login
@pytest.fixture()
def get_username(login):
    print("获取用户名")

......

# 这里通过 usefixtures 只调用 get_username,不调用 login
@pytest.mark.usefixtures("get_username")
def test_order():
    print("测试订单功能")

执行结果:

collected 3 items                                                                                                                                                                                                         

test_fixture.py::test_search 测试搜索功能
PASSED
test_fixture.py::test_cart 测试购物车功能
PASSED
test_fixture.py::test_order 登录
获取用户名
测试订单功能
PASSED

执行结果显示在调用 get_username 之前,先调用了 login

fixture 的 yield

执行测试方法之前,执行 yield 之前的部分,执行测试方法之后,执行 yield 之后的部分

@pytest.fixture()
def login():
    print("登录")
    yield
    print("登出")
......

执行结果:

collected 3 items                                                                                                                                                                                                         

test_fixture.py::test_search 测试搜索功能
PASSED
test_fixture.py::test_cart 测试购物车功能
PASSED
test_fixture.py::test_order 登录
获取用户名
测试订单功能
PASSED登出

使用 yield 返回信息

......
# 给 login 函数添加 fixture
@pytest.fixture()
def login():
    print("登录")
    message = "我是 message"
    yield message
    print("登出")

......
# 此时只能通过传参的方式调用 fixture
def test_order(login):
    print("测试订单功能")
    print(login)

执行结果:

collected 3 items                                                                                                                                                                                                         

test_fixture.py::test_search 测试搜索功能
PASSED
test_fixture.py::test_cart 测试购物车功能
PASSED
test_fixture.py::test_order 登录
测试订单功能
我是 message
PASSED登出

注意,这里只能通过传参的方式调用 fixture ,不然 print(login) 输出的是 login 这个函数的对象。

scope

scope 用来设置 fixture 的作用域,类似于 setup, teardown 的 module, class, function, method 这种

scope 的作用域有:函数(默认),类,模块,session

"function" (default), "class", "module", "session" or "invocation".

  • session 全局/整个会话过程
  • 模块 模块被调用时执行一次
  • 类 类被调用时执行一次
  • 函数 函数/方法被调用时执行一次

conftest 模块

这个模块里面可以添加一些自定义的功能,在当前目录下的所有模块都可以共用这些自定义的功能。可以把常用的 fixture 放在这里。使用了 conftest 中的功能的模块,在执行的时候,会在当前目录下找 conftest 模块,如果当前目录下没有找到 conftest 模块,会向上一级目录查找...

conftest 所在的目录必须是一个包(有 __init__.py 文件)

新建 conftest.py (文件名固定,不能修改)

import pytest


@pytest.fixture()
def login():
    print("登录")
    message = "我是 message"
    yield message
    print("登出")

test_fixture.py 只保留以下内容:

def test_search():
    print("测试搜索功能")


def test_cart():
    print("测试购物车功能")


def test_order(login):
    print("测试订单功能")
    print(login)

执行:pytest test_fixture.py -vs

执行结果:

collected 3 items                                                                                                                                                                                                         

test_fixture.py::test_search 测试搜索功能
PASSED
test_fixture.py::test_cart 测试购物车功能
PASSED
test_fixture.py::test_order 登录
测试订单功能
我是 message
PASSED登出

可以看到,test_fixture.py 文件中不需要导入任何模块,就可以执行成功。

使用 fixture 替换测试计算机模块中的 setup 和 teardown

test_calc.py

import sys
import pytest
import yaml

sys.path.append("..")


from ..dev.calculator import Calculator


# 读取 yml 文件中的数据
def get_datas():
    with open("../datas/calc.yml") as f:
        datas = yaml.safe_load(f)
        return datas

# 添加 fixture 替换 setup 和 teardown
@pytest.fixture()
def get_instance():
    print("开始")
    calculator = Calculator()
    yield calculator
    print("结束")



class TestCalc:
    # 从 yml 文件中读取到的数据
    datas = get_datas()

    # 通过传参的方式调用 get_instance
    @pytest.mark.parametrize("a, b, result", datas["add"]["datas"], ids=datas["add"]["ids"])
    def test_add(self, a, b, result, get_instance):
        assert get_instance.add(a, b) == result

    # 通过传参的方式调用 get_instance
    @pytest.mark.parametrize("a, b, result", datas["div"]["datas"], ids=datas["div"]["ids"])
    def test_div(self, a, b, result, get_instance):
            assert get_instance.div(a, b) == result

    # 测试除法,除数为 0 的情况
    @pytest.mark.parametrize("a, b, result", datas["div_error"]["datas"], ids=datas["div_error"]["ids"])
    def test_div_error(self, a, b, result):
        # 使用 pytest 自带的异常捕获功能,捕获除数为 0 的异常
        with pytest.raises(ZeroDivisionError):
            result = a / b

执行:pytest test_calc.py -vs

执行结果:

collected 6 items                                                                                                                                                                                                         

test_calc.py::TestCalc::test_add[case1] 开始
PASSED结束

test_calc.py::TestCalc::test_add[case2] 开始
PASSED结束

test_calc.py::TestCalc::test_add[case3] 开始
PASSED结束

test_calc.py::TestCalc::test_div[case1] 开始
PASSED结束

test_calc.py::TestCalc::test_div[case2] 开始
PASSED结束

test_calc.py::TestCalc::test_div_error[ZeroDivisionError] PASSED

fixture 参数化

test_fixture_param.py

# fixture 参数化
import pytest


@pytest.fixture(params=["xiaoming", "xiaohong"])
def login(request):
    print("登录")
    # 返回参数,这里 request 也是一个 fixture
    return request.param


def test_login(login):
    name = login
    print(name + "登录")

执行:pytest test_fixture_param.py -vs

执行结果:

collected 2 items                                                                                                                                                                                                         

test_fixture_param.py::test_login[xiaoming] 登录
xiaoming登录
PASSED
test_fixture_param.py::test_login[xiaohong] 登录
xiaohong登录
PASSED

常用插件

失败用例重新运行

pytest-rerunfailures

安装 pip install pytest-rerunfailures

test_rerun.py

def test_rerun():
    assert 1 == 2

重新运行 5 次,执行:pytest test_rerun.py -vs --reruns 5

执行结果:

collected 1 item                                                                                                                                                                                                          

test_rerun.py::test_rerun RERUN
test_rerun.py::test_rerun RERUN
test_rerun.py::test_rerun RERUN
test_rerun.py::test_rerun RERUN
test_rerun.py::test_rerun RERUN
test_rerun.py::test_rerun FAILED

重新运行 5 次,每次之间间隔 1 秒钟:

  • 方式一,执行:pytest test_rerun.py -vs --reruns 5 --reruns-delay 1

  • 方式二,使用装饰器:

    import pytest
    
    
    @pytest.mark.flaky(reruns=5, reruns_delay=1)
    def test_rerun():
        assert 1 == 2
    

控制执行顺序

pytest-ordering

安装 pip install pytest-ordering

使用前:test_order.py

def test_foo():
    assert True


def test_bar():
    assert True

执行:pytest test_order.py -vs

执行结果:

collected 2 items                                                                                                                                                                                                         

test_order.py::test_foo PASSED
test_order.py::test_bar PASSED

使用后:test_order.py

import pytest


@pytest.mark.run(order=2)
def test_foo():
    assert True


@pytest.mark.run(order=1)
def test_bar():
    assert True

执行结果:

collected 2 items                                                                                                                                                                                                         

test_order.py::test_bar PASSED
test_order.py::test_foo PASSED

通过对比执行结果可以看到,使用前是按照从上到下的顺序执行的,使用后是按照 order 的值由小到大执行的。

用例依赖

pytest-dependency

设置用例之间的依赖关系,

安装 pip install pytest-dependency

添加依赖:@pytest.mark.dependency()

test_dependency.py

# 官方例子
import pytest


@pytest.mark.dependency()
@pytest.mark.xfail(reason="deliberate fail")
def test_a():
    # a 失败
    assert False

@pytest.mark.dependency()
def test_b():
    pass

@pytest.mark.dependency(depends=["test_a"])
def test_c():
    # c 依赖 a ,a 失败了,所以 c 不会执行
    pass

@pytest.mark.dependency(depends=["test_b"])
def test_d():
    # d 依赖 b
    pass

@pytest.mark.dependency(depends=["test_b", "test_c"])
def test_e():
    # e 依赖 b 和 c , c 不会执行,e 会不会执行呢?
    pass

执行:pytest test_dependency.py -vs

执行结果:

collected 5 items                                                                                                                                                                                                         

test_dependency.py::test_a XFAIL (deliberate fail)
test_dependency.py::test_b PASSED
test_dependency.py::test_c SKIPPED (test_c depends on test_a)
test_dependency.py::test_d PASSED
test_dependency.py::test_e SKIPPED (test_e depends on test_c)

通过执行结果可以看到 c 和 e 都跳过了,没有执行。

并发执行

pytest-xdist

安装: pip install pytest-xdist

使用:

  • pytest -n number 用 number 指定线程数

  • pytest -n auto 自动分配线程数

test_xdist.py

import time


def test_1():
    time.sleep(3)
    print(1)


def test_2():
    time.sleep(3)
    print(2)


def test_3():
    time.sleep(3)
    print(3)

使用前,执行:pytest test_xdist.py -vs

执行结果:

collected 3 items                                                                                                                                                                                                         

test_xdist.py::test_1 1
PASSED
test_xdist.py::test_2 2
PASSED
test_xdist.py::test_3 3
PASSED

3 passed in 9.02s 

使用后,执行:pytest test_xdist.py -vs -n auto

执行结果:

[gw0] PASSED test_xdist.py::test_1 
[gw2] PASSED test_xdist.py::test_3 
[gw1] PASSED test_xdist.py::test_2 

3 passed in 4.10s 

对比使用前后的执行结果,可以看到使用后的执行用时缩短了近 5 秒钟。

hook 函数

hook 函数 /venv/lib/python3.9/site-packages/_pytest/hookspec.py

我们可以对 hook 函数根据自己的需求做一些修改,修改好之后放在 conftest 模块中,这样在执行测试的时候会先加载 conftest 模块,我们修改的 hook 函数也就生效了。

修改 pytest_collection_modifyitems

conftest.py

# 这个 hook 函数会收集所有测试用例
def pytest_collection_modifyitems(
    session: "Session", config: "Config", items: List["Item"]
) -> None:
    # 这里的 items 是测试用例列表

    # 对测试用例进行中文编码设置
    for item in items:
        # item.name 是测试用例的名称
        item.name = item.name.encode("utf-8").decode("unicode-escape")
        # item._nodeid 是测试用例的路径
        item._nodeid = item.nodeid.encode("utf-8").decode("unicode-escape")

        # 给测试用例加标签
        if "add" in item._nodeid:
            item.add_marker(pytest.mark.add)

    # 反转测试用例列表的顺序
    items.reverse()

打包文件

打包分享自己修改的 hook 函数

创建包,包里面包含:README.md, setup.py, tests, 包

安装软件:

  1. setuptools 虚拟环境中默认已经安装了

  2. wheel 需要自己安装:pip install wheel

打包,执行:python setup.py sdist bdist_wheel

执行完成之后,会多出两个文件夹:build 和 dist

进入 dist 目录下可以通过 pip 安装 :pip install pytest_encode-0.1-py3-none-any.whl

你可能感兴趣的:(pytest)