• Python 自动化测试中最火的第三方开源测试框架 pytest


    一、介绍

    本篇文章是聊聊 Python 的单元测试框架,在Python 世界中最火的第三方单元测试框架:pytest。

    它有如下主要特性:

    • assert 断言失败时输出详细信息(再也不用去记忆 self.assert* 名称了)
    • 自动发现 测试模块和函数
    • 模块化夹具 用以管理各类测试资源
    • unittest 完全兼容,对 nose 基本兼容
    • 非常丰富的插件体系,有超过 315 款第三方插件,社区繁荣

    和前面介绍 unittestnose 一样,我们将从如下几个方面介绍 pytest 的特性。

    二、用例编写

    nose 一样,pytest 支持函数、测试类形式的测试用例。最大的不同点是,你可以尽情地使用 assert 语句进行断言,丝毫不用担心它会在 noseunittest 中产生的缺失详细上下文信息的问题。

    比如下面的测试示例中,故意使得 test_upper 中断言不通过:

    import pytest
    
    def test_upper():
        assert 'foo'.upper() == 'FOO1'
    
    class TestClass:
        def test_one(self):
            x = "this"
            assert "h" in x
    
        def test_two(self):
            x = "hello"
            with pytest.raises(TypeError):
                x + []
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    而当使用 pytest 去执行用例时,它会输出详细的(且是多种颜色)上下文信息:

    =================================== 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_upper ________________________________________
    
        def test_upper():
    >       assert 'foo'.upper() == 'FOO1'
    E       AssertionError: assert 'FOO' == 'FOO1'
    E         - FOO
    E         + FOO1
    E         ?    +
    
    test.py:4: AssertionError
    =========================== 1 failed, 2 passed in 0.08 seconds ============================
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    不难看到,pytest 既输出了测试代码上下文,也输出了被测变量值的信息。相比于 noseunittestpytest 允许用户使用更简单的方式编写测试用例,又能得到一个更丰富和友好的测试结果。

    三、用例发现和执行

    unittestnose 所支持的用例发现和执行能力,pytest 均支持。 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

    四、测试夹具(Fixtures)

    pytest 的测试夹具 和 unittestnosenose2的风格迥异,它不但能实现 setUptearDown这种测试前置和清理逻辑,还其他非常多强大的功能。

    4.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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

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

    4.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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    4.3 生效级别

    unittestnose 均支持测试前置和清理的生效级别:测试方法、测试类和测试模块。

    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)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    4.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()
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    在上述示例中,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
    
    • 1
    • 2
    • 3
    • 4

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

    五、跳过测试和预计失败

    pytest 除了支持 unittestnosetest 的跳过测试和预计失败的方式外,还在 pytest.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():
        ...
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

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

    六、子测试/参数化测试

    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
    
    • 1
    • 2
    • 3

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

    ========================================= 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 ==================================
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    若将参数换成 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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

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

    @pytest.mark.parametrize("x", [0, 1])
    @pytest.mark.parametrize("y", [2, 3])
    def test_foo(x, y):
        pass
    
    • 1
    • 2
    • 3
    • 4

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

    七、测试结果输出

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

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

    八、插件体系

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

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

    九、总结

    三篇关于 Python 测试框架的介绍到这里就要收尾了。写了这么多,各位看官怕也是看得累了。我们不妨罗列一个横向对比表,来总结下这些单元测试框架的异同:

    unittestnosenose2pytest
    自动发现用例
    指定(各级别)用例执行
    支持 assert 断言
    测试夹具
    测试夹具种类前置和清理前置和清理前置和清理前置、清理、内置各类 fixtures,自定义各类 fixtures
    测试夹具生效级别方法、类、模块方法、类、模块方法、类、模块方法、类、模块、包、会话
    支持跳过测试和预计失败
    子测试
    测试结果输出一般较好较好
    插件-较丰富一般丰富
    钩子--
    社区生态作为标准库,由官方维护停止维护维护中,活跃度低维护中,活跃度高

    Python 的单元测试框架看似种类繁多,实则是一代代的进化,有迹可循。抓住其特点,结合使用场景,就能容易的做出选择。

    若你不想安装或不允许第三方库,那么 unittest 是最好也是唯一的选择。反之,pytest 无疑是最佳选择,众多 Python 开源项目(如大名鼎鼎的 requests)都是使用 pytest 作为单元测试框架。甚至,连 nose2 在 官方文档上都建议大家使用 pytest,这得是多大的敬佩呀!

    『讲解开源项目系列』

    ——让对开源项目感兴趣的人不再畏惧、让开源项目的发起者不再孤单。跟着我们的文章,你会发现编程的乐趣、使用和发现参与开源项目如此简单。欢迎留言联系我们、加入我们,让更多人爱上开源、贡献开源~

    推荐Python自动化测试学习视频

    软件测试黄胖子

    软件测试面试官黄财财

  • 相关阅读:
    leetcode 34. 在排序数组中查找元素的第一个和最后一个位置(二分经典题)
    第一行代码Android 第九章9.3 解析XML格式(Pull解析方式,SAX解析方式)
    开源语言大模型演进史:早期革新
    航天科技×辰安科技 打造智慧化工园区安全保障平台
    python开发实验管理系统(lims)中的标准管理--检测项目
    Kafka收发消息核心参数详解
    Java学习之路
    ftl和html的区别?
    数仓建模,什么是宽表?如何设计?好处与不足
    后端技术盲区大清理:事务还没弄明白的小伙伴赶紧来看一看
  • 原文地址:https://blog.csdn.net/2301_78042008/article/details/133213626