《pytest测试实战》-- Brian Okken

一、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.1 资源获取

  pytest的官方文档地址

https://docs.pytest.org

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

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

  建议使用vritualenv来使用

1.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.3 运行单个测试用例

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

pytest -v tasks/test_four.py::test_asdict

1.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.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.2 使用 assert 声明

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

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

2.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.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.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.6 标记预期会失败的测试

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

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

2.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 参数化测试

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

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 != ""

 

三、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.1 通过 conftest.py 共享 fixture

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

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

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

3.2 使用 fixture 执行配置及销毁逻辑

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

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

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

3.3 使用 --setup-show 回溯 fixture 的执行过程

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

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

pytest --setup-show test_add.py

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

3.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.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.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.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.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.9 为 fixture 重命名

@pytest.fixture(name="another")

3.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()

四、内置 fixture 

4.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.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.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.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.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.path 是 Python 导入的系统路径列表

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.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.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 提供了相似的功能。可以自己选择

五、配置configuration

5.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.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.3 更改默认命令行选项

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

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

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

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

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

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

5.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.5 指定 pytest 的最低版本号

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

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

[pytest]
minversion = 3.0

5.6 指定 pytest 忽略某些目录

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

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

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

5.7 指定测试目录

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

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

5.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_*

6.8 禁用XPASS

[pytest]
xfail_strict = true

 

posted @ 2020-08-12 00:19  dongye95  阅读(5151)  评论(0编辑  收藏  举报