《pytest测试实战》-- Brian Okken

1|0一、pytest 入门

这是一个测试用例

ch1/test_one.py def test_passing(): assert (1, 2, 3) == (1, 2, 3)

执行

cd /ch1 pytest test_one.py

结果

(venv) C:\Users\admin\Desktop\ch1>pytest test_one.py ======================================= test session starts ==================== platform win32 -- Python 3.6.8, pytest-6.0.1, py-1.9.0, pluggy-0.13.1 rootdir: C:\Users\admin\Desktop\ch1 collected 1 item test_one.py . [100%] ========================================= 1 passed in 0.01s ====================

这是第二个测试用例

ch1/test_two.py def test_passing(): assert (1, 2, 3) == (3, 2, 1)

运行后结果

(venv) C:\Users\admin\Desktop\ch1>pytest test_two.py ================================================ test session starts ================================ platform win32 -- Python 3.6.8, pytest-6.0.1, py-1.9.0, pluggy-0.13.1 rootdir: C:\Users\admin\Desktop\ch1 collected 1 item test_two.py F [100%] =========================== FAILURES ================================= ________________________________________ test_passing ____________________ def test_passing(): > assert (1, 2, 3) == (3, 2, 1) E assert (1, 2, 3) == (3, 2, 1) E At index 0 diff: 1 != 3 E Use -v to get the full diff test_two.py:2: AssertionError =================================== short test summary info =================================== FAILED test_two.py::test_passing - assert (1, 2, 3) == (3, 2, 1) ==================================== 1 failed in 0.03s ========================================

1|11.1 资源获取

  pytest的官方文档地址

https://docs.pytest.org

  pytest通过PyPI(Python官方包管理索引)分发托管:

https://pypi.python.org/pypi/pytest

  建议使用vritualenv来使用

1|21.2 运行pytest

pytest --help usage: pytest [options] [file_or_dir] [file_or_dir] [...]

  在没有其他参数的情况下,pytest会递归遍历每个目录及其子目录。

  举一个例子,我们创建一个tasks子目录,并且创建以下测试文件:

ch1/tasks/test_three.py """Test the Task data type.""" from collections import namedtuple Task = namedtuple('Task', ['summary', 'owner', 'done', 'id']) Task.__new__.__defaults__ = (None, None, False, None) # 指定默认值 def test_defaults(): """Using no parameters should invoke defaults""" t1 = Task() t2 = Task(None, None, False, None) assert t1 == t2 def test_member_access(): """Check .field functionality of namedtuple.""" t = Task('buy milk', 'brian') assert t.summary == 'buy milk' assert t.owner == 'brian' assert (t.done, t.id) == (False, None)

 

  下面演示下_asdict() 函数和 _replace() 函数的功能:

# ch1/tasks/test_four.py """Type the Task data type""" from collections import namedtuple Task = namedtuple('Task', ['summary', 'owner', 'done', 'id']) Task.__new__.__defaults__ = (None, None, False, None) def test_asdict(): """_asdict() should return a dictionary""" t_task = Task('do something', 'okken', True, 21) t_dict = t_task._asdict() expected = {'summary': 'do something', 'owner': 'okken', 'done': True, 'id': 21} assert t_dict == expected def test_replace(): """replace() should change passed in fields""" t_before = Task('finish book', 'brian', False) t_after = t_before._replace(id=10, done=True) t_expected = Task('finish book', 'brian', True, 10) assert t_after == t_expected

运行时

cd ch1 pytest

 

  如果不指定,pytest会搜索当前目录及子目录中以test_开头或者以_test结尾的测试函数

  我们把 pytest 搜索测试文件和测试用例的过程称为测试搜索(test discovery)。只要遵守命名规则,就能自动搜索。以下是几条主要的命名规则

  1. 测试文件应当命名为 test_<something>.py 或者 <something_test.py>
  2. 测试函数、测试类方法应当命名为teet_<something>
  3. 测试类应当命名为 Test<Something>

 运行单个文件时的控制台信息

================================================= test session starts ======================================================== platform win32 -- Python 3.6.8, pytest-6.0.1, py-1.9.0, pluggy-0.13.1 rootdir: C:\Users\admin\Desktop\ch1,inifile: collected 1 item test_one.py . [100%] ================================================ 1 passed in 0.01s ============================================================
====== test session starts ======

   pytest为每段测试会话(session)做了明确的分隔,一段会话就是pytest的一次调用

platform win32 -- Python 3.6.8, pytest-6.0.1, py-1.9.0, pluggy-0.13.1

   运行平台和版本

rootdir: C:\Users\admin\Desktop\ch1,inifile:

  rootdir(当前起始目录)是pytest搜索测试代码时最常使用的目录,inifile用于列举配置文件(这里没有定义),文件名可能是pytest.ini、tox.ini或者setup.cfg

collected 1 item

   搜索范围内找到两个测试条目

test_one.py . [100%]

   表示测试文件及结果。点号表示通过。Failure(失败)、error(异常)、skip(跳过)、xfail(预期失败)、xpass(预期失败但通过)会被分别标记为F、E、s、x、X,使用 -v  或者 --verbose 可以看到更多细节

=== 1 passed in 0.01s ====

   表示测试通过或者失败等条目的数量以及这段会话耗费的时间,如果存在未通过的测试用例,则会根据未通过的类型列举数量。

以下是可能出现的类型:

PASSED(.):测试通过

FAILED(F):测试失败(也有可能是XPASS状态与strict选项冲突造成的失败,见后文)

SKIPPED(s):测试未被执行。指定测试跳过执行,可以将测试标记为@pytest.mark.skip(),或者使用@pytest.mark.skipif()指定跳过测试的条件

xfail(x):预期测试失败,并且确实失败。使用@pytest.mark.xfail()指定你认为会失败的测试用例。

XPASS(X):预期测试失败,但实际上运行通过,不符合预期。

ERROR(E):测试用例之外的代码触发了异常,可能由 fixture 引起,也可能由 hook 函数引起

1|31.3 运行单个测试用例

可以直接在指定文件后添加 ::test_name

pytest -v tasks/test_four.py::test_asdict

1|41.4 使用命令行选项

--collect-only选项

  使用 --collect-only 选项可以展示在给定的配置下哪些测试用例会被运行。

(venv) C:\Users\admin\Desktop\ch1>pytest --collect-only ================= test session starts ================= platform win32 -- Python 3.6.8, pytest-6.0.1, py-1.9.0, pluggy-0.13.1 rootdir: C:\Users\admin\Desktop\ch1 collected 6 items <Module test_one.py> <Function test_passing> <Module test_two.py> <Function test_passing> <Module tasks/test_four.py> <Function test_asdict> <Function test_replace> <Module tasks/test_three.py> <Function test_defaults> <Function test_member_access> ================= no tests ran in 0.02s =================

  --collect-only选项可以让你非常方便地在测试运行之前,检查选中的测试用例是否符合预期。

-k 选项

  -k 选项允许你使用表达式指定希望运行的测试用例。

  假设希望选中 test_asdict() 和 test_defaults(),name可以使用 --collect-only 验证:

(venv) C:\Users\admin\Desktop\ch1>pytest -k "asdict or defaults" --collect-only ============================ test session starts ============================ platform win32 -- Python 3.6.8, pytest-6.0.1, py-1.9.0, pluggy-0.13.1 rootdir: C:\Users\admin\Desktop\ch1 collected 6 items / 4 deselected / 2 selected <Module tasks/test_four.py> <Function test_asdict> <Module tasks/test_three.py> <Function test_defaults> ============================ 4 deselected in 0.02s ============================

-m选项

  标记(marker)用于标记测试并分组。

  使用什么标记名由你自己决定,比如 @pytest.mark.mark1 或者 @pytest.mark.mark2

@pytest.mark.mark1 def test_member_access(): """Check .field functionality of namedtuple.""" t = Task('buy milk', 'brian') assert t.summary == 'buy milk' assert t.owner == 'brian' assert (t.done, t.id) == (False, None)

此时运行

(venv) C:\Users\admin\Desktop\ch1>pytest -m mark1 =============================================================================================================================== test session starts ================================================================================================================================ platform win32 -- Python 3.6.8, pytest-6.0.1, py-1.9.0, pluggy-0.13.1 rootdir: C:\Users\admin\Desktop\ch1 collected 6 items / 5 deselected / 1 selected tasks\test_three.py . [100%] ================== warnings summary ================== tasks\test_three.py:16 C:\Users\admin\Desktop\ch1\tasks\test_three.py:16: PytestUnknownMarkWarning: Unknown pytest.mark.mark1 - is this a typo? You can register custom marks to avoid this warning - for details, see https://docs.pytest.org/en/stable/mark.html @pytest.mark.mark1 -- Docs: https://docs.pytest.org/en/stable/warnings.html ================== 1 passed, 5 deselected, 1 warning in 0.03s ==================

  使用 -m 选项可以用表达式指定多个标记名。

  使用 -m "mark1 and mark2" 可以同时选中带有这两个标记的所有测试用例。

  使用 -m "mark1 and not mark2" 则会选中带有mark1的测试用例,同时过滤掉带有mark2 的测试用例。

  使用 -m "mark1 or mark2" 同时选中带有 mark1 或者 mark2 的所有测试用例。

-x 选项(小写)

  正常情况下,如果有运行失败的用例,pytest 会标记为失败,但是会继续运行下一个测试用例。

  如果我们希望遇到失败时立即停止整个会话,这时 -x 选项就派上用场了。

(venv) C:\Users\admin\Desktop\ch1>pytest -x ========================== test session starts ========================== platform win32 -- Python 3.6.8, pytest-6.0.1, py-1.9.0, pluggy-0.13.1 rootdir: C:\Users\admin\Desktop\ch1 collected 6 items test_one.py . [ 16%] test_two.py F ========================== FAILURES ========================== ____________________________test_passing ____________________________ def test_passing(): > assert (1, 2, 3) == (3, 2, 1) E assert (1, 2, 3) == (3, 2, 1) E At index 0 diff: 1 != 3 E Use -v to get the full diff test_two.py:2: AssertionError========================== short test summary info ========================== FAILED test_two.py::test_passing - assert (1, 2, 3) == (3, 2, 1) !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! ========================== 1 failed, 1 passed, 1 warning in 0.05s ==========================

  如果没有 -x 选项,那么6个测试都会被执行,去掉 -x 再运行一次,并且使用 --tb=no 选项关闭错误信息回溯。

(venv) C:\Users\admin\Desktop\ch1>pytest --tb=no ====================== test session starts ====================== platform win32 -- Python 3.6.8, pytest-6.0.1, py-1.9.0, pluggy-0.13.1 rootdir: C:\Users\admin\Desktop\ch1 collected 6 items test_one.py . [ 16%] test_two.py F [ 33%] tasks\test_four.py .. [ 66%] tasks\test_three.py .. [100%] ====================== short test summary info ====================== FAILED test_two.py::test_passing - assert (1, 2, 3) == (3, 2, 1) ====================== 1 failed, 5 passed in 0.03s ======================

--maxfail=num

  -x 选项的特点是,一旦遇到测试失败,就会全局停止。

  使用 --maxfail 选项,明确指定可以失败几次。

pytest --maxfail=2 --tb=no

-s 与 --capture=method

  -s选项允许终端在测试运行时输出某些结果(比如print),包括任何符合标准的的输出流信息。

  -s 等价于 --capture=no

--lf(--last-failed)选项

  当一个或多个测试失败时,我们常常希望能够定位到最后一个失败的测试用例重新运行,这时候可以使用 --lf 选项

  至于上一个失败的测试用例,pytest框架会自动记录

--ff(--failed-first)选项

  --ff(--failed-first)选项与 --last-failed选项的作用基本相同,不同之处在于 --ff 会运行完剩余的测试用例。

-v(--verbose)选项

  最明显的区别就是每个文件中的每个测试用例都占一行(先前是每个文件占一行)

(venv) C:\Users\admin\Desktop\ch1>pytest -v ============================ test session starts ============================ platform win32 -- Python 3.6.8, pytest-6.0.1, py-1.9.0, pluggy-0.13.1 -- c:\users\admin\desktop\ch1\venv\scripts\python.exe cachedir: .pytest_cache rootdir: C:\Users\admin\Desktop\ch1 collected 6 items test_one.py::test_passing PASSED [ 16%] test_two.py::test_passing FAILED [ 33%] tasks/test_four.py::test_asdict PASSED [ 50%] tasks/test_four.py::test_replace PASSED [ 66%] tasks/test_three.py::test_defaults PASSED [ 83%] tasks/test_three.py::test_member_access PASSED [100%]

-q(--quiet)选项

  该选项的作用与 -v/--verbose的相反,它会简化输出信息,只保留最核心的内容。

-l(--showlocals)选项

  使用 -l 选项,失败测试用例由于被堆栈追踪,所以局部变量及其值都会显示出来。

(venv) C:\Users\admin\Desktop\ch1>pytest -l tasks/test_four.py ========================= test session starts ========================= platform win32 -- Python 3.6.8, pytest-6.0.1, py-1.9.0, pluggy-0.13.1 rootdir: C:\Users\admin\Desktop\ch1 collected 2 items tasks\test_four.py .F [100%] ============================ FAILURES ============================ ___________________________________ test_replace ___________________________________ def test_replace(): """replace() should change passed in fields""" t_before = Task('finish book', 'brian', False) t_after = t_before._replace(id=10, done=True) t_expected = Task('finish book', 'brian', True, 11) > assert t_after == t_expected E assert Task(summary=...e=True, id=10) == Task(summary=...e=True, id=11) E At index 3 diff: 10 != 11 E Use -v to get the full diff t_after = Task(summary='finish book', owner='brian', done=True, id=10) t_before = Task(summary='finish book', owner='brian', done=False, id=None) t_expected = Task(summary='finish book', owner='brian', done=True, id=11) tasks\test_four.py:25: AssertionError ========================== short test summary info ========================== FAILED tasks/test_four.py::test_replace - assert Task(summary=...e=True, id=10) == Task(summary=...e=True, id=11) ========================== 1 failed, 1 passed in 0.05s ==========================

  assert 触发测试失败之后,代码片段下方显示的是本地变量 t_after、t_before、t_expected详细的值。标红处显示。

--tb=style选项

  --tb=style选项决定捕捉到失败时输出信息的显示方式。某个测试用例失败后,pytest会列举出失败信息,包括失败出现在哪一行、是什么失败、怎么失败的,此过程我们称之为“信息回溯”

  常用的三种模式:

  short 模式仅输出 assert的一行以及系统判定内容(不显示上下文);

  line 模式只使用一行输出显示所有的错误信息

  no 模式则直接屏蔽全部回溯信息

  还有三种可选模式:

  --tb=long 输出最为详尽的回溯信息

  --tb=auto 是默认值,如果有多个测试用例失败,仅打印第一个和最后一个用例的回溯信息(格式与long模式的一致)

  --tb=native 只输出Python标准库的回溯信息,不显示额外信息

--durations=N选项

  --duration=N 选项可以加快测试节奏。它不关心测试时如何运行运行的,只统计测试过程中哪几个阶段是最慢的(包括每个测试用例的call、setup、teardown过程)。

  使用--duration=0,则会将所有阶段按耗时长短排序后显示。

(venv) C:\Users\admin\Desktop\ch1>pytest --durations=0 tasks -vv ========================= test session starts ========================= platform win32 -- Python 3.6.8, pytest-6.0.1, py-1.9.0, pluggy-0.13.1 -- c:\users\admin\desktop\ch1\venv\scripts\python.exe cachedir: .pytest_cache rootdir: C:\Users\admin\Desktop\ch1 collected 4 items tasks/test_four.py::test_asdict PASSED [ 25%] tasks/test_four.py::test_replace PASSED [ 50%] tasks/test_three.py::test_defaults PASSED [ 75%] tasks/test_three.py::test_member_access PASSED [100%] ========================= slowest durations ========================= 0.00s setup tasks/test_four.py::test_asdict 0.00s teardown tasks/test_three.py::test_member_access 0.00s setup tasks/test_four.py::test_replace 0.00s call tasks/test_four.py::test_asdict 0.00s setup tasks/test_three.py::test_member_access 0.00s setup tasks/test_three.py::test_defaults 0.00s teardown tasks/test_four.py::test_asdict 0.00s teardown tasks/test_four.py::test_replace 0.00s call tasks/test_four.py::test_replace 0.00s teardown tasks/test_three.py::test_defaults 0.00s call tasks/test_three.py::test_member_access 0.00s call tasks/test_three.py::test_defaults ========================= 4 passed in 0.02s =========================

--version 选项

  使用 --version 可以显示当前的 pytest 版本及安装目录

-h(--help)选项

  使用 -h 选项可以获得:

基本用法:pytest [options] [file_or_dir] [file_or_dir] [...]

命令行选项及其用法,包括新添加的插件的选项及其用法

可用于ini配置文件中的选项

影响pytest行为的环境变量

使用 pytest --markers 时的可用 marker 列表

使用 pytest --fixtures 时的可用 fixture 列表

2|0二、编写测试函数

2|12.1 目录结构

Tasks项目的文件结构:

tasks_proj/ |——CHANGELOG.rst |——LICENSE |——MANIFEST.in |——README.rst |——setup.py |——src (放源码) | |——tasks | |——__init__.py | |——api.py | |——cli.py | |——config.py | |——tasksdb_pymongo.py | |——taskdb_tinydb.py |——tests (放测试) |——conftest.py |——pytest.ini |——func |——__init__.py |——test_add.py |——。。。 |——unit |——_init__.py |——test_task.py |。。。

2|22.2 使用 assert 声明

  pytest有一个重要功能是可以重写 assert 关键字。pytest 会截断对原生 assert 的调用,替换为 pytest 定义的assert,从而提供更多的失败信息和细节。

  每个失败的测试用例在行首都用一个 > 号来标识。以 E 开头的行时 pytest 提供的额外判断信息,用于帮组我们了解异常的具体情况。

2|32.3 预期异常

  测试异常的格式 with pytest.raises(<expected exception>)

import pytest import tasks def test_add_raises(): """add() should raise an exception with wrong type param""" with pytest.raises(TypeError): tasks.add(task="not a Task object")

  测试用例 test_add_raises() 中有 with pytest.raises(TypeError)声明,意味着无论with中的内容是什么,都至少会发生TypeError异常。如果测试通过,说明确实发生了我们预期 TypeError 异常:如果抛出的是其他类型的异常,则与我们所预期的不一致,说明测试失败。

  上面的测试中只检验了传参数据的 “类型异常”,换可以检验 “值异常”。为校验异常信息是否符合预期,可以通过增加 as excinfo 语句得到异常消息的值,再进行比对。

import pytest import tasks def test_add_raises(): """add() should raise an exception with wrong type param""" with pytest.raises(AttributeError) as excinfo: tasks.add(task="not a Task object") exception_msg = excinfo.value.args[0] # 获得报错信息 assert exception_msg == "module 'tasks' has no attribute 'add'"

2|42.4 测试函数的标记

  pytest 允许使用 marker 对测试函数做标记。

  一个测试函数可以有多个 marker,一个 marker 也可以用来标记多个测试函数。

  带有相同 marker 的测试即使存放在不同的文件下,也会被一起执行。

import pytest import tasks @pytest.mark.smoke def test_add_raises_true(): """add() should raise an exception with wrong type param""" with pytest.raises(AttributeError) as excinfo: tasks.add(task="not a Task object") exception_msg = excinfo.value.args[0] assert exception_msg == "module 'tasks' has no attribute 'add'" @pytest.mark.smoke @pytest.mark.get def test_add_raises_false(): """add() should raise an exception with wrong type param""" with pytest.raises(AttributeError) as excinfo: tasks.add(task="not a Task object") exception_msg = excinfo.value.args[0] assert exception_msg == "module 'tasks' has no attribute 'addtwo'"

  可以通过以下命令运行

pytest -m smoke pytest -m get

  -m 后面也可以加表达式,可以在标记之间添加 add、or、not 关键字

pytest -m "smoke and get" pytest -m "smoke or get" pytest -m "smoke and not get"

  警告信息消除,mark标记时会warn,可以在conftest里面添加

def pytest_configure(config): marker_list = ["search","login"] for markers in marker_list: config.addinivalue_line("markers",markers)

2|52.5 跳过测试

  skip 和 skipif 允许你跳过不希望运行的测试。

@pytest.mark.skip(reason="跳过的原因") @pytest.mark.skipif(表达式,reason="跳过的原因")

  skipif() 的判断条件可以使任何Python 表达式,这里比对的是包版本。

   如果运行的时候要看到跳过的原因,可以使用 -rs

-r 选项

 

-r选项可以在执行结束后,打印一个简短的总结报告。在执行的测试用例很多时,可以让你对结果有个清晰的了解
-r选项后面要紧接以下的一个参数,用于过滤显示测试用例的结果。

 

以下是所有有效的字符参数:

 

  • f:失败的
  • E:出错的
  • s:跳过执行的
  • x:跳过执行,并标记为xfailed的
  • X:跳过执行,并标记为xpassed的
  • p:测试通过的
  • P:测试通过,并且有输出信息的;即用例中有print
  • a:除了测试通过的,其他所有的;即除了pP
  • A:所有的

 

上述字符参数可以叠加使用,例如:我们期望过滤出失败的和未执行的:

pytest -rfs

 

2|62.6 标记预期会失败的测试

  使用 skip 和 skipif 标记,测试会直接跳过,而不会被执行。使用 xfail 标记,则告诉pytest运行此测试,但我们预期它会失败。

@pytest.mark.xfail(表达式,reason="跳过的原因")

2|72.7 运行测试子集

单个目录

  运行单个目录下的所有测试,以目录作为 pytest 的参数即可。

pytest tests/func --tb=no

单个测试文件/模块

  运行单个文件里的全部测试,以路径名加文件名作为 pytest 参数即可。

pytest tests/func/test_add.py

单个测试函数

  运行单个测试函数,只需要在文件名后面添加 :: 符号和函数号

pytest tests/func/test_add.py::test_add_returns_valid_id

单个测试类

  测试类用于将某些相似的测试函数组合在一起。

class TestUpdate(): """Test expected exceptions with tasks.update().""" def test_bad_id(self): """A non-int id should raise an exception""" with pytest.raises(TypeError): tasks.upadte(task_id={"dict instead": 1}, task=tasks.Task()) def test_bad_task(self): """A non-Task task should raise an excption""" with pytest.raises(TypeError): tasks.update(task_id=1, task="not a task")

  要运行该类,可以在文件名后面加上 :: 符号和类名(与运行单个测试函数类似)

pytest tests/func/test_api_exceptions.py::TestUpdate

单个测试类中的测试方法

  如果不希望运行测试类中的所有测试,只想指定运行其中一个,一样可以在文件名后面添加 :: 符号和方法名。

pytest tests/func/test_api.py:TestUpdate:test_bad_id

用测试名划分测试集合

  -k 选项允许用一个表达式指定需要运行的测试,该表达式可以匹配测试名(或其子串)。

  表达式中可以包含and 、or 、not

运行所有名字中包含 _raises 的测试

pytest -k _raises

如果要跳过 test_delete_raises() 的执行,则可以使用 and 和  not

pytest -k "_raises and not delete"

2|8 2.8 参数化测试

  有时候仅仅使用一组数据是无法充分测试函数功能的,参数化测试允许传递多组数据。

import pytest @pytest.mark.parametrize("task", [Task("sleep", done=True), Task("wake", "brian"), Task("breathe", "BRIAN", True), Task("exercise", "BrIaN", "False")]) def test_add_2(task): """Demonstrate paramertrize with one parameter""" task_id = task.add(task) t_from_db = tasks.get(task_id) assert equivalent(t_from_db, task)

  @pytest.mark.parametrize() 的第一个参数是用逗号分隔的字符串列表;第二个参数是一个值列表。

  pytest会轮流对每个task做测试,并分别报告每一个测试用例的结果。

  以下是 多组键值对情况

import pytest @pytest.mark.parametrize("str", ["abc","def","twq","tre"]) def test_add_2(str): """Demonstrate paramertrize with one parameter""" str2 = "abc" assert str == str2

执行如下:

(venv) C:\Users\admin\Desktop\ch2>pytest test_add_variety.py -v =============================== test session starts =============================== platform win32 -- Python 3.6.8, pytest-6.0.1, py-1.9.0, pluggy-0.13.1 -- c:\users\admin\desktop\ch2\venv\scripts\python.exe cachedir: .pytest_cache rootdir: C:\Users\admin\Desktop\ch2 collected 4 items test_add_variety.py::test_add_2[abc] PASSED [ 25%] test_add_variety.py::test_add_2[def] FAILED [ 50%] test_add_variety.py::test_add_2[twq] FAILED [ 75%] test_add_variety.py::test_add_2[tre] FAILED [100%]

如有以下的参数化测试用例

import pytest from collections import namedtuple Task = namedtuple('Task', ['summary', 'owner', 'done', 'id']) Task.__new__.__defaults__ = (1, 1, 1, 1) str_to_try = [Task(2, 2, 2, 2), Task(3, 3, 3, 3), Task(4, 4, 4, 4), Task(5, 5, 5, 5)] @pytest.mark.parametrize("task", str_to_try) def test_add_2(task): """Demonstrate paramertrize with one parameter""" t1 = Task() t2 = Task(None, None, False, None) assert t1 == t2

可见可读性非常差

(venv) C:\Users\admin\Desktop\ch2>pytest test_add_variety.py -v ======================================= test session starts ======================================= platform win32 -- Python 3.6.8, pytest-6.0.1, py-1.9.0, pluggy-0.13.1 -- c:\users\admin\desktop\ch2\venv\scripts\python.exe cachedir: .pytest_cache rootdir: C:\Users\admin\Desktop\ch2 collected 4 items test_add_variety.py::test_add_2[task0] FAILED [ 25%] test_add_variety.py::test_add_2[task1] FAILED [ 50%] test_add_variety.py::test_add_2[task2] FAILED [ 75%] test_add_variety.py::test_add_2[task3] FAILED [100%] ==================== FAILURES ====================

  为了改善可读性,我们为parametrize()引入一个额外参数ids,使列表中的每一个元素都被表示。ids 是一个字符串列表,它和数据对象列表的长度保持一致。由于给数据集分配了一个变量 tasks_to_try,所以可以通过他生成ids。

import pytest from collections import namedtuple Task = namedtuple('Task', ['summary', 'owner', 'done', 'id']) Task.__new__.__defaults__ = (1, 1, 1, 1) str_to_try = [Task(2, 2, 2, 2), Task(3, 3, 3, 3), Task(4, 4, 4, 4), Task(5, 5, 5, 5)] str_ids = ["Task({},{},{})".format(i.summary, i.owner, i.done, i.id) for i in str_to_try] @pytest.mark.parametrize("task",str_to_try,ids=str_ids) def test_add_2(task): """Demonstrate paramertrize with one parameter""" t1 = Task() t2 = Task(None, None, False, None) assert t1 == t2

  自定义测试标识能够被 pytest 识别

(venv) C:\Users\admin\Desktop\ch2>pytest test_add_variety.py -v ============================ test session starts ============================ platform win32 -- Python 3.6.8, pytest-6.0.1, py-1.9.0, pluggy-0.13.1 -- c:\users\admin\desktop\ch2\venv\scripts\python.exe cachedir: .pytest_cache rootdir: C:\Users\admin\Desktop\ch2 collected 4 items test_add_variety.py::test_add_2[Task(2,2,2)] FAILED [ 25%] test_add_variety.py::test_add_2[Task(3,3,3)] FAILED [ 50%] test_add_variety.py::test_add_2[Task(4,4,4)] FAILED [ 75%] test_add_variety.py::test_add_2[Task(5,5,5)] FAILED [100%] ============================ FAILURES ============================

  @pytest.mark.parametrize() 装饰器也可以给测试类加上,在这种情况下,该数据集会被传递给该类的所有类方法。

import pytest from collections import namedtuple Task = namedtuple('Task', ['summary', 'owner', 'done', 'id']) Task.__new__.__defaults__ = (1, 1, 1, 1) str_to_try = [Task(2, 2, 2, 2), Task(3, 3, 3, 3), Task(4, 4, 4, 4), Task(5, 5, 5, 5)] str_ids = ["Task({},{},{})".format(i.summary, i.owner, i.done, i.id) for i in str_to_try] @pytest.mark.parametrize("task",str_to_try,ids=str_ids) class TestAdd(): def test_add_2(self,task): """Demonstrate paramertrize with one parameter""" t1 = Task() t2 = Task(None, None, False, None) assert t1 == t2 def test_add_3(self,task): """Demonstrate paramertrize with one parameter""" t3 = Task() t4 = Task(None, None, False, None) assert t3 == t4

  在给@pytest.mark.parametrize() 装饰器传入列表参数时,还可以在参数值旁边定义一个 id 来做标识,语法是 pytest.param(<value>,id="something")

import pytest from collections import namedtuple @pytest.mark.parametrize("task", [ pytest.param(Task("create"), id="just summary"), pytest.param(Task("inspire", "Michelle"), id="summary/owner"), pytest.param(Task("encourage", "Michelle", Ture), id="summary/oener/done") ]) def test_add_6(task): task_id = tasks.add(task) t_from_db = tasks.get(task_id) assert equivalent(t_from_db, task)

  标识也能够被识别

(venv) C:\Users\admin\Desktop\ch2>pytest test_add_variety.py -v ============================ test session starts ============================ platform win32 -- Python 3.6.8, pytest-6.0.1, py-1.9.0, pluggy-0.13.1 -- c:\users\admin\desktop\ch2\venv\scripts\python.exe cachedir: .pytest_cache rootdir: C:\Users\admin\Desktop\ch2 collected 4 items test_add_variety.py::test_add_6[just summary] PASSED [ 33%] test_add_variety.py::test_add_6[summary/owner] PASSED [ 66%] test_add_variety.py::test_add_6[summary/owner/done] PASSED [ 100%]============================ FAILURES ============================

参数组合

@pytest.mark.parametrize("x",[1,2]) @pytest.mark.parametrize("y",[8,10,11]) def test_foo(x,y): print(f"测试数据组合x:{x},y:{y}")

方法作为参数名

test_user_data = ['Tome','Jerry'] @pytest.fixture(scope="module") def login_r(request): # 这是接受并传入的参数、 user = request.parame print(f"\n 打开首页准备登录,登录用户:{user}") return user # indirect=True,可以把传过来的参数当函数来执行 @pytest.mark.parametrize("login_r",test_user_data,indirect=True) def test_login(login_r): a = login_r print(f"测试用例中login的返回值:{a}") assert a != ""

 

3|0三、pytest fixture

  fixture 是在测试函数运行前后,由pytest执行的外壳函数。

  简单实例

import pytest @pytest.fixture() def some_data(): return 42 def test_some_data(some_data): assert some_data == 42

  测试用例 test_some_data() 的参数列表中包含一个 fixture名 some_data,pytest 会以该名称搜索 fixture(可见命名在pytest 中是非常重要的。)

  pytest 会优先搜索该测试所在的模块,然后搜索 conftest.py

3|13.1 通过 conftest.py 共享 fixture

  fixture 可以放在单独的测试文件里。此时只有这个测试文件能够使用相关的fixture。

  如果希望多个测试文件共享 fixture,可以在某个公共目录下新建一个 conftest.py 文件,将 fixture 放在其中。(作用域根据所放的文件夹决定,最上层文件夹的话整个项目共用,子文件夹的话,子文件夹里面的测试共用。)

  尽管 conftest.py 是Python 模块,但它不能被测试文件导入。import conftest 的用法是不允许出现的。conftest.py 被 pytest 视作一个本地插件库。可以把 tests/conftest.py 看成一是一个供 tests 目录下所有测试使用的 fixture仓库。

3|23.2 使用 fixture 执行配置及销毁逻辑

  fixture 函数会在测试函数之前运行,但如果 fixture 函数包含 yield,那么系统会在 yield 处停止,转而运行测试函数,等测试函数执行完毕后再回到 fixture,继续执行 yield 之后的代码。

  可以将 yield 之前的代码视为 配置(setup)过程,将yield 之后的代码视为清理(teardown)过程。

  无论测试过程中发生了说明,yield之后的代码都会被执行。

3|33.3 使用 --setup-show 回溯 fixture 的执行过程

  直接运行测试,则不会看到fisture的执行过程。

  如果希望看到测试过程中执行的是什么,以及执行的先后顺序。pytest 提供的 --setup-show 选项可以实现这个功能。

pytest --setup-show test_add.py

  fixture 名称前面的F 和S代表的是fixture的作用范围,F代表函数级别的作用范围。S代表会话级别的作用范围。

3|43.4 使用 fixture 传递测试数据

  fixture 非常适合存放测试数据,并且它可以返回任何数据。

import pytest @pytest.fixture() def a_tuple(): return (1, "foo", None, {"bar": 23}) def test_a_tuple(a_tuple): assert a_tuple[3]["bar"] == 32

  yeild 返回数据

import pytest @pytest.fixture() def a_tuple(): print("1111") yield (1, "foo", None, {"bar": 23}) print("2222") def test_a_tuple(a_tuple): assert a_tuple[3]["bar"] == 32

  明显23不等于32,所以会失败。

(venv) C:\Users\admin\Desktop\ch2>pytest test_fixture.py ============================ test session starts ============================ platform win32 -- Python 3.6.8, pytest-6.0.1, py-1.9.0, pluggy-0.13.1 rootdir: C:\Users\admin\Desktop\ch2 collected 1 item test_fixture.py F [100%] ============================ FAILURES ============================ ____________________________ test_a_tuple ____________________________ a_tuple = (1, 'foo', None, {'bar': 23}) def test_a_tuple(a_tuple): > assert a_tuple[3]["bar"] == 32 E assert 23 == 32 test_fixture.py:10: AssertionError ============================ short test summary info ============================ FAILED test_fixture.py::test_a_tuple - assert 23 == 32 ============================ 1 failed in 0.03s ============================

   pytest 给出了具体引起 assert 异常的函数参数值。fixture 作为测试函数的参数,也会被堆栈跟踪并纳入测试报告。

  假设assert 异常(或任何类型的异常)就发生在fixture,会发生什么?

import pytest @pytest.fixture() def a_tuple(): x = 43 assert x == 43 def test_a_tuple(a_tuple): assert a_tuple[3]["bar"] == 32

  在fixture 中,42 不等于 43,断言错误。pytest运行时,如下:

(venv) C:\Users\admin\Desktop\ch2>pytest test_fixture.py ======================== test session starts ======================== platform win32 -- Python 3.6.8, pytest-6.0.1, py-1.9.0, pluggy-0.13.1 rootdir: C:\Users\admin\Desktop\ch2 collected 1 item test_fixture.py E [100%] ======================== ERRORS ======================== __________________________________ ERROR at setup of test_a_tuple __________________________________ @pytest.fixture() def a_tuple(): x = 43 > assert 42 == 43 E assert 42 == 43 test_fixture.py:7: AssertionError ======================== short test summary info ======================== ERROR test_fixture.py::test_a_tuple - assert 42 == 43 ======================== 1 error in 0.04s ========================

  可以看到 执行结果是 ERROR 而不是 FAIL。

  这个区分很清楚,如果测试结果为 fail,用户就知道失败是发生在核心测试函数内,而不是发生在测试依赖的 fixture。

3|53.5 使用多个 fixture

  fixture互相调用

import pytest @pytest.fixture() def a_tuple(): return (1,2,3) @pytest.fixture() def two_tuple(a_tuple): if a_tuple[2] == 3: return (1, "foo", None, {"bar": 23}) return False def test_a_tuple(two_tuple): if two_tuple: assert two_tuple[3]["bar"] == 23

  用例中传入多个fixture

import pytest @pytest.fixture() def a_tuple(): return 1 @pytest.fixture() def two_tuple(): return 2 def test_a_tuple(a_tuple, two_tuple): assert a_tuple == 1 assert two_tuple == 2

3|63.6 指定 fixture 作用范围

  fixture 包含一个叫 scope(作用范围)的可选参数,用于控制 fixture 执行配置和销毁逻辑的频率。@pytest.fixture() 的 scope 参数有四个待选值:

  • function
  • class
  • module
  • session(默认值)

以下是对各个 scope 的概述

scope=“function”

  函数级别的 fixture 每个测试函数只需要运行一次。配置代码在测试用例运行之前运行,销毁代码在测试用例运行之后运行。是默认值

scope=“class”

  类级别的fixture 每个测试类只需要运行一次,无论测试类里面有多少类方法都可以共享这个fixture

scope="module"

  模块级别的fixture每个模块只需要运行一次,无论模块里有多少个测试函数、类方法或其他fixture 都可以共享这个fixture

scope=“session”

  会话级别的 fixture 每次会话只需要运行一次。一次 pytest 会话中所有测试函数、方法都可以共享这个 fixture。

 

import pytest @pytest.fixture(scope="function") def func_scope(): """A function scope fixture.""" @pytest.fixture(scope="module") def mod_scope(): """A module scope fixture.""" @pytest.fixture(scope="session") def sess_scope(): """A session scope fixture.""" @pytest.fixture(scope="class") def class_scope(): """A class scope fixture.""" def test_1(sess_scope,mod_scope,func_scope): """Demo is more fun with multiple tests""" @pytest.mark.usefixtures("class_scope") class TestSomething(): """Demo class scope fixtures.""" def test_3(self): """Test using a class scope fixture.""" def test_4(self): """Again,multiple tests are more fun."""

  执行结果

(venv) C:\Users\admin\Desktop\ch2>pytest --setup-show test_scope.py ================================== test session starts ================================== platform win32 -- Python 3.6.8, pytest-6.0.1, py-1.9.0, pluggy-0.13.1 rootdir: C:\Users\admin\Desktop\ch2 collected 3 items test_scope.py SETUP S sess_scope SETUP M mod_scope SETUP F func_scope test_scope.py::test_1 (fixtures used: func_scope, mod_scope, sess_scope). TEARDOWN F func_scope SETUP C class_scope test_scope.py::TestSomething::test_3 (fixtures used: class_scope). test_scope.py::TestSomething::test_4 (fixtures used: class_scope). TEARDOWN C class_scope TEARDOWN M mod_scope TEARDOWN S sess_scope ================================== 3 passed in 0.01s ==================================

  使用 --setup-show 命令行选项观察每个 fixture 被调用的次数,以及在各自作用范围下执行配置、销毁逻辑的顺序。

  F 代表函数级别,S 代表会话级别,C 代表类级别,M 代表模块级别

  fixture 只能使用同级别的fixture,或比自己级别更高的fixture。

3|73.7 使用 usefixtures 指定fixture

@pytest.mark.usefixtures("class_scope") class TestSomething(): """Demo class scope fixtures.""" def test_3(self): """Test using a class scope fixture.""" def test_4(self): """Again,multiple tests are more fun."""

  使用 usefixtures 和在测试方法中添加 fixture 参数,二者大体上是差不多的。区别之一在于只有后者才能够使用fixture的返回值。

3|83.8 为常用 fixture 添加 autouse 选项

  之前用到的 fixture 都是根据测试本身来命名的(或者针对示例的测试类使用 usefixtures)。我们可以通过制定 autouse=True选项,使作用域内的测试函数都自动运行 fixture

  下面是一个比较生硬的例子

import pytest import time @pytest.fixture(autouse=True,scope="session") def footer_session_scope(): yield now = time.time() print("---") print("finished:{}".format(time.strftime("%d %b %X",time.localtime(now)))) print("---------------------------") @pytest.fixture(autouse=True) def foot_function_scope(): start = time.time() yield stop = time.time() delta = stop - start print("\ntest duration : {0:3} seconds".format(delta)) def test_1(): time.sleep(1) def test_2(): time.sleep(1.4)

3|93.9 为 fixture 重命名

@pytest.fixture(name="another")

3|103.10 fixture 的参数化

import pytest from collections import namedtuple Task = namedtuple('Task', ['summary', 'owner', 'done', 'id']) Task.__new__.__defaults__ = (1, 1, 1, 1) str_to_try = [Task(2, 2, 2, 2), Task(3, 3, 3, 3), Task(4, 4, 4, 4), Task(5, 5, 5, 5)] @pytest.fixture(params=str_to_try) def a_task(request): """Demonstrate paramertrize with one parameter""" return request.param def test_add_a(a_task): assert a_task == Task()

  fixture 参数列表中的request 也是 pytest 内建的fixture 之一。代表 fixture 的调用状态。

  它有一个 param 字段,会被@pytest.fixture(params = tasks_to_try) 的params 列表中的一个元素填充。

  也可以指定 ids。(只不过这里的 ids 也是函数,不是列表)

import pytest from collections import namedtuple Task = namedtuple('Task', ['summary', 'owner', 'done', 'id']) Task.__new__.__defaults__ = (1, 1, 1, 1) str_to_try = [Task(2, 2, 2, 2), Task(3, 3, 3, 3), Task(4, 4, 4, 4), Task(5, 5, 5, 5)] def id_func(fixture_value): t = fixture_value return "Task({}{}{}{}".format(t.summary,t.owner,t.done,t.id) @pytest.fixture(params=str_to_try,ids=id_func) def c_task(request): """Demonstrate paramertrize with one parameter""" return request.param def test_add_c(c_task): assert c_task == Task()

4|0四、内置 fixture 

4|14.1 使用 tmpdir 和 tmpdir_factory

  内置的 tmpdir 和 tmpdir_factory 负责在测试开始运行前创建临时文件目录,并在测试结束后删除。

  tmpdir 的作用范围是函数级别,tmpdir_factory 的作用范围是会话级别。

import pytest, time def test_tmpdir(tmpdir): # 创建一个文件 a_file = tmpdir.join("something.txt") # 创建一个文件夹 anything a_sub_dir = tmpdir.mkdir("anything") # 在创建的文件夹中再创建一个文件 another_file = a_sub_dir.join("something_sele.txt") # 在文件中写入数据 a_file.write("contents mat settle during shipping") another_file.write("something different") # 读取并比对 assert a_file.read() == "contents mat settle during shipping" assert another_file.read() == "something different"

  使用 tmpdir_factory 替换这个脚本

import pytest, time def test_tmpdir_factory(tmpdir_factory): # 相当于创建一个文件夹,相当于比 tmpdir要多做这一步操作。目录mydir a_dir = tmpdir_factory.mktemp("mydir") base_temp = tmpdir_factory.getbasetemp() print("base:", base_temp) #test_scope.py base: C:\Users\admin\AppData\Local\Temp\pytest-of-admin\pytest-11
a_file = a_dir.join("something.txt") a_sub_dir = a_dir.mkdir("anything") another_file = a_sub_dir.join("something_else.txt") a_file.write("contents may settle during shipping") another_file.write("something different") assert a_file.read() == "contents may settle during shipping" assert another_file.read() == "something different"

 

 

   pytest-num 会随着会话的递增而递增。pytest 会记录最近几次会话使用的根目录,更早的根目录记录则会被清理掉。(默认保留 3 次)

在其他作用范围内使用临时目录

  tmpdir_factory 的作用范围是会话级别的,tmpdir 的作用范围是函数级别的。如果需要模块或类级别作用范围的目录,该怎么办?可以利用 tmpdir_factory 再创建一个 fixture

  假设有一个测试模块,其中有很多测试用例要读取一个json文件。有下例:(放在 conftest.py下面)

import json import pytest @pytest.fixture(scope="module") def author_file_json(tmpdir_factory): python_author_data = { "Ned":{"City":"Boston"}, "Brian":{"City":"Portland"}, "Luciano":{"City":"Sau Paulo"} } file = tmpdir_factory.mktemp("data").join("author_file.json") print("file:{}".format((str(file)))) with file.open("w") as f: json.dump(python_author_data,f) return file

  上述代码创建了一个 json 文件。因为这个新 fixture 的作用范围是模块级别的,所以该 json 文件只需要被每个模块创建一次。

import json def test_brian_in_portland(author_file_json): with author_file_json.open() as f: authors = json.load(f) assert authors["Brian"]["City"] == "Portland" def test_all_hava_cities(author_file_json): with author_file_json.open() as f: authors = json.load(f) for a in authors: assert len(authors[a]["City"]) > 0

  这里两个 测试用例 将使用同一个 json 文件。

4|24.2 使用 pytestconfig

  内置 的 pytestconfig 可以通过命令行参数、选项、配置文件、插件、运行目录等方式来控制 pytest。

  下面使用 pytest 的 hook 函数 pytest_addoption 添加几个命令行选项

import pytest def pytest_addoption(parser): parser.addoption("--myopt",action="store_true",help="some boolean option") parser.addoption("--foo",action="store",default="bar",help="foo:bar or baz")

  以 pytest_addoption 添加的命令行选项必须通过插件来实现,或者在项目顶层目录的 conftest.py 文件中完成。它所在的 conftest.py 不能处于测试子目录下。

  再运行 help

pytest --help ... custom options: --myopt some boolean option --foo=FOO foo:bar or baz ...

  加下来就可以使用这些选项了

test_config.py import pytest def test_option(pytestconfig): print('"foo" set to:', pytestconfig.getoption("foo")) print('"myopt" set to:',pytestconfig.getoption("myopt"))

  结果如下

(venv) C:\Users\admin\Desktop\ch2>pytest -s -q test_config.py::test_option "foo" set to: bar "myopt" set to: False . 1 passed in 0.01s —————————————————————————————————— (venv) C:\Users\admin\Desktop\ch2>pytest -s -q --myopt test_config.py::test_option "foo" set to: bar "myopt" set to: True . 1 passed in 0.01s —————————————————————————————————— (venv) C:\Users\admin\Desktop\ch2>pytest -s -q --myopt --foo baz test_config.py::test_option "foo" set to: baz "myopt" set to: True . 1 passed in 0.01s

  因为 pytestconfig 是一个fixture,所以它也可以被其他 fixture 使用。

import pytest @pytest.fixture() def foo(pytestconfig): return pytestconfig.option.foo @pytest.fixture() def myopt(pytestconfig): return pytestconfig.option.myopt def test_fixtures_for_options(for,myopt): print('"foo" set to:',foo) print('"myopt" set to:',myopt)

4|34.3 使用 cache

  有时需要从一段测试会话传递信息给下一段会话很有用。

  cache 的作用是存储一段测试会话的信息,在下一段测试会话中使用。使用 pytest 内置的 --last-failed--failed-first 标识可以很好的展示 cache的功能。

  如果要清空缓存,可以在测试会话开始前传入 --cache-clear

   cache 的接口很简单

cache.get(key,default) cache.set(key,value)

  习惯上,键名以应用名字或插件名字开始,接着是 / ,然后是分隔开的键字符串。键值可以是任何可转化为 JSON 的东西,因为在 .cache 目录里是用 JSON 格式存储的。

  以下是一个 fixture ,记录测试的耗时,并存储到 cache ,如果接下来的测试耗时大于之前的两倍,就抛出超时异常。

import pytest, datetime @pytest.fixture(autouse=True) def check_duration(request, cache): key = "duration/" + request.node.nodeid.replace(":", "_") start_time = datetime.datetime.now() yield stop_time = datetime.datetime.now() this_duration = (stop_time - start_time).total_seconds() last_duration = cache.get(key, None) cache.set(key, this_duration) if last_duration is not None: errorstring = "test duration over 2X last duration" assert this_duration <= last_duration * 2, errorstring

  因为 fixture 设置为了 autouse,所以它不需要被测试用例引用。request 对象用来抓取键名中的 nodeid。nodeid是一个独特的标识,即便实在参数化测试中也能使用。

import pytest, datetime,time,random @pytest.fixture(autouse=True) def check_duration(request, cache): key = "duration/" + request.node.nodeid.replace(":", "_") start_time = datetime.datetime.now() yield stop_time = datetime.datetime.now() this_duration = (stop_time - start_time).total_seconds() last_duration = cache.get(key, None) cache.set(key, this_duration) if last_duration is not None: errorstring = "test duration over 2X last duration" assert this_duration <= last_duration * 2, errorstring @pytest.mark.parametrize("i",range(5)) def test_slow_stuff(i): time.sleep(random.random())

  运行之后,看看 cache 里有什么:

(venv) C:\Users\admin\Desktop\ch2>pytest -q --cache-show cachedir: C:\Users\admin\Desktop\ch2\.pytest_cache ----------------------------------- cache values for '*'----------------------------------- cache\lastfailed contains: {'test_add_variety.py': True, 'test_slower.py::test_slow_stuff[0]': True, 'test_slower.py::test_slow_stuff[1]': True, 'test_slower.py::test_slow_stuff[2]': True, 'test_slower.py::test_slow_stuff[3]': True, 'test_slower.py::test_slow_stuff[4]': True} cache\nodeids contains: ['test_api_exceptions.py::TestUpdate::test_bad_id', 'test_api_exceptions.py::TestUpdate::test_bad_task', 'test_api_exceptions.py::test_add_raises_true', 'test_config.py::test_fixtures_for_options', 'test_scope.py::test_all_hava_cities', 'test_scope.py::test_brian_in_portland', 'test_slower.py::test_slow_stuff[0]', 'test_slower.py::test_slow_stuff[1]', 'test_slower.py::test_slow_stuff[2]', 'test_slower.py::test_slow_stuff[3]', 'test_slower.py::test_slow_stuff[4]'] cache\stepwise contains: [] duration\test_slower.py__test_slow_stuff[0] contains: 0.480436 duration\test_slower.py__test_slow_stuff[1] contains: 0.769699 duration\test_slower.py__test_slow_stuff[2] contains: 0.78271 duration\test_slower.py__test_slow_stuff[3] contains: 0.380345 duration\test_slower.py__test_slow_stuff[4] contains: 0.569517 no tests ran in 0.01s

  接下来的每个测试都将读/写 cache。可以把原先的 fixture 拆分为两个小 fixture:一个作用范围是函数级别,用于测量运行时间;另一个作用范围是会话级别,用来读/写 cache。可如果这样做,就不能使用 cache fixture了,因为它的作用范围是函数级别的。

 

import pytest,datetime,time,random from collections import namedtuple Duration = namedtuple("Duration", ["current", "last"]) @pytest.fixture(scope="session") def duration_cache(request): key = "duration/testdurations" d = Duration({}, request.config.cache.get(key, {})) yield d request.config.cache.set(key, d.current) @pytest.fixture(autouse=True) def check_duration(request, duration_cache): d = duration_cache nodeid = request.node.nodeid start_time = datetime.datetime.now() yield duration = (datetime.datetime.now() - start_time).total_seconds() d.current[nodeid] = duration if d.last.get(nodeid,None) is not None: errorstring = "test duration over 2X last duration" assert duration <= (d.last[nodeid] * 2),errorstring @pytest.mark.parametrize("i",range(5)) def test_slow_stuff(i): time.sleep(random.random())

  duration_cache 的作用范围是会话级别的。在所有测试用例运行之前,它会读取之前的 cache 记录(如果没有记录,就是一个空字典)。在上面的代码中,我们把读取后的字段和一个空字典都存储在名为 Duration 的 namedtuple中,并使用 current 和 last 来访问之。之后将这个 namedtuple 传递给 check_duration,check_duration的作用范围是函数级别的。当测试用例运行时,相同的 namedtuple 被传递给每个测试用例。当前测试的运行时间被存储在 d.current 字典里。测试结束后,汇总的 current 字段被保存在 cache 里。

(venv) C:\Users\admin\Desktop\ch2>pytest -q --cache-clear test_slower_2.py ..... [100%] 5 passed in 3.32s (venv) C:\Users\admin\Desktop\ch2>pytest -q --tb=no test_slower_2.py ..... [100%] 5 passed in 2.84s (venv) C:\Users\admin\Desktop\ch2>pytest -q --cache-show cachedir: C:\Users\admin\Desktop\ch2\.pytest_cache ------------------------------------------------------------------------------------------------------------------------------- cache values for '*' ------------------------------------------------------------------------------------------------------------------------------- cache\nodeids contains: ['test_slower_2.py::test_slow_stuff[0]', 'test_slower_2.py::test_slow_stuff[1]', 'test_slower_2.py::test_slow_stuff[2]', 'test_slower_2.py::test_slow_stuff[3]', 'test_slower_2.py::test_slow_stuff[4]'] cache\stepwise contains: [] duration\testdurations contains: {'test_slower_2.py::test_slow_stuff[0]': 0.190172, 'test_slower_2.py::test_slow_stuff[1]': 0.930845, 'test_slower_2.py::test_slow_stuff[2]': 0.340309, 'test_slower_2.py::test_slow_stuff[3]': 0.652593, 'test_slower_2.py::test_slow_stuff[4]': 0.709644} no tests ran in 0.00s

4|44.4 使用 capsys

  pytest 内置的 capsys 有两个功能:允许使用代码读取 stdout 和 strerr;可以临时禁止抓取日志输出。

  假设某个函数要把欢迎信息输出到 stdout

def greeting(name): print("Hi,{}".format(name))

  这时候不能使用返回值来测试它,只能测试 stdout。可使用capsys来测试。

def greeting(name): print("Hi,{}".format(name)) def test_greeting(capsys): greeting("Earthling") out, err = capsys.readouterr() assert out == "Hi,Earthling\n" assert err == "" greeting("Brian") greeting("Nerd") out, err = capsys.readouterr() assert out == "Hi,Brian\nHi,Nerd\n" assert err == ""

  使用 strerr 的例子

import sys def yikes(problem): print("YIKES!{}".format(problem), file=sys.stderr) def test_yikes(capsys): yikes("Out of coffee!") out,err = capsys.readouterr() assert out == "" assert "Out of coffee!" in err

  pytest 通常会抓取测试用例及被测试代码的输出。仅当全部测试会话运行结束后,抓取到的输出才会随着失败的测试显示出来。-s 参数可以关闭这个功能,在测试仍在运行期间就把输出直接发送到 stdout。

  有时候就是想用 print 打印,但是又不想被捕获。这时候可以是用  capsys.disabled() 临时让输出绕过默认的输出捕获机制。

def test_capsys_disabled(capsys): with capsys.disabled(): print("\nalways print this") print("normal print, usually captured")

  运行如下:

(venv) C:\Users\admin\Desktop\ch2>pytest -q test_capsys.py always print this . [100%] 1 passed in 0.01s (venv) C:\Users\admin\Desktop\ch2>pytest -q test_capsys.py -s always print this normal print, usually captured . 1 passed in 0.01s

  正如你所看到的,不管有没有捕获输出,始终会显示 “always print this”。其他的打印正常,仅当 -s 标识的时候才会显示。(-s 表示关闭输出捕获)

  也可以使用 capsys.readouterr() 捕获。这时候 就算 -s 也不能输出。

def test_capsys_disabled(capsys): with capsys.disabled(): print("\nalways print this") print("normal print, usually captured") out, err = capsys.readouterr() assert out == "normal print, usually captured\n" def test_capsys_disabled2(capsys): print("\nalways print this") print("normal print, usually captured") out, err = capsys.readouterr() assert out == "\nalways print this\nnormal print, usually captured\n"

4|54.5 使用 monkeypatch

  monkey patch 可以在运行期间对类或模块进行动态修改,在测试中,monkey patch 常用于替换被测试代码的部分运行环境,或者将输入依赖或输出依赖替换成更容易测试的对象或函数。

  monkeypatch 提供以下函数:

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 并放在最前,sys.pathPython 导入的系统路径列表 chdir(path):改变当前的工作目录

  raising 参数用于指示 pytest 是否在记录不存在时抛出异常。setenv() 函数里的 prepend 参数可以是一个字符,如果这样设置的话,name环境变量的值就是  value + prepend + <old value>

  为了理解 monkeypatch  的实际应用方式,以下是用于生成配置文件的代码。

import os import json def read_cheese_preferences(): full_path = os.path.expanduser("~/.cheese.json") with open(full_path, "r") as f: prefs = json.load(f) return prefs def write_cheese_preferences(prefs): full_path = os.path.expanduser("~/.cheese.json") with open(full_path, "w") as f: json.dump(prefs, f, indent=4) def write_default_cheese_preferences(): write_cheese_preferences(_default_prefs) _default_prefs = { "slicing": ["manchego", "sharp cheddar"], "spreadable": ["Saint Andre", "camembert", "bucheron", "goat", "humbolt fog", "cambozola"], "salads": ["crumbled feta"] }

  write_default_cheese_preferences() 函数既不含参数,又没有返回值,那么如何测试?它在当前用户目录中编写了一个文件,我们可以利用这点从测试测试。

  一种方法是直接运行代码,检查文件的生成情况。在我们足够信任  read_cheese_preferences() 函数测试结果的前提下,可以直接把它运用到 write_default_cheese_preferences() 函数的测试里。

def test_def_prefs_full(): write_default_cheese_preferences() expected = _default_prefs actual = read_cheese_preferences() assert expected == actual

  但是有一个问题,这样测试,预设值文件会被覆盖,这样不合适。

  如果用户设置了 HOME 变量,那么 os.path.expanduser() 函数会把 ~ 替换为 HOME 环境变量的值。让我们创建一个临时目录并将 HOME 指向它。

def test_def_prefs_change_home(tmpdir, monkeypatch): monkeypatch.setenv("HOME",tmpdir.mkdir("home")) write_default_cheese_preferences() expected = _default_prefs actual = read_cheese_preferences() assert expected == actual

  看起来不错,但其中的 HOME 变量依赖于操作系统。查询 Python 官方文档,可以在 os.path.expanduser() 的介绍中找到这样一句话:“On Windows,HOME and USERPROFILE will be used if set,otherwise a combination of”。这个测试不适合 Windows。

  用 expanduser 替换 HOME 环境变量

def test_def_prefs_change_expanduser(tmpdir, monkeypatch): fake_home_dir = tmpdir.mkdir("home") monkeypatch.setattr(os.path, "expanduser", (lambda x: x.replace("~", str(fake_home_dir)))) write_default_cheese_preferences() expected = _default_prefs actual = read_cheese_preferences() assert expected == actual

  在测试中,cheese 模块中调用的 os.path.expanduser() 函数会被 lambda 表达式替换。原先该函数使用正则表达式模块的 re.sub() 函数,将 ~ 替换为我们新建的临时目录。现在已经使用了 setenv() 和 setattr() 函数来修改环境变量和属性。下面使用 setitem() 函数

  有可能文件已经存在,所有要确保当 write_default_cheese_preferences() 被调用时,文件会被默认内容覆盖。

def test_def_prefs_change_defaults(tmpdir,monkeypatch): # write the file once fake_home_dir = tmpdir.mkdir("home") monkeypatch.setattr(os.path,"expanduser",(lambda x: x.replace("~", str(fake_home_dir)))) write_default_cheese_preferences() defaults_before = copy.deepcopy(_default_prefs) # change the defaults monkeypatch.setitem(_default_prefs,"slicing",["provolone"]) monkeypatch.setitem(_default_prefs,"spreadable",["brie"]) monkeypatch.setitem(_default_prefs,"salads",["pepper jack"]) defaults_modified = _default_prefs # write it again with modified defaults write_default_cheese_preferences() # read, and check actual = read_cheese_preferences() assert defaults_modified == actual assert defaults_modified != defaults_before

  由于 _default_prefs 是字典,所有可以在测试运行时用 monkeypatch.setitem() 来修改字典中的条目。

  我们使用过 setenv(),setattr() 和 setitem() 。有关 del 的几个函数在形式上与 set 非常相似,只不过它们是用来删除环境变量、属性和字典条目。最后的两个 monkeypatch 函数是有关路径操作的。

  syspath_prepend(path) 在 sys.path 列表前加入一条路径,这可以提高你的新路径在模块搜索时的优先级。比如你可以采用这个方法,使用自定义的包、模块替换原先作用于系统范围的版本,接着使用 monkeypatch.syspatch_prepend() 函数来加入含有新版本模块的路径,这样,要测试的代码就会使用新版本的模块。

  chdir(path) 可以在测试运行时改变当前的工作目录。这对于测试命令行脚本和其他依赖于当前目录的工具都很有用。你可以设置一个临时目录,然后使用 monkeypatch.chdir(the_tmpdir)。

4|64.6 使用 doctest_namespace

  doctest 模块是 Python 标准库的一部分,借助它,可以在函数的文档字符串中放入示例代码,并通过测试确保有效。你可以使用 --doctest-modules 标识搜寻并运行 doctest 测试用例。

  在构建被标注为 autouse 的fixture时,可以使用内置的 doctest_namespace,这能够使doctest 中的测试用例在运行时识别某些作用于 pytest 命名空间的字符标识,从而增强文档字符串的可读性。

  下面的模块 unnecessary_math.py 有两个函数:multiply() 和 divide(),我们希望每个人都清楚地了解这两个函数。所以在文件和函数的文档字符传中都加入了一些使用例子:

""" This module defines multiply(a, b) and divide(a, b). >>> import unnecessary_math as um Here's how you use multiply: >>> um.multiply(4, 3) 12 >>> um.multiply('a', 3) 'aaa' Here's how you use divide: >>> um.divide(10, 5) 2.0 """ def multiply(a, b): """ Returns a multiplied by b. >>> um.multiply(4, 3) 12 >>> um.multiply("a", 3) "aaa" """ return a * b def divide(a, b): """ Returns a multiplied by b. >>> um.divide(10, 5) 2.0 """ return a / b

  unnecessary_math 名字太长了,我们决定使用 um 来代替它,所以在文档顶部使用了 import unnecessary_math as um。后面的文档字符串里的代码不包含import语句,但一直在使用 um。问题是 pytest 将每个字符串里的代码看成是不同的测试用例,顶部的 import 语句可以保证第一个例子通过,但是后面的会失败

(venv) C:\Users\admin\Desktop\ch2>pytest -v --doctest-modules --tb=short unnecessary_math.py ============================== test session starts ============================== platform win32 -- Python 3.6.8, pytest-6.0.1, py-1.9.0, pluggy-0.13.1 -- c:\users\admin\desktop\ch2\venv\scripts\python.exe cachedir: .pytest_cache rootdir: C:\Users\admin\Desktop\ch2 collected 3 items unnecessary_math.py::unnecessary_math PASSED [ 33%] unnecessary_math.py::unnecessary_math.divide FAILED [ 66%] unnecessary_math.py::unnecessary_math.multiply FAILED [100%] ============================== FAILURES ============================== ________________________________________[doctest] unnecessary_math.divide ________________________________________ 030 031 Returns a multiplied by b. 032 >>> um.divide(10, 5) UNEXPECTED EXCEPTION: NameError("name 'um' is not defined",) Traceback (most recent call last): File "C:\python36\lib\doctest.py", line 1330, in __run compileflags, 1), test.globs) File "<doctest unnecessary_math.divide[0]>", line 1, in <module> NameError: name 'um' is not defined _______________________________________________________________________________________________________________________ [doctest] unnecessary_math.multiply ________________________________________________________________________________________________________________________ 019 020 Returns a multiplied by b. 021 >>> um.multiply(4, 3) UNEXPECTED EXCEPTION: NameError("name 'um' is not defined",) Traceback (most recent call last): File "C:\python36\lib\doctest.py", line 1330, in __run compileflags, 1), test.globs) File "<doctest unnecessary_math.multiply[0]>", line 1, in <module> NameError: name 'um' is not defined C:\Users\admin\Desktop\ch2\unnecessary_math.py:21: UnexpectedException ============================== short test summary info ============================== FAILED unnecessary_math.py::unnecessary_math.divide FAILED unnecessary_math.py::unnecessary_math.multiply ============================== 2 failed, 1 passed in 0.03s ==============================

  一种解决办法是在每个文档字符串中加入 import 语句。

""" This module defines multiply(a, b) and divide(a, b). >>> import unnecessary_math as um Here's how you use multiply: >>> um.multiply(4, 3) 12 >>> um.multiply('a', 3) 'aaa' Here's how you use divide: >>> um.divide(10, 5) 2.0 """ def multiply(a, b): """ Returns a multiplied by b. >>> import unnecessary_math as um >>> um.multiply(4, 3) 12 >>> um.multiply('a', 3) 'aaa' """ return a * b def divide(a, b): """ Returns a multiplied by b. >>> import unnecessary_math as um >>> um.divide(10, 5) 2.0 """ return a / b

  但是这样做分隔了文档字符串,。

  第二种方法,是在 conftest.py 中使用内置的 doctest_namespace ,构建标记为 autouse 的 fixture,就可以解决之前的问题而且不用修改代码。

import pytest import unnecessary_math @pytest.fixture(autouse=True) def add_um(doctest_namespace): doctest_namespace["um"] = unnecessary_math

  pytest 会把 um 添加到 doctest_namespace 中,并把它作为 unnecessary_math 模块的别名。这样设置 conftest.py 之后,在 conftest.py 的作用范围内的任意一个 doctest 测试用例都可以使用um

4|74.7 使用 recwarn

  内置的 recwarn 可以用来检查待测代码产生的警告信息。在Python 里,可以添加警告信息,它们很像断言,但是并不阻止程序运行。

   例如,我们想停止支持一个原本不该发布的函数,则可以在代码里设置警告信息。

import warnings import pytest def lame_function(): warnings.warn("Please stop using this",DeprecationWarning) # rest of function

  可以用下面的测试用例来确保警告信息显示正确。

import warnings import pytest def lame_function(): warnings.warn("Please stop using this", DeprecationWarning) # rest of function def test_lame_function(recwarn): lame_function() assert len(recwarn) == 1 w = recwarn.pop() print("\nfilename", w.filename) # filename C:\Users\admin\Desktop\ch2\test_warnings.py print("\nlineno",w.lineno) # lineno 6 assert w.category == DeprecationWarning assert str(w.message) == "Please stop using this"

  recwarn 的值就像是一个警告信息列表,列表里的每个警告信息都有 4 个属性 category、message、filename、lineno,从上面的代码中可以看到。

  警告信息在测试开始后收集。如果你在意的警告信息出现在测试尾部,则可以在信息收集前使用 recwarn.clear() 清除不需要的内容。

  除了 recwarn,pytest 还可以使用 pytest.warns() 来检查警告信息。

def test_lame_function_2(): with pytest.warns(None) as warning_list: lame_function() assert len(warning_list) == 1 w = warning_list.pop() assert w.category == DeprecationWarning assert str(w.message) == "Please stop using this"

  pytest.warns() 上下文管理器可以优雅地标识哪些代码需要检查警告信息。recwarn 提供了相似的功能。可以自己选择

5|0五、配置configuration

5|15.1 理解 pytest 的配置文件

pytest.ini:pytest 的主配置文件,可以改变 pytest 的默认行为,其中有很多可配置的选项。

conftest.py:是本地的插件库,其中的hook函数和fixture将作用域该文件所在的目录以及所有子目录

__init__.py:每个测试子目录都包含该文件时,那么在多个测试目录中可以出现同名测试文件。

tox.ini:它与pytest.ini 类似,只不过是 tox 的配置文件。你可以把 pytest 的配置都写在 tox.ini里,这样就不用同时使用 tox.ini 和 pytest.ini 两个文件。

5|25.2 用 pytest --help 查看 ini文件选项

(venv) C:\Users\admin\Desktop\ch2>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. 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. filterwarnings (linelist): Each line specifies a pattern for warnings.filterwarnings. Processed after -W/--pythonwarnings. 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 ...

5|35.3 更改默认命令行选项

  如果测试的时候,经常要用到某些选项,又不想重复输入,这时可以使用 pytest.ini 文件里的 addopts 设置。下面是我自己常用的设置。

[pytest] addopts = -rsxX -l --tb=short -strict

  --rsxX 表示 pytest 报告所有测试用例被跳过、预计失败、预计失败但实际通过的原因。

  -l 表示 pytest 报告所有失败测试的堆栈中的局部变量。

  --tb=short 表示简化堆栈回溯信息,只保留文件和行数。

  --strict 选项表示禁止使用未在配置文件中注册的标记。

5|45.4 注册标记来防范拼写错误

  如果我们要标记,@pytest.mark.smoke ,但是拼错,@pytest.mark.somke , 默认情况下,这不会引起程序错误。pytest 会以为这是你创建的另一个标记。为了避免拼写错误,可以在 pytest.ini 文件里注册标记。

[pytest]markers = smoke: run the smoke test functions for tasks project get: run the test functions that test tasks.get()

  标记注册好后,可以通过 pytest --markers 来查看

 

  没有注册的标记不会出现在 --markers 列表里。如果使用了 --strict 选项,遇到拼写错误的标记或未注册的标记就会报错。

import pytest @pytest.mark.sooke def test_capsys_disabled(capsys): with capsys.disabled(): print("\nalways print this") print("normal print, usually captured") out, err = capsys.readouterr() assert out == "normal print, usually captured\n" def test_capsys_disabled2(capsys): print("\nalways print this") print("normal print, usually captured") out, err = capsys.readouterr() assert out == "\nalways print this\nnormal print, usually captured\n"

  这里 @pytest.mark.sooke 写错了。

(venv) C:\Users\admin\Desktop\ch2>pytest test_capsys.py --strict ================================ test session starts ================================ platform win32 -- Python 3.6.8, pytest-6.0.1, py-1.9.0, pluggy-0.13.1 rootdir: C:\Users\admin\Desktop\ch2, configfile: pytest.ini collected 0 items / 1 error ================================ ERRORS ================================ _______________________________ ERROR collecting test_capsys.py _______________________________ 'sooke' not found in `markers` configuration option ================================ short test summary info ================================ ERROR test_capsys.py !!!!!!!!!!!!!!!!!!!!!!!!!!!!!! Interrupted: 1 error during collection !!!!!!!!!!!!!!!!!!!!!!!!!!!!!! ================================ 1 error in 0.07s ================================

  如果你在 pytest.ini 文件里注册了标记,那么可以同时在 addopts 里加入 --strict

[pytest] addopts = -rsxX -l --tb=short -strict markers = smoke: run the smoke test functions for tasks project get: run the test functions that test tasks.get()

5|55.5 指定 pytest 的最低版本号

  minversion 选项可以指定运行测试用例的 pytest 的最低版本。例如,测试两个浮点数的值是否非常接近。比如 approx()函数,但是这个功能 直到 pytest 3.0 才出现。

  为了避免混淆,可以在使用 approx() 函数的项目中增加一行配置

[pytest] minversion = 3.0

5|65.6 指定 pytest 忽略某些目录

  pytest 执行测试搜索时,会递归遍历所有子目录,包括某些本来不想遍历的目录。

  可以使用 norecurse 选项简化 pytest 的搜索工作。norecurse 的默认设置是 .* build dist CVS_darcs {arch} 和 *.egg。因为有 .* ,所以将虚拟环境命名为 .venv 是一个好主意,所有以 . 开头的目录都不会被访问。但是,我习惯将它命名为 venv,那么需要把它加入 norecursedirs里。

[pytest] norecursedirs = .* venv src *.egg dist build

5|75.7 指定测试目录

  norecursedirs 告诉pytest 哪些路径不用访问,而 testpaths 则指示 pytest 去哪里访问。

  testpaths 是一系列相对于根目录的路径,用于限定测试用例的搜索范围。只有在pytest未指定文件目录参数或测试用例标识符时,该选项才会启用

5|85.8 更改测试搜索的规则

pytest 根据一定的规则搜索并运行测试。标准的测试搜索规则如下:

  • 从一个或多个目录开始查找。你可以在命令行指定文件名或目录名。如果未指定,则使用当前目录。
  • 在该目录和所有子目录下递归查找测试模块。
  • 测试模块是指文件名为 test_*.py 或 *_test.py 的文件。
  • 在测试模块中查找以 test_开头的函数名。
  • 查找名字以 Test开头的类。其中,首先筛选掉包含 __init_函数的类,再查找类中以 test_ 开头的类方法。

pytest_classes

  通常,pytest 的测试搜索规则是寻找以 Test*开头的测试类,而且这个类不能有 __init__() 函数。要改类搜索命名格式的话可以如下。

[pytest] python_classes = *Test Test* *Suite 这样设置后允许我们像下面这样给类取名 class DeleteSuite(): def test_delete_1(): ... def test_delete_2(): ...

python_files

  可以更改默认的测试搜索规则,而不是仅查找以 test_* 开头的文件和以 *_test 结尾的文件。

[pytest] python_files = test_* *_test check_*

python_functions

  更改测试函数和方法的搜索方式

[pytest] python_functions = test_* check_*

5|96.8 禁用XPASS

[pytest] xfail_strict = true

 


__EOF__

本文作者😎
本文链接https://www.cnblogs.com/dongye95/p/13488235.html
关于博主:评论和私信会在第一时间回复。或者直接私信我。
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
声援博主:如果您觉得文章对您有帮助,可以点击文章右下角推荐一下。您的鼓励是博主的最大动力!
posted @   dongye95  阅读(5185)  评论(0编辑  收藏  举报
编辑推荐:
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!
点击右上角即可分享
微信分享提示