02-pytest 测试用例管理

2.pytest 测试用例管理

2.1 测试用例命名管理

    为了更好的管理自动化用例,需要建立用例命名规范以便统一用例的命名。主要规范如下所示:

  • 用例命名不能使用关键字
  • 用例命名各单词之间使用下划线(_)分隔
  • 用例命名不用担心字符过长,但需要清晰

2.2 用例执行顺序

    用例执行顺序通常会遵循一定的基本原则,但执行顺序也可以通过插件改变。其基本原则如下所示:

  • 根据用例名称的字母逐一比较ASCII值,值越小优选执行。
  • 当含有多个测试模块时,根据基本原则执行
  • 在同一个测试模块中,优选执行测试函数,再执行测试类,如果含有多个测试类,则遵循基本原则,类中的测试方法遵循方法输入顺序

    如果想改变执行顺序,可以使用以下方式:

  • 使用插件
  • 修改用例位置

2.3 断言管理

2.3.1 断言定义

    测试的本质是通过输入,来验证输出结果是否与预期结果一致。而在测试框架中,则是通过断言来实现,即通过断言功能来比对输出与预期是否一致。

2.3.2 断言时机

    什么时候使用断言比较好呢?分别从开发和测试角度来介绍,对于开发而言,通常在以下时机进行断言

  • 防御性编程,即在不满足条件时就进行断言
  • 运行时对程序进行逻辑检测
  • 合约性检查(例如前置条件、后置条件等)
  • 常量检查

    对于测试而言,通常需要在比对需求和功能检查时使用断言,如下所示:

  • 验证计算结果是否与预期结果一致
  • 验证类型是否一致
  • 验证添加/删除功能是否添加/删除成功
  • 验证接口返回的状态码和数据是否正确
  • 验证页面跳转是否正确
  • 验证元素是否可被选择
  • 验证元素是否不可操作
  • 验证返回值是否与预期一致

2.3.3 断言分类

    pytest 使用 Python 的assert函数,支持Python中所有断言,包含返回值是否相等、表达式执行结果是否正确、各类比较表达式等。

  • 返回值断言
def f():
    return 3

def test_f_01():
    assert f() == 4
  • 表达式断言
def f():
    return 3

def test_f_02():
    assert f()//2 == 2
  • 比较型断言
def f():
    return 3

def test_f_03():
    assert  f() < 4
  • 数据类型断言
def test_01():
    a="123"
    b=123
    assert a == b

def test_02():
    assert [0,1,2]==[2,1,0]

def test_03():
    a="123"
    assert type(a) == str

2.3.4 触发指定异常断言

    在进行异常测试时,希望程序在某时某地抛出一段指定的异常,如果在某时某地确实抛出异常,则程序结果是符合预期,如果抛出的异常不是指定的异常或未抛出异常,则表示程序是存在问题的。针对这种场景,可以使用raise抛出一个指定的异常,再通过测试方法检查代码是否可抛出这个异常,如果抛出异常,则表示程序运行正确,如果未抛出或抛出的异常不正确,则表示程序是错误的。示例代码如下所示:

def foo():
    raise SystemExit(2)

# 运行通过
def test_foo_01():
    with pytest.raises(SystemExit):
        foo()

# 运行失败
def test_foo_02():
    with pytest.raises(ZeroDivisionError):
        foo()

def foo_02():
    raise  ValueError("Value Error")

def test_foo_02():
    with pytest.raises(ValueError):
        assert "Value Error" in foo_02()

2.3.5 添加断言提示

    有时候需要在断言时添加自定义说明,可以直接在断言后面添加相应的提示内容,代码如下所示:

def foo_03():
    return 28

def test_foo_03():
    expect = 29
    actual = foo_03()
    assert actual == expect,f"期望结果为{expect},实际结果{actual}"

2.4 用例运行管理

    测试用例的运行管理可以通过 pytest 命令行选项来完成各种运行情况处理。查看pytest可以使用命令pytest -h

2.4.1 常见运行方式

 pytest [options] [file_or_dir] [file_or_dir] [...]

    后面不添加任何参数和文件时,pytest会递归搜索收集当前目录及其子目录所有符合命名要求的用例并执行。

    在日常测试过程,常用的参数是m(m可以理解为marker),即通过给用例添加的标记有选择的运行指定用例,例如-m 'mark1 and not mark2'

    如果在Python代码中调用pytest,可以使用pytest.main()

2.4.2 pytest 退出状态码

    pytest 命令执行结束后,会返回以下几种状态码:

  • 0:所有搜索收集到的用例都运行通过
  • 1:用例执行失败
  • 2:用户中断用例执行
  • 3:用例执行过程中,发生内部错误
  • 4:命令使用错误
  • 5:未能搜索收集到用例

    以上错误代码可以通过查看源代码

from pytest import ExitCode

@final
class ExitCode(enum.IntEnum):
    """Encodes the valid exit codes by pytest.

    Currently users and plugins may supply other exit codes as well.

    .. versionadded:: 5.0
    """

    #: Tests passed.
    OK = 0
    #: Tests failed.
    TESTS_FAILED = 1
    #: pytest was interrupted.
    INTERRUPTED = 2
    #: An internal error got in the way.
    INTERNAL_ERROR = 3
    #: pytest was misused.
    USAGE_ERROR = 4
    #: pytest couldn't find tests.
    NO_TESTS_COLLECTED = 5

2.4.3 运行用例方式

    在pytest 运行用例大致可以分为以下几种方式

2.4.3.1 运行指定用例

    运行指定用例文件,命令如下所示:

pytest 文件名

2.4.3.2 运行指定目录中用例

    运行指定目录中所有用例,命令如下所示:

# 方式一
pytest ../文件夹名 
# 方式二
pytest 

2.4.3.3 运行包含特定关键字用例

    如果需要运行测试方法或函数名包含、不包含的名字,且能进行与或非的组合逻辑搜索,可以使用参数 -k,命令如下所示:

pytest -k "Case and not name"

注意Python的关键字不可以应用到-k选项中

2.4.3.4 运行指定测试方法/函数

    如果需要运行某一个指定的测试方法或函数,可以使用模块名加说明符构成,其中说明符为 ::,命令如下所示:

pytest -s test_setup_teardown_class.py::TestClass::testcase_01

如果对搜索到的用例不太清楚,可以使用参数 --collect-only

2.4.3.5 运行指定标记用例

    在编写用例时,可以给用例指定相应的标记,例如:smoke、p1、p2等标记,在运行用例,可以有选择性的运行带指定标记的用例,命令如下所示:

pytest -s -m "smoke and not p2"

2.4.3.6 指定用例运行失败的次数

    当达到最大上限时,退出执行;如未配置,则没有上限,命令如下所示:

pytest -x  # 遇到第一个失败时,退出执行
pytest -maxfail=2 # 最多遇到两次失败,则退出执行,如果-maxfail=1则与-x等效

2.4.4 跳过用例执行

    在实际工作中,部分测试用例执行可能依赖一些外部条件。例如一些用例仅能运行在特定版本、某些已知Bug生成阻塞等。针对这种场景,我们可以提前添加一些标记,则pytest可以相应地预处理这些用例,在这种场景下,常用的标记有以下几种:

  • skip: 只有当某些条件满足时才运行用例,否则跳过整个用例执行
  • xfail: 该用例属于已知会运行失败,即期望用例运行就是失败

2.4.4.1 @pytest.makr.skip 装饰器

    跳过执行某个用例最简单的方法是使用装饰器 @pytest.makr.skip 且还可以设置一个可选参数 reason 来表明跳过的原因。示例代码如下所示:

# @IDE:      PyCharm
# @Project:  PyCharmProjects
# @File:     test_skip.py
# @Time      2025-03-11 18:42
# @Author:   Surpass Lee
# @E-mail:   surpassmebyme@gmail.com

import pytest

def foo():
    return 3

@pytest.mark.skip()
def test_foo_01():
    assert 34 == foo()

@pytest.mark.skip(reason="test skip ecorator")
def test_foo_02():
    assert 3 == foo()

    运行结果如下所示:

collecting ... collected 2 items

test_skip.py::test_foo_01 SKIPPED (unconditional skip)                   [ 50%]
Skipped: unconditional skip

test_skip.py::test_foo_02 SKIPPED (test skip ecorator)                   [100%]
Skipped: test skip ecorator

2.4.4.2 @pytest.makr.skipif 装饰器

    当满足特定条件时,运行相应用例,否则则跳过用例,可以使用装饰器 @pytest.makr.skipif(condition,reason),当condition=True,执行跳过用例,使用场景如下所示:

1.在单个用例中使用@pytest.makr.skipif

import pytest

ENV="prod"

@pytest.mark.skipif(ENV=="prod",reason="must be on non-prod env")
def test_foo_02():
    assert 3 == foo()

2.在单个用例中使用多个@pytest.makr.skipif

import pytest
import pytest
import sys

ENV="prod"

def foo():
    return 3

@pytest.mark.skipif(ENV=="prod",reason="must be on prod env")
@pytest.mark.skipif(sys.platform.lower() == "win32",reason="must be run on non-windows platform")
def test_foo_02():
    assert 3 == foo()

在使用多个pytest.mark.skipif后,只要满足其中一个条件,则跳过执行该用例

3.在多个用例中共享@pytest.makr.skipif

# @IDE:      PyCharm
# @Project:  PyCharmProjects
# @File:     test_skip_01.py
# @Time      2025-03-11 18:42
# @Author:   Surpass Lee
# @E-mail:   surpassmebyme@gmail.com

import pytest
import sys

min_verson=pytest.mark.skipif(sys.version_info < (3,9),reason="min version is 3.9")
platform=pytest.mark.skipif(sys.platform.lower() == "win32",reason="must be run on non-windows platform")

@min_verson
def test_foo_02():
    assert 3 == foo()

# @IDE:      PyCharm
# @Project:  PyCharmProjects
# @File:     test_skip_02.py
# @Time      2025-03-11 19:07
# @Author:   Surpass Lee
# @E-mail:   surpassmebyme@gmail.com

from test_skip_01 import min_verson,platform

@min_verson
@platform
def test_foo_03():
    assert "Surpass" == "surpass"

2.4.4.3 pytest.skip 方法

    如果我们想在测试执行期间强制跳过后续步骤,则可以使用pytest.skip方法,同时也可以设置一个参数reason,表明跳过的原因。示例代码如下所示:

# @IDE:      PyCharm
# @Project:  PyCharmProjects
# @File:     test_skip_method.py
# @Time      2025-03-11 19:18
# @Author:   Surpass Lee
# @E-mail:   surpassmebyme@gmail.com

import sys
import pytest

def test_case_01():
    assert 1 == "1"

if (platform:=sys.platform.lower()) == "win32":
    print(f"current platform is {platform}")
    pytest.skip("如果系统平台是Windows平台,则跳过运行用例",allow_module_level=True)

def test_case_01():
    assert 1 == "1"

def test_case_02():
    assert  2 == 2

    代码运行结果如下所示:

collecting ... current platform is win32
collected 0 items / 1 skipped

2.4.4.4 pytest.importorskip 方法

    当引入某个模块或引入的版本不符合要求时,可以使用pytest.importorskip来跳过后续部分的执行。

1.模块引入失败时跳过

# @IDE:      PyCharm
# @Project:  PyCharmProjects
# @File:     test_skip_import_module.py
# @Time      2025-03-11 19:30
# @Author:   Surpass Lee
# @E-mail:   surpassmebyme@gmail.com

import pytest

login_module=pytest.importorskip("login")

@login_module
def test_skip_import_01():
    pass

def test_skip_import_02():
    print("call test_skip_import_02")
    assert  1 == 1

2.模块引入版本不符合要求时跳过

# @IDE:      PyCharm
# @Project:  PyCharmProjects
# @File:     test_skip_import_version.py
# @Time      2025-03-11 19:36
# @Author:   Surpass Lee
# @E-mail:   surpassmebyme@gmail.com

import pytest

skip_version=pytest.importorskip(modname="csv",minversion="2.0",reason="最低版本要求为2.0")

import csv

print(f"csv version is:{csv.__version__}")

@skip_version
def test_skip_import_version():
    assert 2 == 2

    运行结果为:

collecting ... csv version is:1.0

Skipped: module 'csv' has __version__ '1.0', required is: '2.0'
collected 0 items / 1 skipped

2.4.4.5 跳过测试类

    跳过的层次也是有对应关系的:跳过函数、跳过方法、跳过模块、跳过类等。那么如何使用@pytest.mark.skip和@pytest.makr.skipif呢?

# @IDE:      PyCharm
# @Project:  PyCharmProjects
# @File:     test_skip_class.py
# @Time      2025-03-11 20:00
# @Author:   Surpass Lee
# @E-mail:   surpassmebyme@gmail.com
import pytest

@pytest.mark.skip("作用于测试类,跳过类中所有测试用例")
class TestMyClass():

    def test_01(self):
        assert 1 == 1

    def test_02(self):
        assert 2 == 2

2.4.4.6 跳过测试模块

# @IDE:      PyCharm
# @Project:  PyCharmProjects
# @File:     test_skip_module.py
# @Time      2025-03-11 20:03
# @Author:   Surpass Lee
# @E-mail:   surpassmebyme@gmail.com
import  pytest

skip_module=pytest.skip("作用于模块,跳过模块中所有用例",allow_module_level=True)


def test_01(self):
    assert 1 == 1


def test_02(self):
    assert 2 == 2

2.4.4.7 跳过指定文件或目录

    要实现跳过指定文件或目录,可以使用pytest的配置文件conftest.py,并在其中配置collect_ignore_glob,示例如下所示:

collect_ignore_glob=["test_module/test_*.py","test_login*.py"]

以上配置代表跳过目录test_module所有以test开头的py文件和所有以test_login开头的py文件

2.4.5 标记预期为失败用例

    在测试过程中会遇到执行某个用例运行失败,但开发在最近修复的版本中未能修复,需要在后续版本才能修复,这时我们是知道这个用例当下运行肯定是失败的,针对这种情况,我们可以使用 @pytest.mark.xfail,表示期望该用例运行失败,用例正常运行,但不会显示堆栈信息,此时用例运行后的结果为用例执行失败(xfail:符合预期的失败)用例执行成功(xpass:不符合预期的成功,即执行成功反倒是有问题的)

2.4.5.1 标记期望失败用例 @pytest.mark.xfail

    示例代码如下所示:

# @IDE:      PyCharm
# @Project:  PyCharmProjects
# @File:     test_xfail_case_01.py
# @Time      2025-03-11 21:46
# @Author:   Surpass Lee
# @E-mail:   surpassmebyme@gmail.com
import sys
import  pytest

@pytest.mark.xfail(reason="测试xfail")
def test_case_01():
    assert 1 == 2

def test_case_02():
    assert "surpass".__contains__("s")

@pytest.mark.xfail(reason="测试xpass")
def test_case_03():
    assert 1 == 1

@pytest.mark.xfail(sys.platform.lower() == "win32",reason="windows 运行失败")
def test_case_04():
    assert 1 == 2

    运行结果如下所示:

collecting ... collected 4 items

test_xfail_case_01.py::test_case_01 XFAIL (测试xfail)                    [ 25%]
@pytest.mark.xfail(reason="测试xfail")
    def test_case_01():
>       assert 1 == 2
E       assert 1 == 2

test_xfail_case_01.py:12: AssertionError

test_xfail_case_01.py::test_case_02 PASSED                               [ 50%]
test_xfail_case_01.py::test_case_03 XPASS (测试xpass)                    [ 75%]
test_xfail_case_01.py::test_case_04 XFAIL (windows 运行失败)             [100%]
@pytest.mark.xfail(sys.platform.lower() == "win32",reason="windows 运行失败")
    def test_case_04():
>       assert 1 == 2
E       assert 1 == 2

test_xfail_case_01.py:23: AssertionError


=================== 1 passed, 2 xfailed, 1 xpassed in 0.04s ===================

2.4.5.12 标记期望失败用例 pytest.xfail

    除以上方法,也可以使用 pytest.xfail 方法在用例执行过程中直接将用例结果标记为xfail,并跳过剩余部分,即该用例中后续代码不再执行。常用应用场景是功能未完成已知问题

# @IDE:      PyCharm
# @Project:  PyCharmProjects
# @File:     test_xfail_case_02.py
# @Time      2025-03-11 22:02
# @Author:   Surpass Lee
# @E-mail:   surpassmebyme@gmail.com

from typing import Dict,Any
import pytest

def valid_config(config:Dict[str,Any]={}):
    """
    验证配置信息是否正确
    :return:
    """

    return True if config else False

class TestXFail():

    def test_case_01(self):
        print("01-开始执行第一个用例")
        pytest.xfail(reason="该用例暂未实现")
        print("01-完成执行第一个用例")
        assert 1 == 2

    def test_case_02(self):
        print("01-开始执行第二个用例")
        assert "s" in "surpass"

    def test_case_03(self):
        print("01-开始执行第三个用例")
        assert 1000 > 10

    def test_case_04(self):
        if not valid_config():
            pytest.xfail(reason="配置校验未通过")

if __name__ == '__main__':
    pytest.main(["-sr","test_xfail_case_02.py"])

    运行结果如下所示:

collecting ... collected 4 items

test_xfail_case_02.py::TestXFail::test_case_01 XFAIL (该用例暂未实现)    [ 25%]01-开始执行第一个用例

self = <test_xfail_case_02.TestXFail object at 0x00000299BF3794C0>

    def test_case_01(self):
        print("01-开始执行第一个用例")
>       pytest.xfail(reason="该用例暂未实现")
E       _pytest.outcomes.XFailed: 该用例暂未实现

test_xfail_case_02.py:23: XFailed

test_xfail_case_02.py::TestXFail::test_case_02 PASSED                    [ 50%]01-开始执行第二个用例

test_xfail_case_02.py::TestXFail::test_case_03 PASSED                    [ 75%]01-开始执行第三个用例

test_xfail_case_02.py::TestXFail::test_case_04 XFAIL (配置校验未通过)    [100%]
self = <test_xfail_case_02.TestXFail object at 0x00000299C0E5EF30>

    def test_case_04(self):
        if not valid_config():
>           pytest.xfail(reason="配置校验未通过")
E           _pytest.outcomes.XFailed: 配置校验未通过

test_xfail_case_02.py:37: XFailed


======================== 2 passed, 2 xfailed in 0.05s =========================
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容