pytest学习整理

一、安装及入门

1.安装及使用条件

  • 支持的python版本:python2.6及以上
  • 支持的平台:Unix、Posix、Windows
  • 安装pytest:pip install –U pytest
  • 查看安装的pytest版本:pytest –version

2.入门小栗子

import pytest

# 被测函数
def fun(x):
    return x + 1

# 测试函数
def test_Fun():
    assert fun(3) == 5

if __name__ == '__main__':
    # 执行测试
    pytest.main()

执行结果:


可以看到返回了一个断言失败的结果

二、用例运行

1.pytest用例的设计原则

  • 文件名格式符合 test_*.py 及 *_test.py

  • 测试函数以 test_ 开头

  • 测试类以 Test 开头且类中的方法以 test_ 开头,并且类中不能有__init__ 方法

  • 所有的 package 必须要有 __init__.py 文件

2.执行用例的方式

  • 直接使用 pytest 命令会执行当前目录及子目录下所有 test_*.py 及 *_test.py 格式的文件

  • 若想执行指定文件中的测试用例,则可使用 pytest test_files,test_files 表示指定的用例目录或用例文件。例: pytest test_mod.py 或 pytest testing/

  • pytest –k “MyClass and not method” 按关键字表达式运行用例。这将运行包含与表达式匹配的名称的测试用例,匹配的名称可以是文件名、类名、函数名,并支持 python运算符 or 和 and 操作。上面的示例将运行TestMyClass.test_something但不运行TestMyClass.test_method_simple。

  • pytest test_mod.py::test_func 按节点ID运行测试。每次执行收集到的测试用例集合都会被分配一个唯一的nodeid,其中包含模块文件名,后跟说明符,如类名、函数名及参数,由:: 字符分隔。该命令表示执行指定模块中某条指定的测试用例。

  • pytest –m 自定义标记 运行指定mark标记的测试用例。如:pytest –m slow 这将会执行所有带@pytest.mark.slow装饰器的用例

  • pytest -pyargs pkg.testing 导入pkg.testing并使用其文件系统位置来查找和运行测试,简单说就是从包中运行测试。

3.pytest 常用命令行参数

  • -v--verbose 显示更详细的测试结果信息;例:pytest -v test_case.py

  • -q--quiet 启用“静默模式”,只在测试结果失败时显示详细信息,正常运行时不显示测试过程的详细信息。

  • -s--Silent:禁用捕获测试用例的标准输出和标准错误流,将这些输出显示在控制台上,便于调试和查看打印信息。

  • -n=NUM:启用多线程或多进程运行测试用例,需要安装pytest-xdist插件模块。

  • -k=VALUE:用例的nodeid包含VALUE值则该用例被执行。可以使用通配符或正则表达式来构建匹配规则。

  • -m=LABELNAME:仅执行被@pytest.mark.LABELNAME标记的用例。

  • -x--xfail:如果任何一个用例执行失败,则停止当前线程的测试执行。

  • --maxfail=NUM:当用例运行时,允许的最大失败次数为NUM,超过则立即停止执行。

  • --reruns=NUM:失败的用例尝试重跑NUM次。

  • -r char:用于在测试会话结束时显示测试结果摘要;例:pytest -rf test_case.py 表示测试运行时显示 fail 测试结果的详细信息。(char:(f)ailed, (e)rror, (s)kipped, (x)failed, (X)passed, (P)assed, a(ll)...其中 a 表示除了通过以外的所有结果)

更多参数的使用方式及作用可通过 pytest -h 查看

4.测试结果的状态

  • passed:测试通过
  • failed:断言失败;预期的失败结果与测试函数返回的失败结果不一致时,测试结果也是该状态
  • error:测试的程序代码报错
  • xfail:预期失败,测试函数或测试类上加了装饰器 @pytest.mark.xfail()

三、pytest常用

1.assert(断言)的使用

1.1.基础断言:

assert expression :表达式为True时,断言成功,测试结果为 passed;表达式为False时,断言失败,测试结果为 failed。例:

import pytest


def fun(x):
    return x ** 2


def test_fun():
    assert fun(3) == 9


def test_fun2():
    assert fun(4) < 10


if __name__ == "__main__":
    pytest.main()

执行结果:



1.2. 异常断言:

使用 pytest.raises 作为上下文管理器,当测试函数抛出异常时可以获取到对应的异常实例。例:

def test_zero_division():
    with pytest.raises(ZeroDivisionError):
        1 / 0

执行结果(抛出的错误与预期的错误一致,则执行成功):




若需要访问测试函数抛出的异常信息,那么可以这样:

import pytest


def zero_division():
    with pytest.raises(ZeroDivisionError) as excinfo:
        1 / 0

	# type, traceback, value 是异常实例的主要属性
    print(excinfo.type)
    print(excinfo.traceback)
    print(excinfo.value)


if __name__ == "__main__":
    zero_division()

执行结果:

注意:断言 value 值时需要将其转换为 str。如:

import pytest


def test_zero_division():
    with pytest.raises(ZeroDivisionError) as excinfo:
        1 / 0		# 这里是函数表达式

        assert 'zero' in str(excinfo.value)  # 从这里获得值


此外,上下文管理还接受 match 关键字参数,用以匹配表达式中的异常,例如:

import pytest


def fun():
    raise ValueError("exception 123 raised")


def test_fun():
    with pytest.raises(ValueError, match=r'.* 123 .*'):
        fun()

执行结果:



2.fixture函数

包含unittest的setUp() 和 tearDown() 方法,用于对测试用例做初始化或清扫工作,使用时更灵活。

那么,如何使用它呢?首先我们来看看fixture函数的初始化参数列表,并通过以下例子逐步熟悉使用它吧!

@pytest.fixture(scope = "function",params=None,autouse=False,ids=None,name=None)
def test():
	pass

2.1、scope:function、class、module、package、session

  • 值为session时,表示一次会话(一次运行算一次会话)内该fixture函数只调用一次(如果用到该fixture函数)。

  • 值为package时,表示运行测试时每个包前仅调用一次该fixture函数

  • 值为module时,表示运行测试时每个模块前仅调用一次该fixture函数(在conftest.py时有效,否则只在fixture函数所在模块有效)。

  • 值为class时,表示运行测试时每个测试类前仅调用一次该fixture函数

  • 值为function时,表示运行测试时每个测试函数前调用一次该fixture函数。

注意:范围比较大的 fixture 函数将优先被实例化

小栗子:

# test_fixture.py

import pytest


@pytest.fixture(scope="class")
def return_info():
    print('this is fixture !')
    return 'this is example!'


# 调用fixture
class TestClass:
	# 测试方法(非测试方法不能使用该方式)通过在形参中使用fixture函数名来调用fixture函数并获取其返回值
    def test_case(self, return_info):
        assert 'this' in return_info

    def test_case2(self, return_info):
        assert 'is' in return_info


if __name__ == "__main__":
    pytest.main()

执行结果:

可看到一个测试类里仅执行了一次 fixture 函数。

将 @pytest.fixture(scope="class") 中的 scope 值改为 "function" 再运行一次:

可看到该fixture函数执行了两次!


2.2、params

  • Fixture函数的可选形参列表(支持列表传入),默认值为None。

  • params的每个元素都会执行调用一次(每个使用到该fixture函数的函数都会被执行一次)

  • 与参数ids一起使用,作为每次每个元素调用的标识。

小栗子:

import pytest


@pytest.fixture(scope="class", params=[10, 20, 30], ids=['one', 'two', 'three'])
def return_num(request):  # request:pytest内建fixture函数,有字段param,通过request.param 获取注解中传入的params值
    return request.param  # 这里获取值的写法为固定写法


def test_num(return_num):
    print('parm的值为:{}'.format(return_num))
    assert return_num >= 5


if __name__ == '__main__':
    pytest.main()

执行结果:



2.3、autouse

默认False,表示不自动调用 fixture 函数。值为True时,该fixture函数调用效果根据scope的值而定,当scope的值为“function”时,测试方法执行前调用一次该fixture函数,且测试函数中,不需要传入该fixture函数名(fixture函数有返回值时仍需通过使用fixture函数名来获取返回值)。

小栗子:

import pytest


@pytest.fixture(scope="function", autouse=True)
def return_info():
    print('this is fixture !')
    return 'this is example!'


# 调用fixture
class TestClass:
    def test_case(self, return_info):	# 仍需通过fixture函数名来获取其返回值
        assert 'this' in return_info
	
	# 若无需fixture函数的返回值,则可不写fixture函数名,因autouse为True,此时不写fixture函数名仍可调用该fixture函数
    def test_case2(self):
        assert 'is' in 'this is example!'


if __name__ == "__main__":
    pytest.main()

执行结果:



2.4、ids

搭配 params 使用,有点用但不多。详见上方 params 例子。

2.5、name

用于在调用fixture函数时,重命名fixture函数名,有点用但不多。

小栗子:

import pytest


@pytest.fixture(name='num')
def return_num():
    return 10


def test_num(num):
    assert num == 20

注意:在调用fixture函数时,只能使用重命名后的函数名num, 使用实际函数名将不会生效

2.6、conftest.py——共享 fixture 函数

可以认为是一个专门存放fixture函数的配置文件,且测试文件(即文件命名符合test_*.py 及 *_test.py)中的测试函数可以直接引用fixture函数名来调用该fixture函数而不需要导入。

小栗子:

# conftest.py
import pytest


@pytest.fixture
def return_num():
    print('这是一个conftest.py中的fixture函数!')
    return 10
# test_case.py
import pytest


def test_num(return_num):
    assert return_num < 100


if __name__ == "__main__":
    pytest.main()

运行测试:




conftest.py 的特性:

  • conftest.py 文件名称固定,不可更改

  • pytest会默认读取conftest.py里面的所有fixture,因此测试文件中引用时无需导入相关 fixture 函数,非测试文件不可直接引入。

  • 生效范围:只对同一个package下的所有测试用例生效,且不同目录下可拥有各自的 conftest.py 文件;同一目录下,若有多个conftest.py,且拥有同名的fixture函数,那么调用时将只调用离测试文件近的 conftest.py 中的相关fixture函数。

tips:最顶层的 conftest.py 一般用于初始化动作,如接口测试中的登录获取token、WebUI自动化测试中的初始化driver等。


2.7、fixture函数的后置处理

前面所举的例子,都是 setUp() 的处理,接下来我们看看如何实现 tearDown() 。

使用 yield

import pytest


@pytest.fixture(autouse=True)
def exercise_yield():
    print('setUp() 部分')
    yield
    print('tearDown() 部分')


def test_num(return_num):
    assert return_num < 100


if __name__ == "__main__":
    pytest.main()

运行测试:

可看到yield之前的部分将在测试运行前被执行,之后的部分在测试运行完成后被执行。

注意:yield之前的代码若抛出异常或断言失败,那么yield之后的代码将不会被执行,当然,使用了该fixture函数的测试代码也不会执行

通过request上下文对象的 addfinalizer 方法来完成后置作用

import pytest


@pytest.fixture(scope="class", name='num')
def return_num(request):
    print("setUp代码块")

    def print_one():
        print('tearDown代码块 one')

    def print_two():
        print('tearDown代码块 two')

    request.addfinalizer(print_one)
    request.addfinalizer(print_two)


def test_case():
    assert 1


if __name__ == "__main__":
    pytest.main()

运行测试:


可看到两个addfinalizer 添加的方法都被执行了。

如果 setUp() 部分出现异常,会不会运行addfinalizer() 方法添加的 tearDown() 代码呢,试试看:

import pytest


@pytest.fixture()
def print_info(request):
    print("setUp代码块")
    print(1 / 0)

    def print_one():
        print('tearDown代码块 one')

    request.addfinalizer(print_one)


def test_exercise(print_info):
    assert 1


if __name__ == "__main__":
    pytest.main()

运行测试:

答案是不会。这里我稍微有点疑惑,很多答案都说,终结函数 addfinalizer() 函数即使 setUp() 代码部分抛出异常或断言失败时仍可执行 tearDown() 部分的代码,但实践来看,和yield并无区别,希望有大佬来解答我的疑惑。

两相比较,使用yield 看起来更简洁,终结函数 addfinalizer() 则可添加多个tearDown() 的处理函数。

3.mark 的使用

3.1、常见内置标记

通过使用 pytest.mark 你可以轻松地在测试用例上设置标记。常用的内置标记如下:

  • skip -始终跳过该测试用例。用法:在测试用例前添加装饰器 @pytest.mark.skip(reason) ,可传入的可选参数 reason 表示自定义的描述(关于为什么跳过的描述)
    当想要跳过整个模块的用例时,在模块代码中可以用:@pytest.skip(reason, allow_module_level=True)

  • skipif -遇到特定情况下跳过该测试用例。
    用法:@pytest.mark.skipif(条件表达式, reason),如果条件表达式为True,那么就会跳过该用例。

  • usefixtures -使用指定的fixture方法
    用法:@pytest.mark.usefixtures(fixture函数名称)
    注意:用此方式使用fixture函数时,无法获取其返回值,必须使用fixture函数名的方式获取其返回值

  • xfail -遇到特定情况,产生一个“期望失败“的输出,直接使用时,测试通过则标记为XPASS,失败则标记为XFAIL。
    用法:@pytest.mark.xfail(condition = None,reason = None,raises = None,run = True,strict = False)
    condition(bool):bool表达式
    reason(str):标记为xfail的原因
    raises(str):期望由测试函数引发的异常,异常若对应则通过测试,不对应无法通过测试
    run(bool):如果值为False,那么被标记的函数将不会被执行,并始终标记为xfail
    strict(bool):值为False,该函数将在终端输出中显示,并且无论被标记的测试是否通过,都不会导致测试套件整体失败;值为True,被标记的函数在终端输出显示中为xfailed,如果意外地通过测试,将使整体测试套件失败。

  • parametrize -在同一个测试用例上运行多次调用(数据驱动/参数化)。

注意:标记仅对测试用例有效。对fixture函数无效


3.2、自定义标记

在测试用例函数上可以打上自定义的标记,运行测试用例时,可以通过标签名来过滤要运行的测试用例:@pytest.mark.标记名
示例:

import pytest


@pytest.mark.smoke
def test_case_01():
    print('mark is smoke')
    assert 1


@pytest.mark.webtest
def test_case_02():
    print('no smoke,this is webtest')
    assert 1

运行测试 pytest -m "smoke" :

可看到只执行了标记为 “smoke”的测试。测试结果中出现了warnings,这是因为自定义的标记未注册。

注册自定义标记

  • 第一种方法:使用 pytest.ini 文件,该文件须和测试用例在同一个目录,或者在根目录下作用于全局。
# pytest.ini
[pytest]
markers =
    smoke
    webtest

若运行测试时出现错误:UnicodeDecodeError: 'gbk' codec can't decode byte 0xaa in position 48: illegal multibyte sequence 这是解决方法

  • 第二种方法:在 conftest.py 文件中使用如下钩子函数注册自定义标记
def pytest_configure(config):
    marker_list = ["smoke: this is smoke mark", "webtest"]
    # 注册自定义标记
    for marker in marker_list:
        config.addinivalue_line("markers", marker)

执行两个自定义标记的测试用例:

pytest –m "smoke or webtest"

运行结果:




4.参数化

有时我们需要使用不同的值对一个接口进行重复的测试,这时就需要进行参数化了,接下来让我们看看几种常见的参数化方式。

4.1、标记参数化

@pytest.mark.parametrize
用法:在用例类/方法前加上@pytest.mark.parametrize(‘参数名‘,参数列表/参数字典)

  • 使用列表:
import pytest


@pytest.mark.parametrize('int_type, float_type', [(10, 9.99), (100, 99.9)])  # 第一种传入多组参数的示例
def test_print(int_type, float_type):
    print(int_type)
    print(float_type)
    assert 1


parametrize = [(66, 66.66), (99, 99.99)]


@pytest.mark.parametrize('int_type, float_type', parametrize)  # 第二种传入多组参数的示例
def test_print_02(int_type, float_type):
    print(int_type)
    print(float_type)
    assert 1

使用列表时,还可装饰多个@pytest.mark.parametrize 进行参数间的排列组合测试:
# -*- coding:utf8 -*-
import pytest


@pytest.mark.parametrize('x', [1, 2])
@pytest.mark.parametrize('y', [3, 4])
def test_01(x, y):
    assert x * y % 2 == 0


if __name__ == "__main__":
    pytest.main()

  • 使用字典:
# -*- coding:utf8 -*-
import pytest

test_dict = [{'质数': 2}, {'水仙花数': 153}, {'完全数': 28}]


@pytest.mark.parametrize('case_dict', test_dict)
def test_print(case_dict):
    num_name = [key for key in case_dict.keys()]
    num_value = [value for value in case_dict.values()]
    print('特殊数名:%s, 举例:%s' % (num_name, num_value))
    assert 1

运行测试:



4.2、使用fixture函数进行参数化

# -*- coding:utf8 -*-
import pytest


@pytest.fixture(params=[33, 99])
def request_params(request):
    return request.param


def test_01(request_params):
    assert request_params ** 2 % 3 == 0

运行测试:



5.常用的pytest配置(pytest.ini文件的使用)

pytest.ini:pytest的配置文件,一个项目中仅存一个,且固定名字不可更改,若需使其作用于全局,则须将其放在项目根目录下。

5.1、markers

用于注册自定义标记:

[pytest]
markers =
    markers_one: this is markers_one
    markers_two
    markers_three

5.2、addopts

用于设置自定义执行参数,pytest运行时以此设置为默认执行条件

[pytest]
# 表示默认执行pytest时显示详细输出、不捕获标准输出,并且每个用例失败时重跑两次
addopts = -v -s --reruns=2

5.3、testpaths

定义测试文件存放路径。pytest会按照此配置去指定路径下寻找测试文件。

[pytest]
# 表示在当前目录下的test文件夹中查找测试文件
testpaths = ./test

5.4、自定义用例执行规则

[pytest]
# 表示pytest会执行所有以test开头的文件
python_files = test*.py
# 表示pytest会执行所有以Test开头的类
python_classes = Test*
# 表示pytest会执行所有以test开头的函数
python_functions = test*

5.5、xfail_strict

用于设置当预期失败的测试用例实际通过时是否应视为错误。如果设置为True,则预期失败的测试用例如果实际通过,将被视为错误。

[pytest]
# 值可设置为 True, False, 0, 1
xfail_strict = True

5.6、log_cli 和 log_cli_level

用于配置pytest的命令行日志记录。log_cli控制是否启用日志记录,而log_cli_level设置日志级别(如DEBUG, INFO, WARNING, ERROR, CRITICAL)

[pytest]
# 值可设置为 True, False, 0, 1
log_cli = True  
log_cli_level = INFO

平时测试自己写的自动化测试代码时有用,真正批量运行时不建议使用。

5.7、cache_dir

指定pytest缓存的目录,用于在多次运行之间缓存测试结果,加速测试执行。

[pytest]
cache_dir = .pytest_cache



可通过cmd命令 pytest --help 查看更多配置项的使用:

[pytest] ini-options in the first pytest.ini|tox.ini|setup.cfg file found:

  markers (linelist):   markers for test functions
  empty_parameter_set_mark (string):
                        default marker for empty parametersets
  norecursedirs (args): directory patterns to avoid for recursion
  testpaths (args):     directories to search for tests when no files or directories are given in the command line.
  filterwarnings (linelist):
                        Each line specifies a pattern for warnings.filterwarnings. Processed after -W/--pythonwarnings.
  usefixtures (args):   list of default fixtures to be used with this project
  python_files (args):  glob-style file patterns for Python test module discovery
  python_classes (args):
                        prefixes or glob names for Python test class discovery
  python_functions (args):
                        prefixes or glob names for Python test function and method discovery
  disable_test_id_escaping_and_forfeit_all_rights_to_community_support (bool):
                        disable string escape non-ascii characters, might cause unwanted side effects(use at your own
                        risk)
  console_output_style (string):
                        console output: "classic", or with additional progress information ("progress" (percentage) |
                        "count").
  xfail_strict (bool):  default for the strict parameter of xfail markers when not given explicitly (default: False)
  enable_assertion_pass_hook (bool):
                        Enables the pytest_assertion_pass hook.Make sure to delete any previously generated pyc cache
                        files.
  junit_suite_name (string):
                        Test suite name for JUnit report
  junit_logging (string):
                        Write captured log messages to JUnit report: one of no|log|system-out|system-err|out-err|all
  junit_log_passing_tests (bool):
                        Capture log information for passing tests to JUnit report:
  junit_duration_report (string):
                        Duration time to report: one of total|call
  junit_family (string):
                        Emit XML for schema: one of legacy|xunit1|xunit2
  doctest_optionflags (args):
                        option flags for doctests
  doctest_encoding (string):
                        encoding used for doctest files
  cache_dir (string):   cache directory path.
  log_level (string):   default value for --log-level
  log_format (string):  default value for --log-format
  log_date_format (string):
                        default value for --log-date-format
  log_cli (bool):       enable log display during test run (also known as "live logging").
  log_cli_level (string):
                        default value for --log-cli-level
  log_cli_format (string):
                        default value for --log-cli-format
  log_cli_date_format (string):
                        default value for --log-cli-date-format
  log_file (string):    default value for --log-file
  log_file_level (string):
                        default value for --log-file-level
  log_file_format (string):
                        default value for --log-file-format
  log_file_date_format (string):
                        default value for --log-file-date-format
  log_auto_indent (string):
                        default value for --log-auto-indent
  faulthandler_timeout (string):
                        Dump the traceback of all threads if a test takes more than TIMEOUT seconds to finish.
  addopts (args):       extra command line options
  minversion (string):  minimally required pytest version
  required_plugins (args):
                        plugins that must be present for pytest to run

最后,附 pytest官方文档地址pytest中文官方文档地址

热门相关:我和超级大佬隐婚了   唐朝小官人   王妃不乖:独宠倾城妃   名门贵妻:暴君小心点   走私大明