命令行参数

  • -v: 输出详细信息
  • -s: 输出调试信息(如 print)
  • -x: 遇到失败的测试用例立即停止测试
  • -m: 执行特定的标记(mark) pytest -m "mark1 or mark2
  • -k: 执行包含特定字符串的测试用例
  • –maxfail=num: 当失败测试用例数量达到num时,停止测试
  • –ff (–failed-first): 之前运行失败的测试用例会首先被运行,然后才运行其他的测试用例
  • –lf (–last-failed): 仅运行上次运行失败的测试用例
  • –showlocals(简写-l): 调试失败的测试用例。
  • –tb=native:这个参数用于在显示测试失败时使用本机(native)的回溯(traceback)格式。
  • –assert=plain:这个参数用于设置断言失败时的输出格式为简单模式。简单模式可能对某些情况下的补丁操作更友好。
  • –capture=no:这个参数用于禁用输出捕获。在某些情况下,输出捕获可能受到补丁操作的影响,禁用捕获可以帮助诊断问题。

fixtures

What is fixtures

  • 在测试中,fixtures 为测试提供了定义的、可靠的和一致的上下文。这可能包括环境(例如配置了已知参数的数据库)或内容(例如数据集)。

  • 当我们运行一个测试时,pytest 会在函数参数中寻找同名的 fixture,一旦找到了某一个fixture,就会去获取相应的 fixture 的返回值,将这个返回值传递给测试函数

  • 使用方法,在函数前加注解(装饰器) @pytest.fixtures

  • example

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    import pytest


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

    def __eq__(self, other):
    return self.name == other.name


    @pytest.fixture
    def my_fruit():
    return Fruit("apple")


    @pytest.fixture
    def fruit_basket(my_fruit):
    return [Fruit("banana"), my_fruit]


    def test_my_fruit_in_basket(my_fruit, fruit_basket):
    assert my_fruit in fruit_basket
  • 测试文件中可以多个函数都有 fixture 注解,一个带有 fixture 注解的函数也可以去调用另外一个 fixture 函数

Improvements over xUnit-style setup/teardown functions

  • 显式命名与声明的使用:

    • pytest 的 fixture 具有明确的名称,并且通过从测试函数、模块、类或整个项目中声明其使用来激活。

    • 这意味着可以清楚地知道每个 fixture 的作用,并且只在需要时调用它们。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    # Fixture with explicit name 'database'
    @pytest.fixture
    def database():
    return Database()

    # Using the 'database' fixture in a test function
    def test_database_operations(database):
    assert database.query() == expected_result

  • 模块化实现:

    • 每个 fixture 的名称触发一个 fixture 函数,这种模块化的实现允许一个 fixture 函数使用其他 fixtures。

    • 这种设计使得你可以构建和组织自己的 fixtures,将它们组合在一起以创建更复杂的测试环境。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    @pytest.fixture
    def user():
    return User()

    @pytest.fixture
    def authenticated_user(user):
    # Using the 'user' fixture inside the 'authenticated_user' fixture
    return authenticate_user(user)

  • 规模化的管理:

    • 从简单的单元测试到复杂的功能测试,pytest 的 fixture 管理可以进行扩展。

    • 可以根据配置和组件选项对 fixtures 和测试进行参数化,或者在不同的作用域(函数、类、模块、整个测试会话)中重复使用 fixtures。

    1
    2
    3
    4
    5
    6
    7
    @pytest.fixture(params=[True, False])
    def feature_enabled(request):
    return request.param

    def test_feature(feature_enabled):
    assert feature_enabled

  • 简化拆卸逻辑:

    • 可以轻松、安全地管理拆卸逻辑,而不管使用了多少个 fixtures,而不需要手动处理错误或精心安排清理步骤的顺序。

    • 能够更容易地处理测试后的清理工作,无论测试使用了多少 fixtures。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    @pytest.fixture
    def setup_and_teardown():
    # Setup code
    yield
    # Teardown code

    def test_with_teardown(setup_and_teardown):
    # Test code

  • 支持 xUnit 风格的设置:

    • pytest 仍然支持 xUnit 风格的设置。你可以混合使用两种风格,逐步从经典的方式过渡到新的 fixture 风格,也可以从现有的 unittest.TestCase 风格或基于 nose 的项目中开始。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    import unittest

    class MyTestCase(unittest.TestCase):
    def setUp(self):
    # Classic setup code

    def test_something(self):
    # Test code

    def tearDown(self):
    # Classic teardown code

Fixtures error

  • 对于测试用例中的 fixtures 最好以线性排序,以确保能够清晰地了解哪个 fixture 先执行,哪个后执行。

  • 然而,如果较早的 fixture 存在问题并引发异常,pytest 会停止执行该测试,并标记为具有错误。

  • 当一个测试被标记为有错误时,并不意味着测试失败,而是表示测试甚至无法尝试,因为它所依赖的某个组件出现了问题。

  • 所以,在编写测试用例时尽量减少不必要的依赖关系的重要性。通过减少依赖,可以确保不相关的问题不会导致我们无法得知测试用例的实际问题,从而帮助更准确地定位和解决测试中的异常情况。

  • example

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    import pytest

    @pytest.fixture
    def order():
    return []


    @pytest.fixture
    def append_first(order):
    order.append(1)


    @pytest.fixture
    def append_second(order, append_first):
    order.extend([2])


    @pytest.fixture(autouse=True)
    def append_third(order, append_second):
    order += [3]


    def test_order(order):
    assert order == [1, 2, 3]
  • 假如,由于某种原因,在执行 order.append(1) 的时候出错了,那么我们就无法知道 order.append([2])order += [3] 是否也出错了,当 append_first 抛出异常之后,pytest将不再为 test_order 执行任何的 fixtures,同样也不会执行 test_order,唯一执行的只有 orderappend_first

Built-in fixtures

  • pytest 提供了一些内置的 fixtrue 函数:

tmpdir 和 tempdir_factory

  • 内置的 tmpdirtmpdir_factory 负责在测试开始运行前创建临时文件目录,并在测试结束后删除。其主要特性如下所示:

    • 如果测试代码要对文件进行读写操作,可以使用 tmpdirtmpdir_factory 来创建文件或目录,单个测试使用 tmpdir ,多个测试使用 tmpdir_factory

    • tmpdir 的作用范围为函数(function)级别, tmpdir_factory 作用范围是会话(session)级别

  • example

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    import pytest

    def test_tmpDir(tmpdir):
    tmpfileA = tmpdir.join("testA.txt")
    tmpSubDir = tmpdir.mkdir("subDir")
    tmpfileB = tmpSubDir.join("testB.txt")
    tmpfileA.write("this is pytest tmp file A")
    tmpfileB.write("this is pytest tmp file B")

    assert tmpfileA.read() == "this is pytest tmp file A"
    assert tmpfileB.read() == "this is pytest tmp file B"
  • tmpdir 的作用范围是函数级别,所以只能针对测试函数使用 tmpdir 创建文件或目录。如果 fixture 作用范围高于函数级别(类、模块、会话),则需要使用 tmpdir_factory 。tmpdir 与 tmpdir_factory 类似,但提供的方法有一些不同,如下所示:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    import pytest

    def test_tmpDir(tmpdir_factory):
    baseTmpDir = tmpdir_factory.getbasetemp()
    print(f"\nbase temp dir is :{baseTmpDir}")
    tmpDir_factory = tmpdir_factory.mktemp("tempDir")
    tmpfileA = tmpDir_factory.join("testA.txt")
    tmpSubDir = tmpDir_factory.mkdir("subDir")
    tmpfileB = tmpSubDir.join("testB.txt")
    tmpfileA.write("this is pytest tmp file A")
    tmpfileB.write("this is pytest tmp file B")

    assert tmpfileA.read() == "this is pytest tmp file A"
    assert tmpfileB.read() == "this is pytest tmp file B"
  • getbasetemp() 用于返回该会话使用的根目录, pytest-NUM 会随着会话的增加而进行自增,pytest 会记录最近几次(官方文档给的默认值为3次)会话使用的根目录,更早的根目录记录则会被清理掉。可通过配置 tmp_path_retention_counttmp_path_retention_policy 来更改:

    1
    2
    3
    4
    5
    6
    [pytest]
    tmp_path_retention_count = 3
    tmp_path_retention_policy = "all"
    ; all: retains directories for all tests, regardless of the outcome.
    ; failed: retains directories only for tests with outcome error or failed.
    ; none: directories are always removed after each test ends, regardless of the outcome.
  • 另外也可在命令行指定临时目录,如下所示:

    1
    pytest --basetemp=dir
  • 如何在其他作用范围内使用临时目录

    • tmpdir_factory 的作用范围是会话级别的, tmpdir 的作用范围是函数级别的。如果需要模块级别或类级别的作用范围的目录,该如何解决?针对这种情况,可以利用 tmpdir_factory 再创建一个 fixture

    • example

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      # content of conftest.py
      import json
      import pytest

      @pytest.fixture(scope="module")
      def readJson(tmpdir_factory):
      jsonData={
      "name":"zhangsan",
      "age":28,
      "locate":"shangahi",
      "loveCity":{"shanghai":"shanghai",
      "wuhai":"hubei",
      "shenzheng":"guangdong"
      }
      }

      file=tmpdir_factory.mktemp("jsonTemp").join("tempJSON.json")

      with open(file,"w",encoding="utf8") as fo:
      json.dump(jsonData,fo,ensure_ascii=False)

      # print(f"base dir is {tmpdir_factory.getbasetemp()}")
      return file

      # content of test_module_temdir.py
      import json

      def test_getData(readJson):
      with open(readJson,"r",encoding="utf8") as fo:
      data=json.load(fo)
      assert data.get("name")=="zhangsan"

      def test_getLoveCity(readJson):
      with open(readJson,"r",encoding="utf8") as fo:
      data=json.load(fo)
      getCity=data.get("loveCity")
      for k,v in getCity.items():
      assert len(v)>0

tmp_path 和 tmp_path_factory

  • tmp_path 是一个 pathlib.Path 对象.

  • 提供了一个临时目录的路径,用于在测试过程中创建和操作临时文件和目录。这个 fixture 在测试中经常用于执行文件操作而不影响实际文件系统。

  • 使用方法:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    # content of test_tmp_path.py
    import pathlib
    CONTENT = "content"

    def test_create_file(tmp_path: pathlib.WindowsPath):
    d = tmp_path / "sub"
    d.mkdir()
    p = d / "hello.txt"
    p.write_text(CONTENT, encoding="utf-8")
    assert p.read_text(encoding="utf-8") == CONTENT
    assert len(list(tmp_path.iterdir())) == 1
    assert 0
  • tmp_pathtmpdir 的区别:

    • tmp_path 是一个 pathlib.Path 对象,表示一个临时目录的路径。通常用于执行文件和目录操作。(python standard object)

    • tmpdir 是一个 py.path.local 对象,也表示一个临时目录的路径。它提供了一些额外的方法用于文件操作,与 pathlib.Path 有一些不同之处。 (pytest custom designed object)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    import py

    def test_tmp_path(tmp_path: pathlib.WindowsPath, tmpdir: py.path.LocalPath):
    # tmp_path 是 pathlib.Path 对象,tmpdir 是 py.path.local 对象
    print(f"\n{type(tmp_path)}") # <class 'pathlib.WindowsPath'>
    print(f"{type(tmpdir)}") # <class '_pytest._py.path.LocalPath'>
    # 使用 tmp_path 进行文件操作
    tmp_file_path = tmp_path / "example.txt"
    tmp_file_path.write_text("Hello, World!", encoding="utf-8")
    assert tmp_file_path.read_text(encoding="utf-8") == "Hello, World!"

    # 使用 tmpdir 进行文件操作
    tmp_file_dir = tmpdir / "example.txt"
    tmp_file_dir.write_text("Hello, Pytest!", encoding="utf-8")
    assert tmp_file_dir.read_text(encoding="utf-8") == "Hello, Pytest!"
  • tmp_path_factorytmpdir_factory

    • tmp_path_factory 是一个工厂函数,用于创建 tmp_path 。每次调用 tmp_path_factory 时,都会返回一个新的 pathlib.Path 对象,表示一个新的临时目录。

    • tmpdir_factory 是一个工厂函数,用于创建 tmpdir 。每次调用 tmpdir_factory 时,都会返回一个新的 py.path.local 对象,表示一个新的临时目录。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    from pytest import TempPathFactory, TempdirFactory

    def test_tmp_path_factory(tmp_path_factory: TempPathFactory, tmpdir_factory: TempdirFactory):
    # tmp_path_factory 和 tmpdir_factory 是工厂函数

    print(f"\n{type(tmp_path_factory)}") # <class '_pytest.tmpdir.TempPathFactory'>
    print(type(tmpdir_factory)) # <class '_pytest.legacypath.TempdirFactory'>

    # 创建新的临时目录,返回 pathlib.Path 对象
    tmp_path = tmp_path_factory.mktemp("test_dir")
    assert isinstance(tmp_path, pathlib.Path)

    # 创建新的临时目录,返回 py.path.local 对象
    tmpdir = tmpdir_factory.mktemp("test_dir")
    assert isinstance(tmpdir, py.path.local)

pytestconfig

  • 内置的 pytestconfig 可以通过命令行参数、选项、配置文件、插件、运行目录等方式来控制 pytest。pytestconfig 是 request.config 的快捷方式,在 pytest 中称之为 pytest 配置对象

  • 该函数可以配合 pytest_addoption 钩子函数一起使用

  • example

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    # content of conftest.py
    def pytest_addoption(parser):
    parser.addoption("--myopt",action="store_true",help="test boolean option")
    parser.addoption("--foo",action="store",default="zhangsan",help="test stroe")

    # content of test_pyteestconfig.py
    import pytest

    def test_myOption(pytestconfig):
    print(f"--foo {pytestconfig.getoption('foo')}")
    print(f"--myopt {pytestconfig.getoption('myopt')}")

    # 执行:pytest -vs --myopt --foo zhangsan test_pytestconfig.py
  • 因为 pytestconfig 是一个 fixture 函数,所以也可以被其他 fixture 函数调用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    import pytest

    @pytest.fixture()
    def foo(pytestconfig):
    return pytestconfig.option.foo

    @pytest.fixture()
    def myopt(pytestconfig):
    return pytestconfig.option.myopt

    def test_fixtureForAddOption(foo,myopt):
    print(f"\nfoo -- {foo}")
    print(f"\nmyopt -- {myopt}")
  • 除了使用 pytestconfig 自定义之外,也可以使用内置的选项和 pytest 启动时的信息,如目录、参数等。

    1
    2
    3
    4
    5
    6
    7
    8
    def test_pytestconfig(pytestconfig):
    print(f"args : {pytestconfig.args}")
    print(f"ini file is : {pytestconfig.inifile}")
    print(f"root dir is : {pytestconfig.rootdir}")
    print(f"invocation dir is :{pytestconfig.invocation_dir}")
    print(f"-q, --quiet {pytestconfig.getoption('--quiet')}")
    print(f"-l, --showlocals:{pytestconfig.getoption('showlocals')}")
    print(f"--tb=style: {pytestconfig.getoption('tbstyle')}")

cache

  • 通常情况下,每个测试用例彼此都是独立的,互不影响。但有时,一个测试用例运行完成后,希望将其结果传递给下一个测试用例,这种情况下,则需要使用pytest内置的cache。

  • 为记住上次测试失败的用例,pytest 存储了上一个测试会话中测试失败的信息,可以使用 --cache-show 标识来显示存储的信息。

  • 如果需要清空cache,可以在测试会话之前,传入–clear-cache标识即可

  • cache除了–lf和–ff两个标识之外,还可以使用其接口,如下所示:

    1
    2
    cache.get(key,default)
    cache.set(key,value)
  • 参考代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    import datetime
    import time
    import random
    import pytest

    # 创建一个fixture,记录测试的耗时,并存储到cache中
    # 如果后面的测试耗时大于之前的2倍,就抛出超时异常。

    # pytest -vs --cache-clear test_cache.py

    @pytest.fixture(autouse=True)
    def checkDuration(request,cache):
    key = "duration/"+request.node.nodeid.replace(":","_")
    startTime = datetime.datetime.now()
    yield
    endTime = datetime.datetime.now()
    duration = (endTime-startTime).total_seconds()
    lastDuration = cache.get(key,None)
    cache.set(key,duration)
    if lastDuration is not None:
    errorString = "test duration over twice last duration"
    assert duration <= 2 * lastDuration,errorString

    @pytest.mark.parametrize("t",range(5))
    def test_duration(t):
    time.sleep(random.randint(0,5))

capsys

  • pytest内置的capsys主要有两个功能:

    • 允许使用代码读取stdout和stderr

    • 可以临时禁止抓取日志输出

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    import sys

    def greeting(name):
    print(f"Hello,{name}")


    def test_greeting(capsys):
    greeting("Surpass")
    out,err = capsys.readouterr()
    assert "Hello,Surpass" in out
    assert err == ""


    def greeting_err(name):
    print(f"Hello,{name}",file=sys.stderr)


    def test_greeting(capsys):
    greeting_err("Surpass")
    out,err = capsys.readouterr()
    assert "Hello,Surpass" in err
    assert out == ""
  • pytest 通常会抓取测试用例及被测试代码的输出。而且是在全部测试会话结束后,抓取到的输出才会随着失败的测试显示出来。 --s 参数可以关闭该功能,在测试仍在运行时就把输出直接发送到 stdout ,但有时仅需要其中的部分信息,则可以使用 capsys.disable() ,可以临时让输出绕过默认的输出捕获机制,示例如下所示:

    1
    2
    3
    4
    def test_capsysDisable(capsys):
    with capsys.disabled():
    print("\nalways print this information")
    print("normal print,usually captured")

monkeypatch

  • monkey patch 可以在运行期间对类或模块进行动态修改。在测试中,monkey patch 常用于替换被测试代码的部分运行环境或装饰输入依赖或输出依赖替换成更容易测试的对象或函数。在 pytest 内置的 monkey patch 允许单一环境中使用,并在测试结束后,无论结果是失败或通过,所有修改都会复原。monkeypatch 常用的函数如下所示:

    1
    2
    3
    4
    5
    6
    7
    8
    setattr(target, name, value=<notset>, raising=True):   # 设置属性
    delattr(target, name=<notset>, raising=True): # 删除属性
    setitem(dic, name, value): # 设置字典中一个元素
    delitem(dic, name, raising=True): # 删除字典中一个元素
    setenv(name, value, prepend=None): # 设置环境变量
    delenv(name, raising=True): # 删除环境变量
    syspath_prepend(path) # 将path路径添加到sys.path中
    chdir(path) # 改变当前的工作路径
    • raising 参数用于指示 pytest 在记录不存在时,是否抛出异常
    • setenv() 中的 prepend 可以是一个字符,如果是这样设置,则环境变量的值就是 value+ prepend +
  • 不建议对内置函数(如 open、compile 等)进行补丁(patch)操作,因为这可能会破坏 Pytest 的内部实现。如果确实无法避免这样的操作,可以尝试使用一些参数(–tb=native、–assert=plain 和 –capture=no)来减轻可能出现的问题,尽管并不保证能够完全解决。

  • 在进行补丁操作时,需要注意对标准库函数和 pytest 使用的一些第三方库进行补丁可能会破坏 pytest 本身。因此,在这些情况下,建议使用 MonkeyPatch.context() 来将补丁限制在你想要测试的代码块中。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    import functools

    def test_partial(monkeypatch):
    # 使用 MonkeyPatch.context() 来限制补丁的作用范围
    with monkeypatch.context() as m:
    # 在这个代码块中对 functools.partial 进行补丁
    m.setattr(functools, "partial", 3)

    # 在代码块内,functools.partial 被补丁为 3
    assert functools.partial == 3

    # 在代码块外,functools.partial 恢复为原来的值
    assert functools.partial != 3
  • 看如下例子:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    import os
    import json

    defaulData={
    "name":"Surpass",
    "age":28,
    "locate":"shangahi",
    "loveCity":{"shanghai":"shanghai",
    "wuhai":"hubei",
    "shenzheng":"guangdong"
    }
    }

    def readJSON():
    path=os.path.join(os.getcwd(),"surpass.json")
    with open(path,"r",encoding="utf8") as fo:
    data=json.load(fo)
    return data

    def writeJSON(data:str):
    path = os.path.join(os.getcwd(), "surpass.json")
    with open(path,"w",encoding="utf8") as fo:
    json.dump(data,fo,ensure_ascii=False,indent=4)

    def writeDefaultJSON():
    writeJSON(defaulData)
  • writeDefaultJSON() 既没有参数也没有返回值,该如何测试?仔细观察函数,它会在当前目录中保存一个JSON文件,那就可以从侧面来进行测试。通常比较直接的方法,运行代码并检查文件的生成情况。如下所示:

    1
    2
    3
    4
    5
    def test_writeDefaultJSON():
    writeDefaultJSON()
    expectd = defaulData
    actual = readJSON()
    assert expectd == actual
  • 上面这种方法虽然可以进行测试,但却覆盖了原有文件内容。函数里面所传递的路径为当前目录,那如果将目录换成临时目录了,示例如下所示:

    1
    2
    3
    4
    5
    6
    7
    def test_writeDefaultJSONChangeDir(tmpdir, monkeypatch):
    tmpDir = tmpdir.mkdir("TestDir")
    monkeypatch.chdir(tmpDir)
    writeDefaultJSON()
    expectd = defaulData
    actual = readJSON()
    assert expectd == actual
  • 以上这种虽然解决了目录的问题,那如果测试过程,需要修改数据,又该如何,示例如下所示:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    def test_writeDefaultJSONChangeDir(tmpdir,monkeypatch):
    tmpDir = tmpdir.mkdir("TestDir")
    monkeypatch.chdir(tmpDir)
    # 保存默认数据
    writeDefaultJSON()
    copyData = deepcopy(defaulData)
    # 增加项
    monkeypatch.setitem(defaulData, "hometown", "hubei")
    monkeypatch.setitem(defaulData, "company", ["Surpassme","Surmount"])
    addItemData = defaulData
    # 再次保存数据
    writeDefaultJSON()
    # 获取保存的数据
    actual = readJSON()
    assert addItemData == actual
    assert copyData != actual
  • 更多例子参考:

    • Monkeypatching functions

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      # contents of test_module.py with source code and the test
      from pathlib import WindowsPath
      from pytest import MonkeyPatch


      def getssh():
      """Simple function to return expanded homedir ssh path."""
      return WindowsPath.home() / ".ssh" # C:/User/Admin/.ssh


      def test_getssh(monkeypatch: MonkeyPatch):
      print(type(monkeypatch)) # <class '_pytest.monkeypatch.MonkeyPatch'>
      # mocked return function to replace Path.home
      # always return '/abc'
      def mockreturn():
      return WindowsPath("/abc")

      # Application of the monkeypatch to replace Path.home
      # with the behavior of mockreturn defined above.
      monkeypatch.setattr(WindowsPath, "home", mockreturn)

      # Calling getssh() will use mockreturn in place of Path.home
      # for this test with the monkeypatch.
      x = getssh()
      assert x == WindowsPath("/abc/.ssh")

    • Monkeypatching returned objects: building mock classes

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      import requests
      import pytest
      from pytest import MonkeyPatch


      def get_json(url):
      """Takes a URL, and returns the JSON."""
      r = requests.get(url)
      return r.json()

      # custom class to be the mock return value
      # will override the requests.Response returned from requests.get
      class MockResponse:
      # mock json() method always returns a specific testing dictionary
      @staticmethod
      def json():
      return {"mock_key": "mock_response"}


      # monkeypatched requests.get moved to a fixture
      @pytest.fixture
      def mock_response(monkeypatch: MonkeyPatch):
      """Requests.get() mocked to return {'mock_key':'mock_response'}."""

      # Any arguments may be passed and mock_get() will always return our
      # mocked object, which only has the .json() method.
      def mock_get(*args, **kwargs):
      return MockResponse()

      # apply the monkeypatch for requests.get to mock_get
      # let mock_get() replace requests.get()
      monkeypatch.setattr(requests, "get", mock_get)


      def test_get_json(mock_response):
      # get_json, which contains requests.get, uses the monkeypatch
      result = get_json("https://fakeurl")
      assert result["mock_key"] == "mock_response"
    • Monkeypatching environment variables

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      import os
      import pytest

      from pytest import MonkeyPatch

      def get_os_user_lower():
      """Simple retrieval function.
      Returns lowercase USER or raises OSError."""
      username = os.getenv("USER")

      if username is None:
      raise OSError("USER environment is not set.")

      return username.lower()


      @pytest.fixture
      def mock_env_user(monkeypatch):
      monkeypatch.setenv("USER", "TestingUser")


      @pytest.fixture
      def mock_env_missing(monkeypatch):
      monkeypatch.delenv("USER", raising=False)


      # notice the tests reference the fixtures for mocks
      def test_upper_to_lower(mock_env_user):
      assert get_os_user_lower() == "testinguser"


      def test_raise_exception(mock_env_missing):
      with pytest.raises(OSError):
      _ = get_os_user_lower()

recwarn

  • 内置的 recwarn 可以用来检查待测代码产生的警告信息。在 Python 中,我们可以添加警告信息,很像断言,但不阻止程序运行。假如在一份代码,想要停止支持一个已经过时的函数,则可以在代码中设置警告信息,示例如下所示:
1
2
3
4
5
6
7
8
9
10
11
12
13
import warnings
import pytest

def depricateFunc():
warnings.warn("This function is not support after 3.8 version",DeprecationWarning)


def test_depricateFunc(recwarn):
depricateFunc()
assert len(recwarn) == 1
warnInfo=recwarn.pop()
assert warnInfo.category == DeprecationWarning
assert str(warnInfo.message) == "This function is not support after 3.8 version"
  • recwarn 的值就是一个警告信息列表,列表中的每个警告信息都有4个属性 categorymessagefilenamelineno 。警告信息 在测试开始后收集,如果待测的警告信息在最后,则可以在信息收集前使用 recwarn.clear() 清除不需要的内容。

  • 除 recwarn,还可以使用 pytest.warns() 来检查警告信息。示例如下所示:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    import warnings
    import pytest

    def depricateFunc():
    warnings.warn("This function is not support after 3.8 version",DeprecationWarning)

    def test_depricateFunc():
    with pytest.warns(None) as warnInfo:
    depricateFunc()
    assert len(warnInfo)==1
    w=warnInfo.pop()
    assert w.category==DeprecationWarning
    assert str(w.message) == "This function is not support after 3.8 version"

Sharing test data

  • 如果要在测试用例中导入文件中的数据,推荐将这些文件数据加载到 fixtrues 中,这种做法利用 pytest 的自动缓存机制

  • 另一个做法是将数据文件放在一个 tests 文件夹下,要用到一个插件 pytest-datadir

    1
    pip install pytest-datadir
    • 假设目录结构如下:
    1
    2
    3
    4
    5
    6

    project/
    |-- tests/
    | |-- test_hello.py
    |-- data/
    | |-- hello.txt
    • hello.txt 文件包含一些测试数据
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    import pytest

    # 使用 pytest-datadir 插件
    def test_read_hello_file(datadir):
    # datadir 是一个 fixture,它提供了测试数据目录的路径
    file_path = datadir / 'hello.txt'

    # 读取文件内容
    with open(file_path, 'r') as file:
    content = file.read()

    # 在这个例子中,你可以根据文件内容执行相应的断言
    assert "Hello, World!" in content

Fixture availability

  • 从测试的角度来看,一个 fixture 只有在其定义的作用域内才能被测试请求到。如果一个 fixture 在类内定义,那么只有在该类内的测试才能请求到它。但如果一个 fixture 在模块的全局作用域内定义,那么该模块内的任何测试,即使是在类内定义的测试,都可以请求到它。

  • 一个 fixture 可以请求任何其他 fixture,无论它们在哪里定义,只要请求它们的测试能够看到所有涉及的 fixture

  • example

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    import pytest


    @pytest.fixture
    def order():
    return []


    @pytest.fixture
    def outer(order, inner):
    order.append("outer")


    class TestOne:
    @pytest.fixture
    def inner(self, order):
    order.append("one")

    def test_order(self, order, outer):
    assert order == ["one", "outer"]


    class TestTwo:
    @pytest.fixture
    def inner(self, order):
    order.append("two")

    def test_order(self, order, outer):
    assert order == ["two", "outer"]

sharing fixtures across multiple files

  • conftest.py 文件可以用作在整个目录中提供 fixtures 的手段。在 conftest.py 中定义的 fixtures 可以被该目录中的任何测试使用,而无需显式导入它们(pytest 会自动发现它们)。

  • 可以有多个嵌套的目录或包包含自定义测试,每个目录都可以有自己的 conftest.py 文件,其中定义了特定目录下的 fixtures,这些 fixtures 会添加到父目录中的 conftest.py 文件中定义的 fixtures 中,从而构建出一个层级的 fixtures 结构。

  • 假定有如下目录结构:

    1
    2
    3
    4
    5
    6
    7
    8

    project/
    |-- tests/
    | |-- conftest.py # 这个文件定义了全局的 fixture
    | |-- test_module1.py
    | |-- subdirectory/
    | |-- conftest.py # 这个文件定义了子目录专有的 fixture
    | |-- test_module2.py
  • 各个目录下的文件内容如下:

    • tests/conftest.py

      1
      2
      3
      4
      5
      6
      7
      8
      9
      import pytest

      @pytest.fixture
      def order():
      return []

      @pytest.fixture
      def top(order, innermost):
      order.append("top")
    • tests/test_module1.py

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      import pytest

      @pytest.fixture
      def innermost(order):
      order.append("innermost top")

      def test_order(order, top):
      assert order == ["innermost top", "top"]


      if __name__ == "__main__":
      pytest.main(['-v', '-s'])
    • tests/subdirectory/conftest.py

      1
      2
      3
      4
      5
      import pytest

      @pytest.fixture
      def mid(order):
      order.append("mid subpackage")
    • tests/subdirectory/test_module2.py

      1
      2
      3
      4
      5
      6
      7
      8
      import pytest

      @pytest.fixture
      def innermost(order, mid):
      order.append("innermost subpackage")

      def test_order(order, top):
      assert order == ["mid subpackage", "innermost subpackage", "top"]
    • tests/test_module1.py 为例:执行 test_order() 测试时,首先调用 order (定义在 tests/conftest.py 中),返回值为 [] ,接着调用 top ,top 依次调用 orderinnermost ,但需要注意的是 innermost 具体是哪一个,因为在子目录的测试文件中也有一个 innermost ,这里的 top 调用的是 tests/test_module1.py 里的 innermost ,返回值为 [innermost top] , 再加上 top 的返回值,最终结果为 ["innermost top", "top"],可以通过测试

    • 还有一点需要注意:tests/subdirectory/test_module2.py 可以直接调用 ordertop 而无需显式导入 tests/conftest.py,当调用 top 时, innermost 则为定义在子目录下的 innermost

    • 但反过来,在 tests/test_module1.py 中调用 tests/subdirectory/conftest.py 中的 mid 就不可行了

Fixture instantiation order

  • 当执行测试时,确定 fixture 实例化的顺序主要考虑以下3个因素:

    • 作用域(scope): fixture 的作用域是影响实例化顺序的重要因素。不同作用域的 fixture 会在不同阶段实例化,例如,函数级别(function)、模块级别(module)、类级别(class)、以及整个会话级别(session)。

    • 依赖关系(dependencies): fixture 之间的依赖关系也会影响它们的实例化顺序。如果一个 fixture 依赖于另一个 fixture,那么被依赖的 fixture 会在依赖它的 fixture 之前被实例化。

    • 自动使用(autouse): 如果一个 fixture 被设置为自动使用,它会在其作用域内的所有测试之前被实例化。

Higher-scoped fixtures are executed first

  • 有更高级作用域的 fixture 将会被先执行,一般的顺序为 session --> package --> module --> class --> function

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    import pytest


    @pytest.fixture(scope="session")
    def order():
    return []


    @pytest.fixture
    def func(order):
    order.append("function")


    @pytest.fixture(scope="class")
    def cls(order):
    order.append("class")


    @pytest.fixture(scope="module")
    def mod(order):
    order.append("module")


    @pytest.fixture(scope="package")
    def pack(order):
    order.append("package")


    @pytest.fixture(scope="session")
    def sess(order):
    order.append("session")


    class TestClass:
    def test_order(self, func, cls, mod, pack, sess, order):
    assert order == ["session", "package", "module", "class", "function"]

Fixtures of the same order execute based on dependencies

  • 当一个 fixture 依赖另一个 fixture ,被依赖的 fixture 首先执行

  • 假定有如下依赖关系:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    @pytest.fixture
    def order():
    return []

    @pytest.fixture
    def a(order):
    order.append("a")

    @pytest.fixture
    def b(a, order):
    order.append("b")

    @pytest.fixture
    def c(b, order):
    order.append("c")

    @pytest.fixture
    def d(c, b, order):
    order.append("d")

    @pytest.fixture
    def e(d, b, order):
    order.append("e")

    @pytest.fixture
    def f(e, order):
    order.append("f")

    @pytest.fixture
    def g(f, c, order):
    order.append("g")

    def test_order(g, order):
    assert order == ["a", "b", "c", "d", "e", "f", "g"]
    • 具体来说:

      • g fixture 依赖于 f 和 c。
      • f fixture 依赖于 e。
      • e fixture 依赖于 d 和 b。
      • d fixture 依赖于 c 和 b。
      • c fixture 依赖于 b。
      • b fixture 依赖于 a。
      • a fixture 依赖于 order。
    • fixture 的执行顺序为:

      1. order fixture 执行,返回一个空列表。
      2. a fixture 执行,将字符串 “a” 添加到 order 列表。
      3. b fixture 执行,将字符串 “b” 添加到 order 列表。
      4. c fixture 执行,将字符串 “c” 添加到 order 列表。
      5. d fixture 执行,将字符串 “d” 添加到 order 列表。
      6. e fixture 执行,将字符串 “e” 添加到 order 列表。
      7. f fixture 执行,将字符串 “f” 添加到 order 列表。
      8. g fixture 执行,将字符串 “g” 添加到 order 列表。
    • 在上述代码中,每个 fixture 只被执行一次。这是因为默认情况下,pytest 的 fixture 的作用域是函数级别(function scope),每个测试函数调用时,相关的 fixture 会被执行一次。

    • 关于多次调用同一个 fixture 的补充说明:

    • 这一点可参考官方文档:Fixtures can also be requested more than once during the same test, and pytest won’t execute them again for that test. This means we can request fixtures in multiple fixtures that are dependent on them (and even again in the test itself) without those fixtures being executed more than once.

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      # contents of test_append.py
      import pytest


      # Arrange
      @pytest.fixture
      def first_entry():
      return "a"


      # Arrange
      @pytest.fixture
      def order():
      return []


      # Act
      @pytest.fixture
      def append_first(order, first_entry):
      return order.append(first_entry)


      def test_string_only(append_first, order, first_entry):
      # Assert
      # If a requested fixture was executed once for every time it was requested during a test
      # then this test would fail because both append_first and test_string_only would see order as an empty list (i.e. [])
      # but since the return value of order was cached (along with any side effects executing it may have had) after the first time it was called,
      # both the test and append_first were referencing the same object
      # and the test saw the effect append_first had on that object.
      assert order == [first_entry]

      if __name__ == "__main__":
      pytest.main(['-v', '-s'])

Autouse fixtures are executed first within their scope

  • 如果 fixture A 是自动使用的,而 fixture B 不是,但 fixture A 请求了 fixture B,那么在实际应用到 fixture A 的测试中,fixture B 也会被有效地当作自动使用的 fixture。这意味着 fixture B 在这些测试中会在其他非自动使用的 fixtures 之前执行。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    # 可以自己运行看看实际的 fixture 函数的 实例化顺序
    import pytest

    @pytest.fixture
    def order():
    print("order")
    return []

    @pytest.fixture
    def a(order):
    print("a")
    order.append("a")

    @pytest.fixture
    def b(a, order):
    print("b")
    order.append("b")

    @pytest.fixture(autouse=True)
    def c(b, order):
    print("c")
    order.append("c")

    @pytest.fixture
    def d(b, order):
    print("d")
    order.append("d")

    @pytest.fixture
    def e(d, order):
    print("e")
    order.append("e")

    @pytest.fixture
    def f(e, order):
    print("f")
    order.append("f")

    @pytest.fixture
    def g(f, c, order):
    print("g")
    order.append("g")

    def test_order_and_g(g, order):
    print("test_order_and_g")
    assert order == ["a", "b", "c", "d", "e", "f", "g"]


    if __name__ == "__main__":
    pytest.main(['-v', '-s'])
  • 在同一个作用域下,每一个 autouse 的 fixture 会在每个测试前自动执行,即使你并没有显式地请求这个 fixture。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    import pytest

    @pytest.fixture(scope="class")
    def order():
    print("order")
    return []

    @pytest.fixture(scope="class", autouse=True)
    def c1(order):
    print("c1 autouse")
    order.append("c1")

    @pytest.fixture(scope="class")
    def c2(order):
    print("c2")
    order.append("c2")

    @pytest.fixture(scope="class")
    def c3(order, c1):
    print("c3")
    order.append("c3")

    class TestClassWithC1Request:
    def test_order(self, order, c1, c3):
    print("test_order c1")
    assert order == ["c1", "c3"]

    class TestClassWithoutC1Request:
    def test_order(self, order, c2):
    print("test_order c2")
    assert order == ["c1", "c2"]


    if __name__ == "__main__":
    pytest.main(['-v', '-s'])
  • 即使一个自动使用的 fixture 请求了一个非自动使用的 fixture,这个非自动使用的 fixture 只会在请求它的那个自动使用 fixture 的上下文中被有效地当作自动使用 fixture。这并不会使得非自动使用的 fixture 在所有可能的情境中都变成自动使用的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    import pytest

    @pytest.fixture
    def order():
    print("order")
    return []

    @pytest.fixture
    def c1(order):
    print("c1")
    order.append("c1")

    @pytest.fixture
    def c2(order):
    print("c2")
    order.append("c2")

    class TestClassWithAutouse:
    @pytest.fixture(autouse=True)
    def c3(self, order, c2):
    print("c3")
    order.append("c3")

    def test_req(self, order, c1):
    print("test_req")
    assert order == ["c2", "c3", "c1"]

    def test_no_req(self, order):
    print("test_no_req")
    assert order == ["c2", "c3"]

    class TestClassWithoutAutouse:
    def test_req(self, order, c1):
    print("test_req")
    assert order == ["c1"]

    def test_no_req(self, order):
    print("test_no_req")
    assert order == []

    if __name__ == "__main__":
    pytest.main(['-v', '-s'])
  • yield 替代 return

    • 在使用 yield 定义 fixture 时,它不再使用 return 返回值,而是使用 yield

    • yield 的作用是在测试执行之前运行一些代码,并将一个对象传递回请求该 fixture 或测试的地方。

  • 拆分 yield 前后的代码

    • yield 语句之前的代码在测试执行之前运行,用于设置或准备测试环境

    • yield 语句之后的代码在测试执行完成后运行,用于清理或执行拆卸操作

  • Fixture 执行顺序

    • Pytest 会确定 fixtures 的线性顺序,然后依次执行每个 fixture 直到它返回或使用 yield

    • 一旦测试完成,Pytest 将按照相反的顺序返回到 fixtures 列表,对每个使用了 yield 的 fixture 运行在 yield 语句之后的代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    # content of test_emaillib.py
    from emaillib import Email, MailAdminClient

    import pytest

    @pytest.fixture
    def mail_admin():
    return MailAdminClient()

    @pytest.fixture
    def sending_user(mail_admin):
    user = mail_admin.create_user()
    yield user
    mail_admin.delete_user(user)

    @pytest.fixture
    def receiving_user(mail_admin):
    user = mail_admin.create_user()
    yield user
    user.clear_mailbox()
    mail_admin.delete_user(user)

    def test_email_received(sending_user, receiving_user):
    email = Email(subject="Hey!", body="How's it going?")
    sending_user.send_email(email, receiving_user)
    assert email in receiving_user.inbox
  • 除了使用 yield 之外,还可以使用 “finalizer” 函数

    • 相对于使用 yield 的 fixtures,另一种选择是直接将 “finalizer” 函数添加到测试的请求上下文对象中

    • 这种方法达到的结果与使用 yield 的 fixtures 类似,但相对来说需要更多的冗余代码

    • 为了使用这种方法,我们需要在需要添加清理代码的 fixture 中请求测试的请求上下文对象,就像请求其他 fixtures 一样。

    • 在获取到 request 上下文对象后,可以通过调用其 addfinalizer 方法,将一个包含清理代码的可调用对象传递给它。

    • 这个清理代码会在测试执行完成后运行,类似于 yield 的后续代码。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      # content of test_emaillib.py
      from emaillib import Email, MailAdminClient

      import pytest

      @pytest.fixture
      def mail_admin():
      return MailAdminClient()

      @pytest.fixture
      def sending_user(mail_admin):
      user = mail_admin.create_user()
      yield user
      mail_admin.delete_user(user)

      @pytest.fixture
      def receiving_user(mail_admin, request):
      user = mail_admin.create_user()

      def delete_user():
      mail_admin.delete_user(user)

      request.addfinalizer(delete_user)
      return user


      @pytest.fixture
      def email(sending_user, receiving_user, request):
      _email = Email(subject="Hey!", body="How's it going?")
      sending_user.send_email(_email, receiving_user)

      def empty_mailbox():
      receiving_user.clear_mailbox()

      request.addfinalizer(empty_mailbox)
      return _email

      def test_email_received(receiving_user, email):
      assert email in receiving_user.inbox

Mark

  • 使用 pytest.mark 辅助工具设置测试函数的元数据(metadata)

  • pytest.mark 辅助工具:

    • 使用 pytest.mark 辅助工具,可以在测试函数上设置元数据。元数据可以是标记、条件等,用于影响测试运行的行为。
  • 内置标记:

    • usefixtures:在测试函数或类上使用 fixtures。

    • filterwarnings:过滤特定的警告。

    • skip:始终跳过测试函数。

    • skipif:如果满足特定条件,则跳过测试函数。

    • xfail:如果满足特定条件,则产生“预期失败”的结果。

    • parametrize:对同一个测试函数使用不同的参数执行多次调用。

  • 获取所有标记的方法:

    • 可以使用 pytest –markers 命令行选项列出所有标记,包括内置标记和自定义标记。
  • 自定义标记和应用标记的范围:

    • 可以创建自定义标记,并将标记应用于整个测试类或模块。
    • 这些标记可以被插件使用,也常用于使用 -m 选项在命令行上选择特定的测试。
  • Note: mark 只能被用于测试,对 fixture 并没有影响

Registering marks

  • 可以在 pytest.ini 文件中设置 markers

    1
    2
    3
    4
    [pytest]
    markers =
    slow: marks tests as slow (deselect with '-m "not slow"')
    serial
  • 或者在 pyproject.toml 文件中设置 marker

    1
    2
    3
    4
    5
    [tool.pytest.ini_options]
    markers = [
    "slow: marks tests as slow (deselect with '-m \"not slow\"')",
    "serial",
    ]

Raising errors on unknown marks

  • 当使用 @pytest.mark.name_of_the_mark 去应用一个 pytest.ini 中未定义的 mark,pytest将会发出警告

  • 如果还在 pytest.ini 中声明 --strict-marker ,那么使用未定义的 mark 将会报告错误,而不是警告

    1
    2
    3
    4
    5
    [pytest]
    addopts = --strict-markers
    markers =
    slow: marks tests as slow (deselect with '-m "not slow"')
    serial

parametrize

  • 在 Pytest 中,当将参数值传递给测试函数时,它们是原样传递的,没有进行任何复制。这意味着如果将一个列表或字典作为参数值传递给测试函数,并且测试函数中对该列表或字典进行了修改,这些修改会在后续的测试用例调用中反映出来。

  • example

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    # content of test_example.py

    import copy
    import pytest

    # 参数化测试函数,接受一个列表作为参数
    @pytest.mark.parametrize("my_list", [[1, 2, 3]])
    def test_modify_list(my_list):
    # 在测试函数中对传递进来的列表进行修改
    my_list.append(4)
    assert len(my_list) == 4

    ###########################################################
    # 更推荐
    ###########################################################
    @pytest.mark.parametrize("my_list", [[1, 2, 3]])
    def test_modify_list_deep_copy(my_list):
    # 在测试函数中对传递进来的列表进行深复制
    my_list_copy = copy.deepcopy(my_list)
    my_list_copy.append(4)
    assert len(my_list_copy) == 4


    # 参数化测试函数,接受一个字典作为参数
    @pytest.mark.parametrize("my_dict", [{"key": "value"}])
    def test_modify_dict(my_dict):
    # 在测试函数中对传递进来的字典进行修改
    my_dict["new_key"] = "new_value"
    assert "new_key" in my_dict

pytest-generate_tests

  • 这是 pytest 内置的钩子函数,该钩子在收集测试函数时被调用。通过传递的 metafunc 对象,可以检查请求测试上下文,最重要的是,可以调用 metafunc.parametrize() 来实现参数化。

  • metafunc 是 Pytest 中的一个内置对象,它是 Metafunc 类的实例。Metafunc 类提供了一些方法,允许在测试收集阶段动态生成和配置测试函数。下面是一些 metafunc 常用的方法:

    • fixturenames: 返回测试函数中声明的所有 fixture 的名称的列表

    • function: 返回当前测试函数的 Function 对象

    • config: 返回 Pytest 配置的 Config 对象

    • parametrize: 允许在 pytest_generate_tests 钩子中调用,用于动态生成参数化的测试

pytest_addoption

  • pytest_addoption 是 Pytest 中的一个内置函数。它是一个钩子函数,用于在 Pytest 运行时处理命令行选项。用户可以在测试项目中的 conftest.py 文件中定义这个函数,用于添加自定义的命令行选项。

  • example:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    # content of conftest.py
    def pytest_addoption(parser):
    parser.addoption(
    "--stringinput",
    action="append",
    default=[],
    help="list of stringinputs to pass to test functions",
    )

    def pytest_generate_tests(metafunc):
    if "stringinput" in metafunc.fixturenames:
    metafunc.parametrize("stringinput", metafunc.config.getoption("stringinput"))
    1
    2
    3
    4
    5
    # content of test_strings.py
    def test_valid_string(stringinput):
    assert stringinput.isalpha()

    # 执行:pytest --stringinput="hello" --stringinput="world" test_hook.py
    • 这里对 addoption 函数的 action 参数进行补充说明:

      • store :将选项的值存储在一个单独的变量中。如果同一个选项多次出现,后面的值会覆盖前面的值。

        1
        parser.addoption("--myoption", action="store", default="default_value", help="My custom option")
      • store_const :将选项的值存储为一个常量。通常与 const 参数一起使用。

        1
        parser.addoption("--myoption", action="store_const", const="constant_value", help="My custom option")
      • store_truestore_false :用于处理布尔选项,分别表示 True 和 False。

        1
        2
        parser.addoption("--enable-feature", action="store_true", help="Enable a feature")
        parser.addoption("--disable-feature", action="store_false", help="Disable a feature")
      • append :如果同一个选项在命令行中出现多次,将其值添加到一个列表中。

        1
        parser.addoption("--stringinput", action="append", default=[], help="List of stringinputs")
      • count :记录选项在命令行中出现的次数,用于计数。

        1
        parser.addoption("--verbose", action="count", default=0, help="Increase verbosity level")
      • callback :允许指定一个回调函数来处理选项的值。

        1
        2
        3
        4
        def callback_function(option, opt_str, value, parser):
        # Custom logic to handle the option's value
        pass
        parser.addoption("--custom-option", action="callback", callback=callback_function, help="Custom option")

参考