zoukankan      html  css  js  c++  java
  • 《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:UsersadminDesktopch1>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:UsersadminDesktopch1
    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:UsersadminDesktopch1>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:UsersadminDesktopch1
    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:UsersadminDesktopch1,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:UsersadminDesktopch1,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:UsersadminDesktopch1>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:UsersadminDesktopch1
    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:UsersadminDesktopch1>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:UsersadminDesktopch1
    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:UsersadminDesktopch1>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:UsersadminDesktopch1
    collected 6 items / 5 deselected / 1 selected                                                                                                                                                                                                                                       
    
    tasks	est_three.py .                [100%]
    
    ================== warnings summary ==================
    tasks	est_three.py:16
      C:UsersadminDesktopch1	asks	est_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:UsersadminDesktopch1>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:UsersadminDesktopch1
    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:UsersadminDesktopch1>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:UsersadminDesktopch1
    collected 6 items                         
    
    test_one.py .                                                                                                                                                                                                                                                                 [ 16%]
    test_two.py F                                                                                                                                                                                                                                                                 [ 33%]
    tasks	est_four.py ..                                                                                                                                                                                                                                                         [ 66%]
    tasks	est_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:UsersadminDesktopch1>pytest -v
    ============================ test session starts ============================
    platform win32 -- Python 3.6.8, pytest-6.0.1, py-1.9.0, pluggy-0.13.1 -- c:usersadmindesktopch1venvscriptspython.exe
    cachedir: .pytest_cache
    rootdir: C:UsersadminDesktopch1
    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:UsersadminDesktopch1>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:UsersadminDesktopch1
    collected 2 items                                                                                                                                                                                                                                                                   
    
    tasks	est_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	est_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:UsersadminDesktopch1>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:usersadmindesktopch1venvscriptspython.exe
    cachedir: .pytest_cache
    rootdir: C:UsersadminDesktopch1
    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"

     2.5 跳过测试

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

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

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

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

    -r 选项

    show extra test summary info as specified by chars
    (f)failed 
    (E)error
    (s)skipped
    (x)failed
    (X)passed
    (p)passed
    (P)passes with output
    (a)all except pP

    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:UsersadminDesktopch2>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:usersadmindesktopch2venvscriptspython.exe
    cachedir: .pytest_cache
    rootdir: C:UsersadminDesktopch2
    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:UsersadminDesktopch2>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:usersadmindesktopch2venvscriptspython.exe
    cachedir: .pytest_cache
    rootdir: C:UsersadminDesktopch2
    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:UsersadminDesktopch2>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:usersadmindesktopch2venvscriptspython.exe
    cachedir: .pytest_cache
    rootdir: C:UsersadminDesktopch2
    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:UsersadminDesktopch2>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:usersadmindesktopch2venvscriptspython.exe
    cachedir: .pytest_cache
    rootdir: C:UsersadminDesktopch2
    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 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:UsersadminDesktopch2>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:UsersadminDesktopch2
    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:UsersadminDesktopch2>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:UsersadminDesktopch2
    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:UsersadminDesktopch2>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:UsersadminDesktopch2
    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("
    test 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:UsersadminAppDataLocalTemppytest-of-adminpytest-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:UsersadminDesktopch2>pytest -s -q test_config.py::test_option
    "foo" set to: bar
    "myopt" set to: False
    .
    1 passed in 0.01s
    ——————————————————————————————————
    
    (venv) C:UsersadminDesktopch2>pytest -s -q --myopt test_config.py::test_option
    "foo" set to: bar
    "myopt" set to: True
    .
    1 passed in 0.01s
    ——————————————————————————————————
    
    (venv) C:UsersadminDesktopch2>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:UsersadminDesktopch2>pytest -q --cache-show
    cachedir: C:UsersadminDesktopch2.pytest_cache
    ----------------------------------- cache values for '*'-----------------------------------
    cachelastfailed 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
    odeids 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]']
    cachestepwise contains:
      []
    duration	est_slower.py__test_slow_stuff[0] contains:
      0.480436
    duration	est_slower.py__test_slow_stuff[1] contains:
      0.769699
    duration	est_slower.py__test_slow_stuff[2] contains:
      0.78271
    duration	est_slower.py__test_slow_stuff[3] contains:
      0.380345
    duration	est_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:UsersadminDesktopch2>pytest -q --cache-clear test_slower_2.py
    .....                                                                                                                                                                                                                                                                         [100%]
    5 passed in 3.32s
    
    (venv) C:UsersadminDesktopch2>pytest -q --tb=no test_slower_2.py
    .....                                                                                                                                                                                                                                                                         [100%]
    5 passed in 2.84s
    
    (venv) C:UsersadminDesktopch2>pytest -q --cache-show
    cachedir: C:UsersadminDesktopch2.pytest_cache
    ------------------------------------------------------------------------------------------------------------------------------- cache values for '*' -------------------------------------------------------------------------------------------------------------------------------
    cache
    odeids 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]']
    cachestepwise contains:
      []
    duration	estdurations 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
    "
        assert err == ""
    
        greeting("Brian")
        greeting("Nerd")
        out, err = capsys.readouterr()
        assert out == "Hi,Brian
    Hi,Nerd
    "
        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("
    always print this")
        print("normal print, usually captured")

      运行如下:

    (venv) C:UsersadminDesktopch2>pytest -q test_capsys.py
    
    always print this
    .                                                                                                                                                                                                                                                                            [100%]
    1 passed in 0.01s
    
    (venv) C:UsersadminDesktopch2>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("
    always print this")
        print("normal print, usually captured")
        out, err = capsys.readouterr()
        assert out == "normal print, usually captured
    "
    
    def test_capsys_disabled2(capsys):
        print("
    always print this")
        print("normal print, usually captured")
        out, err = capsys.readouterr()
        assert out == "
    always print this
    normal print, usually captured
    "

    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:UsersadminDesktopch2>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:usersadmindesktopch2venvscriptspython.exe
    cachedir: .pytest_cache
    rootdir: C:UsersadminDesktopch2
    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:python36libdoctest.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:python36libdoctest.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:UsersadminDesktopch2unnecessary_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("
    filename", w.filename)         # filename C:UsersadminDesktopch2	est_warnings.py
        print("
    lineno",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:UsersadminDesktopch2>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("
    always print this")
        print("normal print, usually captured")
        out, err = capsys.readouterr()
        assert out == "normal print, usually captured
    "
    
    def test_capsys_disabled2(capsys):
        print("
    always print this")
        print("normal print, usually captured")
        out, err = capsys.readouterr()
        assert out == "
    always print this
    normal print, usually captured
    "

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

    (venv) C:UsersadminDesktopch2>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:UsersadminDesktopch2, 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未指定文件目录参数或测试用例标识符时,该选项才会启用

  • 相关阅读:
    JDK的几种分析工具
    心理价值
    通过Proxool辅助数据库优化
    人生缄言
    grep 用法
    多服务器快速定位
    RandomAccessFile读取远程系统日志
    20101116 视频处理几个常用指令
    Flickr架构
    JAVA正则表达式语法
  • 原文地址:https://www.cnblogs.com/dongye95/p/13488235.html
Copyright © 2011-2022 走看看