跳转至

Pytest使用

1.什么是Pytest

pytest是一个强大的Python测试工具,它可以用于所有类型和级别的软件测试。 Pytest可以被开发团队,QA团队,独立测试小组,实践TDD的个人和开放源代码项目。实际上,整个互联网上的项目都是从unittest或者nose转向pytest,包括Mozilla和Dropbox。为什么?因为pytest提供 强大的功能,如'断言'重写,第三方插件模型,强大但简单的fixture模型。

pytest是软件测试框架,这意味着pytest是命令行工具。它会自动找到你写的测试,运行测试并报告结果。可编写插件或安装第三方来扩展插件。它可以用来测试Python发行版。它很容易与其他工具对接,如持续集成和网页自动化

2.Pytest 特点

Pytest脱颖而出的原因:

  • 简单
  • 易读
  • 用assert来测试失败,而不是self.assertEqual() 或者self.assertLessThan()
  • 可运行unittest或nose测试。

事实上很多自动化测试平台,底层就是用python驱动的。它们用flask或django等提供友好的页面展示,但是核心层还是在pytest和一些测试库的开发。

3.快速入门

3.1 用例编写

import pytest

def test():
    assert (1,2,3) == (1,2,3)


if __name__ == "__main__":
    pytest.main()

执行结果如下:

[root@yuuriso:/home/git_code/pytest]# /usr/bin/python3 /home/git_code/pytest/test_func.py
=========================================================================================test session starts =========================================================================================
platform linux -- Python 3.6.9, pytest-5.4.2, py-1.8.1, pluggy-0.13.1
rootdir: /home/git_code/pytest
collected 1 item                                                                                                                                                                                                                                        

test_func.py .                                                                                                                                                                                                                                    [100%]

========================================================================================= 1 passed in 0.18s =========================================================================================

点表示一个测试运行并通过。 如果你需要更多信息,您可以使用-v或--verbose

[root@yuuriso:/home/git_code/pytest]# /usr/bin/python3 /home/git_code/pytest/test_func.py -v
========================================================================================= test session starts =========================================================================================
platform linux -- Python 3.6.9, pytest-5.4.2, py-1.8.1, pluggy-0.13.1 -- /usr/bin/python3
cachedir: .pytest_cache
rootdir: /home/git_code/pytest
collected 1 item                                                                                                                                                                                                                                        

test_func.py::test PASSED                                                                                                                                                                                                                         [100%]

========================================================================================= 1 passed in 0.10s =========================================================================================

在彩色终端,PASSED和底线是绿色的。

[root@yuuriso:/home/git_code/pytest]# /usr/bin/python3 /home/git_code/pytest/test_func.py
========================================================================================= test session starts =========================================================================================
platform linux -- Python 3.6.9, pytest-5.4.2, py-1.8.1, pluggy-0.13.1
rootdir: /home/git_code/pytest
collected 1 item                                                                                                                                                                                                                                        

test_func.py F                                                                                                                                                                                                                                    [100%]

=========================================================================================FAILURES =========================================================================================
_________________________________________________________________________________________ test _________________________________________________________________________________________

    def test():
>       assert (1,2,4) == (1,2,3)
E       assert (1, 2, 4) == (1, 2, 3)
E         At index 2 diff: 4 != 3
E         Use -v to get the full diff

test_func.py:4: AssertionError
========================================================================================= short test summary info =========================================================================================
FAILED test_func.py::test - assert (1, 2, 4) == (1, 2, 3)
=========================================================================================1 failed in 0.35s =========================================================================================
  • pytest 支持用例自动(递归)发现:

默认发现当前目录下所有符合test_*.py*_test.py的测试用例文件中,以test开头的测试函数或以 Test 开头的测试类中的以test 开头的测试方法使用 pytest命令同 nose2 的理念一样,通过在配置文件中指定特定参数,可配置用例文件、类和函数的名称模式(模糊匹配)

  • 通过在配置文件中指定特定参数,可配置用例文件、类和函数的名称模式(模糊匹配)

pytest 也支持执行指定用例:

  • 指定测试文件路径
  • pytest /path/to/test/file.py
  • 指定测试类
  • pytest /path/to/test/file.py:TestCase
  • 指定测试方法
  • pytest another.test::TestClass::test_method
  • 指定测试函数
  • pytest /path/to/test/file.py:test_function

3.2 测试case 规则

发现规则小结

  • 测试文件应该命名为test_.py或_test.py
  • 测试方法和函数应该被命名为test_。
  • 测试类应该被命名为Test

结果类型:

以下是测试功能的可能结果:

  • PASSED (.):测试成功。
  • FAILED (F):测试失败(或XPASS + strict)。
  • SKIPPED (s): 测试被跳过。 可以使用@pytest.mark.skip()或 pytest.mark.skipif()修饰器告诉pytest跳过测试
  • xfail (x):预期测试失败。@pytest.mark.xfail()
  • XPASS (X):测试不应该通过。
  • ERROR (E):错误

3.3 更多选项

  • marker 标签
  • -x, –exitfirst 失败后停止执行
  • 设置捕捉
  • --lf, --last-failed 执行上次失败的测试 多在--tb  之后使用
  • --ff / --failed-first则会先执行失败的,然后执行成功的。
  • -q 静默模式 -q, --quiet decrease verbosity.
  • -l 在traceback中显示本地变量 --showlocals 在traceback中显示本地变量
  • –durations=N

4.进阶使用

4.1 Pytest Fixture

4.1.1 声明和使用

pytest 中的测试夹具更像是测试资源,你只需定义一个夹具,然后就可以在用例中直接使用它。得益于 pytest 的依赖注入机制,你无需通过from xx import xx的形式显示导入,只需要在测试函数的参数中指定同名参数即可,比如:

import pytest


@pytest.fixture
def smtp_connection():
    import smtplib

    return smtplib.SMTP("smtp.gmail.com", 587, timeout=5)


def test_ehlo(smtp_connection):
    response, msg = smtp_connection.ehlo()
    assert response == 250

上述示例中定义了一个测试夹具 smtp_connection,在测试函数 test_ehlo 签名中定义了同名参数,则 pytest 框架会自动注入该变量。

4.1.2 共享

pytest 中,同一个测试夹具可被多个测试文件中的多个测试用例共享。只需在包(Package)中定义 conftest.py 文件,并把测试夹具的定义写在该文件中,则该包内所有模块(Module)的所有测试用例均可使用 conftest.py 中所定义的测试夹具。

比如,如果在如下文件结构的 test_1/conftest.py 定义了测试夹具,那么 test_a.pytest_b.py 可以使用该测试夹具;而 test_c.py 则无法使用。

`-- test_1
|   |-- conftest.py
|   `-- test_a.py
|   `-- test_b.py
`-- test_2
    `-- test_c.py

4.1.3 生效级别

pytest 的测试夹具支持测试前置和清理的生效级别:测试方法、测试类和测试模块。。通过在 pytest.fixture 中指定 scope 参数来设置:

  • function —— 函数级,即调用每个测试函数前,均会重新生成 fixture
  • class —— 类级,调用每个测试类前,均会重新生成 fixture
  • module —— 模块级,载入每个测试模块前,均会重新生成 fixture
  • package —— 包级,载入每个包前,均会重新生成 fixture
  • session —— 会话级,运行所有用例前,只生成一次 fixture

当我们指定生效级别为模块级时,示例如下:

import pytest
import smtplib


@pytest.fixture(scope="module")
def smtp_connection():
    return smtplib.SMTP("smtp.gmail.com", 587, timeout=5)

4.1.4 测试前置和清理

pytest 的测试夹具也能够实现测试前置和清理,通过 yield 语句来拆分这两个逻辑,写法变得很简单,如:

import smtplib
import pytest


@pytest.fixture(scope="module")
def smtp_connection():
    smtp_connection = smtplib.SMTP("smtp.gmail.com", 587, timeout=5)
    yield smtp_connection  # provide the fixture value
    print("teardown smtp")
    smtp_connection.close()复制代码

在上述示例中,yield smtp_connection 及前面的语句相当于测试前置,通过 yield 返回准备好的测试资源 smtp_connection; 而后面的语句则会在用例执行结束(确切的说是测试夹具的生效级别的声明周期结束时)后执行,相当于测试清理。

如果生成测试资源(如示例中的 smtp_connection)的过程支持 with 语句,那么还可以写成更加简单的形式:

@pytest.fixture(scope="module")
def smtp_connection():
    with smtplib.SMTP("smtp.gmail.com", 587, timeout=5) as smtp_connection:
        yield smtp_connection  # provide the fixture value复制代码

pytest 的测试夹具除了文中介绍到的这些功能,还有诸如参数化夹具工厂夹具在夹具中使用夹具等更多高阶玩法,详情请阅读 "pytest fixtures: explicit, modular, scalable"

4.2 跳过测试和预计失败

pytestpytest.mark 中提供对应方法:

  • 通过 skip 装饰器或 pytest.skip 函数直接跳过测试
  • 通过 skipif按条件跳过测试
  • 通过 xfail 预计测试失败

示例如下:

@pytest.mark.skip(reason="no way of currently testing this")
def test_mark_skip():
    ...

def test_skip():
    if not valid_config():
        pytest.skip("unsupported configuration")

@pytest.mark.skipif(sys.version_info < (3, 6), reason="requires python3.6 or higher")
def test_mark_skip_if():
    ...

@pytest.mark.xfail
def test_mark_xfail():
    ...复制代码

关于跳过测试和预计失败的更多玩法,参见 "Skip and xfail: dealing with tests that cannot succeed"

4.3 子测试/参数化测试

pytest 除了支持 unittest 中的 TestCase.subTest,还支持一种更为灵活的子测试编写方式,也就是 参数化测试,通过 pytest.mark.parametrize 装饰器实现。

在下面的示例中,定义一个 test_eval 测试函数,通过 pytest.mark.parametrize 装饰器指定 3 组参数,则将生成 3 个子测试:

@pytest.mark.parametrize("test_input,expected", [("3+5", 8), ("2+4", 6), ("6*9", 42)])
def test_eval(test_input, expected):
    assert eval(test_input) == expected复制代码

示例中故意让最后一组参数导致失败,运行用例可以看到丰富的测试结果输出:

========================================= test session starts =========================================
platform darwin -- Python 3.7.1, pytest-4.0.1, py-1.7.0, pluggy-0.8.0
rootdir: /Users/prodesire/projects/tests, inifile:
plugins: cov-2.6.0
collected 3 items

test.py ..F                                                                                     [100%]

============================================== FAILURES ===============================================
__________________________________________ test_eval[6*9-42] __________________________________________

test_input = '6*9', expected = 42

    @pytest.mark.parametrize("test_input,expected", [("3+5", 8), ("2+4", 6), ("6*9", 42)])
    def test_eval(test_input, expected):
>       assert eval(test_input) == expected
E       AssertionError: assert 54 == 42
E        +  where 54 = eval('6*9')

test.py:6: AssertionError
================================= 1 failed, 2 passed in 0.09 seconds ==================================复制代码

若将参数换成 pytest.param,我们还可以有更高阶的玩法,比如知道最后一组参数是失败的,所以将它标记为 xfail:

@pytest.mark.parametrize(
    "test_input,expected",
    [("3+5", 8), ("2+4", 6), pytest.param("6*9", 42, marks=pytest.mark.xfail)],
)
def test_eval(test_input, expected):
    assert eval(test_input) == expected复制代码

如果测试函数的多个参数的值希望互相排列组合,我们可以这么写:

@pytest.mark.parametrize("x", [0, 1])
@pytest.mark.parametrize("y", [2, 3])
def test_foo(x, y):
    pass复制代码

上述示例中会分别把 x=0/y=2x=1/y=2x=0/y=3x=1/y=3带入测试函数,视作四个测试用例来执行。

4.4 测试结果输出

pytest 的测试结果输出相比于 unittestnose 来说更为丰富,其优势在于:

  • 高亮输出,通过或不通过会用不同的颜色进行区分
  • 更丰富的上下文信息,自动输出代码上下文和变量信息
  • 测试进度展示
  • 测试结果输出布局更加友好易读

4.5 Pytest 插件

pytest插件十分丰富,而且即插即用,作为使用者不需要编写额外代码。关于插件的使用,参见"Installing and Using plugins"

此外,得益于 pytest 良好的架构设计和钩子机制,其插件编写也变得容易上手。关于插件的编写,参见"Writing plugins"

5.参考资料

[1].https://docs.pytest.org/en/latest/getting-started.html (Pyest 官方文档)

[2]. https://china-testing.github.io/python_pytest_testing.html (Pytest 测试教程)

[3].https://juejin.im/post/5d896c31518825095103467c

[4].https://www.jianshu.com/p/54b0f4016300 (Pytest Fixtures)