• Python单元测试-pytest


    前言🥁

    本文从两个部分总结pytest库的用法和使用场景

    构造测试用例 🏀

    如何构造🤔

    pytest在test*.py 或者 *test.py 文件中; 寻找以 test开头或结尾的函数,以Test开头的类里面的 以 test开头或结尾的方法,将这些作为测试用例。
    所以需要满足以下

    • 1.文件名以 test开头或结尾;

    • 2.函数/方法以test开头;

    • 3.class类名以Test开头, 且不能有__init__方法

      ps: 基于PEP8规定一般我们的测试用例都是 test_xx, 类则以Test_; 即带下划线的,但是要注意的是不要下划线也是可以运行的!

    如何运行🥱

    • pytest.main() 作为测试用例的入口
      • 当前目录
        pytest.main(["./"])
        
        • 1
      • 指定目录/模块
        pytest.main(["./sub_dir"]) # 运行sub_dir目录
        pytest.main(["./sub_dir/test_xx.py"]) # 运行指定模块
        
        • 1
        • 2
      • 指定用例
        pytest.main(["./sub_dir/test_xx.py::test_func"]) # 运行指定函数用例
        pytest.main(["./sub_dir/test_xx.py::TestClass::test_method"]) # 运行指定类用例下的方法
        
        • 1
        • 2
      • 参数
        其实和pytest命令行参数一样,只是将参数按照顺序放到列表中传参给main函数
    • 命令行
      (参下 - 以下用例都使用的命令行)

    简单目录结构示例🕸

    • 目录树
      tests
      ├── test_class.py

      class TestClass:
      	def test_pass():
      		assert 1
      	
      	def test_faild():
      		assert 0
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6

      └── test_func.py

      def test_pass():
      	assert 1
      
      def test_faild():
      	assert 0
      
      • 1
      • 2
      • 3
      • 4
      • 5

      项目目录下会建立一个tests目录,里面存放单元测试用例,如上所示 ,两个文件 test_class.py是测试类,test_func.py是测试函数
      在如上目录下运行pytest, pytest会在当前目录及递归子目录下按照上述的规则运行测试用例;
      如果只是想收集测试用例,查看收集到哪些测试用例可以查看--collect-only 命令选项

      [python -m] pytest --collect-only
      
      • 1

      输出, 可以看到collected 3 items 即该命令收集到3个测试用例

      ============================================================= test session starts ==============================================================
      platform darwin -- Python 3.7.3, pytest-4.6.5, py-1.11.0, pluggy-0.13.1
      rootdir: /Users/huangxiaonan/PycharmProjects/future
      plugins: anyio-3.5.0
      collected 3 items                                                                                                                              
      
        
            
            
      
        
      
      ========================================================= no tests ran in 0.15 seconds =========================================================
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13

    基础用法🐢

    使用断言🤨

    测试用例基础工具assert

    • 自定义断言
      contest.py自定义pytest_assertrepr_compare该函数有三个参数
      • op
      • left
      • right
      class Foo:
      def __init__(self, val):
          self.val = val
      
      def __eq__(self, other):
          return self.val == other.val
      
      
      def pytest_assertrepr_compare(op, left, right):
          if isinstance(left, Foo) and isinstance(right, Foo) and op == "==":
              return [
                  "Comparing Foo instances:",
                  "   vals: {} != {}".format(left.val, right.val),
              ]
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      test_assert.py
      from conftest import Foo
      
      
      def test_compare():
          f1 = Foo(1)
          f2 = Foo(2)
          assert f1 == f2
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      输出
      ============================================================= test session starts ==============================================================
      platform darwin -- Python 3.7.3, pytest-4.6.5, py-1.11.0, pluggy-0.13.1 -- /Library/Developer/CommandLineTools/usr/bin/python3
      cachedir: .pytest_cache
      rootdir: /Users/PycharmProjects/future/tests, inifile: pytest.ini
      plugins: anyio-3.5.0
      collected 1 item                                                                                                                               
      
      tests/test_assert.py::test_compare FAILED
      
      =================================================================== FAILURES ===================================================================
      _________________________________________________________________ test_compare _________________________________________________________________
      
          def test_compare():
              f1 = Foo(1)
              f2 = Foo(2)
      >       assert f1 == f2
      E       assert Comparing Foo instances:
      E            vals: 1 != 2
      
      tests/test_assert.py:7: AssertionError
      =========================================================== 1 failed in 0.05 seconds ===========================================================	
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21

    捕获异常🧐

    • 使用pytest.raises()捕获指定异常,以确定异常发生
      import pytest
      
      
      def test_raises():
          with pytest.raises(TypeError) as e:
              raise TypeError("TypeError")
          exec_msg = e.value.args[0]
          assert exec_msg == 'TypeError'
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8

    指定运行测试用例🐗

    • 命令行:: 显示指定
      pytest test__xx.py::test_func
      
      • 1
    • 命令行-k模糊匹配
      pytest -k a # 调用所有带a字符的测试用例
      
      • 1
    • pytest.mark.自定义标记 装饰器做标记
      • test_mark.py
        @pytest.mark.finished
        def test_func1():
            assert 1 == 1
        
        @pytest.mark.unfinished
        def test_func2():
            assert 1 != 1
        
        @pytest.mark.success
        @pytest.finished
        def test_func3():
            assert 1 == 1
        
        
        • 1
        • 2
        • 3
        • 4
        • 5
        • 6
        • 7
        • 8
        • 9
        • 10
        • 11
        • 12
        • 13
      • 注册标记到配置
        • 方式1: conftest.py
          def pytest_configure(config):
              marker_list = ["finished", "unfinished", "success"]  # 标签名集合
              for markers in marker_list:
                  config.addinivalue_line("markers", markers)
          
          • 1
          • 2
          • 3
          • 4
        • 方式2: pytest.ini
          [pytest]
          markers=
              finished: finish
              error: error
              unfinished: unfinished
          
          • 1
          • 2
          • 3
          • 4
          • 5
        ps: 如果不注册也可以运行,但是会有 warnning
      • 运行方式注意: 标签要用双引号
        • 运行带 finished标签的用例
          pytest -m "finished" # 
          
          • 1
        • 多选 运行 test_fun1 test_fun2
          pytest -m "finished or unfinished" 
          
          • 1
        • 多标签用例运行 test_fun3
          pytest -m "finished and success""
          
          • 1

    跳过测试用例 SKIPPED💦

    装饰测试函数或者测试类

    • pytest.mark.skip(reason="beause xxx") 直接跳过
    • pytest.mark.skipif(a>=1, reason="当a>=1执行该测试用例") 满足条件跳过
      • code
        import pytest
        
        
        @pytest.mark.skip(reason="no reason, skip")
        class TestB:
        
            def test_a(self):
                print("------->B test_a")
                assert 1
        
            def test_b(self):
                print("------->B test_b")
        
        @pytest.mark.skipif(a>=1, reason="当a>=1执行该测试用例")
        def test_func2():
            assert 1 != 1
        
        • 1
        • 2
        • 3
        • 4
        • 5
        • 6
        • 7
        • 8
        • 9
        • 10
        • 11
        • 12
        • 13
        • 14
        • 15
        • 16
      • 输出 (运行 pytest tests/test_mark.py -v)
        ============================================================= test session starts ==============================================================
        platform darwin -- Python 3.7.3, pytest-4.6.5, py-1.11.0, pluggy-0.13.1 -- /Library/Developer/CommandLineTools/usr/bin/python3
        cachedir: .pytest_cache
        rootdir: /Users/huangxiaonan/PycharmProjects/future/tests, inifile: pytest.ini
        plugins: anyio-3.5.0
        collected 3 items                                                                                                                              
        
        tests/test_mark.py::TestB::test_a SKIPPED                                                                                                [ 33%]
        tests/test_mark.py::TestB::test_b SKIPPED                                                                                                [ 66%]
        tests/test_mark.py::test_func2 SKIPPED                                                                                                   [100%]
        
        ========================================================== 3 skipped in 0.02 seconds ===========================================================
        
        • 1
        • 2
        • 3
        • 4
        • 5
        • 6
        • 7
        • 8
        • 9
        • 10
        • 11
        • 12

    预见的错误 XPASS😅

    可预见的错误,不想skip, 比如某个测试用例是未来式的(版本升级后)

    • pytest.mark.xfail(version < 2, reason="no supported until version==2")

    参数化💡

    • pytest.mark.parametrize(argnames, argvalues)
    • code
      @pytest.mark.parametrize('passwd',
                            ['123456',
                             'abcdefdfs',
                             'as52345fasdf4'])
      def test_passwd_length(passwd):
          assert len(passwd) >= 8
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
    • 输出
      ============================================================= test session starts ==============================================================
      platform darwin -- Python 3.7.3, pytest-4.6.5, py-1.11.0, pluggy-0.13.1 -- /Library/Developer/CommandLineTools/usr/bin/python3
      cachedir: .pytest_cache
      rootdir: /Users/huangxiaonan/PycharmProjects/future/tests, inifile: pytest.ini
      plugins: anyio-3.5.0
      collected 3 items                                                                                                                              
      
      tests/test_params.py::test_passwd_length[123456] FAILED                                                                                  [ 33%]
      tests/test_params.py::test_passwd_length[abcdefdfs] PASSED                                                                               [ 66%]
      tests/test_params.py::test_passwd_length[as52345fasdf4] PASSED                                                                           [100%]
      
      =================================================================== FAILURES ===================================================================
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      多参数情况
      @pytest.mark.parametrize('user, passwd', [('jack', 'abcdefgh'), ('tom', 'a123456a')])

    Fixture⚙️

    常见的就是数据库的初始连接和最后关闭操作

    简单范例🔔

    • code
      @pytest.fixture()
      def postcode():
          return '010'
      
      
      def test_postcode(postcode):
          assert postcode == '010'
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      在生产环境中一般定义在conftest.py集中管理;
      可以看到如上的范例中定义了一个 fixture 名称以被pytest.fixture()装饰的函数此处为 postcode , 如果想要使用它,需要给测试用例添加同名形参;也可以自定义固件名称pytest.fixture(name="code")此时 fixture的名称为code.

    预处理和后处理🙈

    yield分割,预处理在yield之前,后处理在yield之后

    • code
      import pytest
      
      
      @pytest.fixture(scope="module")
      def db_conn():
          print(f"mysql conn")
          conn = None
          yield conn
          print(f"mysql close")
          del conn
      
      
      def test_postcode(db_conn):
          print("db_conn = ", db_conn)
          assert db_conn == None
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15

    fixture作用域🥅

    scope可接受如下参数

    • function: 函数级(默认),每个测试函数都会执行一次固件;
    • class: 类级别,每个测试类执行一次,所有方法都可以使用;
    • module: 模块级,每个模块执行一次,模块内函数和方法都可使用;
    • session: 会话级,一次测试只执行一次,所有被找到的函数和方法都可用。

    pytest.mark.usefixtures🔗

    装饰类或者函数用例,完成一些用例的预处理和后处理工作

    • code
      import pytest
      
      
      @pytest.fixture(scope="module")
      def db_conn():
         print(f"mysql conn")
         conn = None
         yield conn
         print(f"mysql close")
         del conn
      
      
      @pytest.fixture(scope="module")
      def auth():
         print(f"login")
         user = None
         yield user
         print(f"logout")
         del user
      
      
      @pytest.mark.usefixtures("db_conn", "auth")
      class TestA:
         def test_a(self):
             assert 1
      
         def test_b(self):
             assert 1
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
      • 23
      • 24
      • 25
      • 26
      • 27
      • 28
      执行python3 -m pytest tests/test_fixture.py -vs
      以上两个fixture将以 db_conn -> auth 的顺序(即位置参数的先后顺序)在用例TestA构建作用域
      另外:也可以多个以多个pytest.mark.usefixtures的方式构建,此时靠近被装饰对象的fixture优先
    • 输出
      ============================================================= test session starts ==============================================================
      platform darwin -- Python 3.7.3, pytest-4.6.5, py-1.11.0, pluggy-0.13.1 -- /Library/Developer/CommandLineTools/usr/bin/python3
      cachedir: .pytest_cache
      rootdir: /Users/PycharmProjects/future/tests, inifile: pytest.ini
      plugins: anyio-3.5.0
      collected 2 items                                                                                                                              
      
      tests/test_fixture.py::TestA::test_a mysql conn
      login
      PASSED
      tests/test_fixture.py::TestA::test_b PASSEDlogout
      mysql close
      
      
      =========================================================== 2 passed in 0.06 seconds ===========================================================
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15

    fixture自动化🐝

    fixture参数autouse=True, 则fixture将自动执行
    新建一个conftest.py文件,添加如下代码,以下代码是官方给出的demo,计算 session 作用域(总测试),及 function 作用域(每个函数/方法)的运行时间

    • code
      import time
      import pytest
      
      
      DATE_FORMAT = '%Y-%m-%d %H:%M:%S'
      
      
      @pytest.fixture(scope='session', autouse=True)
      def timer_session_scope():
         start = time.time()
         print('\nstart: {}'.format(time.strftime(DATE_FORMAT, time.localtime(start))))
      
         yield
      
         finished = time.time()
         print('finished: {}'.format(time.strftime(DATE_FORMAT, time.localtime(finished))))
         print('Total time cost: {:.3f}s'.format(finished - start))
      
      
      @pytest.fixture(autouse=True)
      def timer_function_scope():
         start = time.time()
         yield
         print(' Time cost: {:.3f}s'.format(time.time() - start))
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
      • 23
      • 24
      当执行conftest.py当前目录及递归子目录下的所有用例时将自动执行以上fixture

    fixture参数化🔧

    区别于参数化测试, 这部分主要是对固件进行参数化,比如连接两个不同的数据库
    固件参数化需要使用 pytest 内置的固件 request,并通过 request.param 获取参数。

    • code
      import pytest
      
      
      @pytest.fixture(params=[
          ('redis', '6379'),
          ('mysql', '3306')
      ])
      def param(request):
          return request.param
      
      
      @pytest.fixture(autouse=True)
      def db(param):
          print('\nSucceed to connect %s:%s' % param)
      
          yield
      
          print('\nSucceed to close %s:%s' % param)
      
      
      def test_api():
          assert 1 == 1
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
    • 输出
      ============================================================= test session starts ==============================================================
      platform darwin -- Python 3.7.3, pytest-4.6.5, py-1.11.0, pluggy-0.13.1 -- /Library/Developer/CommandLineTools/usr/bin/python3
      cachedir: .pytest_cache
      rootdir: /Users/PycharmProjects/future/tests, inifile: pytest.ini
      plugins: anyio-3.5.0
      collected 2 items                                                                                                                              
      
      tests/test_fixture.py::test_api[param0] 
      Succeed to connect redis:6379
      PASSED
      Succeed to close redis:6379
      
      tests/test_fixture.py::test_api[param1] 
      Succeed to connect elasticsearch:9200
      PASSED
      Succeed to close elasticsearch:9200
      
      
      =========================================================== 2 passed in 0.01 seconds ===========================================================
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19

    内置fixture🪛

    • tmpdir
      作用域 function
      用于临时文件和目录管理,默认会在测试结束时删除
      tmpdir.mkdir() 创建目临时目录返回创建的目录句柄,tmpdir.join() 创建临时文件返回创建的文件句柄

      • code
        def test_tmpdir(tmpdir):
            a_dir = tmpdir.mkdir('mytmpdir')
            a_file = a_dir.join('tmpfile.txt')
        
            a_file.write('hello, pytest!')
        
            assert a_file.read() == 'hello, pytest!'
        
        • 1
        • 2
        • 3
        • 4
        • 5
        • 6
        • 7
    • tmpdir_factory
      作用于所有作用域

      • code
        @pytest.fixture(scope='module')
        def my_tmpdir_factory(tmpdir_factory):
            a_dir = tmpdir_factory.mktemp('mytmpdir')
            a_file = a_dir.join('tmpfile.txt')
        
            a_file.write('hello, pytest!')
        
            return a_file
        
        • 1
        • 2
        • 3
        • 4
        • 5
        • 6
        • 7
        • 8
    • pytestconfig
      读取命令行参数和配置文件; 等同于request.config

      • conftest.py定义 pytest_addoption用于添加命令行参数
        def pytest_addoption(parser):
            parser.addoption('--host', action='store',
                             help='host of db')
            parser.addoption('--port', action='store', default='8888',
                             help='port of db')
        
        • 1
        • 2
        • 3
        • 4
        • 5
      • test_config.py 定义
        def test_option1(pytestconfig):
            print('host: %s' % pytestconfig.getoption('host'))
            print('port: %s' % pytestconfig.getoption('port'))
        
        
        • 1
        • 2
        • 3
        • 4
    • capsys
      临时关闭标准输出stdoutstderr

      • code
        import sys
        
        
        def test_stdout(capsys):
            sys.stdout.write("stdout>>")
            sys.stderr.write("stderr>>")
        
            out, err = capsys.readouterr()
        
            print(f"capsys stdout={out}")
            print(f"capsys stderr={err}")
        
        • 1
        • 2
        • 3
        • 4
        • 5
        • 6
        • 7
        • 8
        • 9
        • 10
        • 11
      • 输出
        ============================================================= test session starts ==============================================================
        platform darwin -- Python 3.7.3, pytest-4.6.5, py-1.11.0, pluggy-0.13.1 -- /Library/Developer/CommandLineTools/usr/bin/python3
        cachedir: .pytest_cache
        rootdir: /Users/huangxiaonan/PycharmProjects/future/tests, inifile: pytest.ini
        plugins: anyio-3.5.0
        collected 1 item                                                                                                                               
        
        tests/test_assert.py::test_stdout capsys stdout=stdout>>
        capsys stderr=stderr>>
        PASSED
        
        =========================================================== 1 passed in 0.05 seconds ===========================================================
        
        
        • 1
        • 2
        • 3
        • 4
        • 5
        • 6
        • 7
        • 8
        • 9
        • 10
        • 11
        • 12
        • 13
    • recwarn
      捕获程序中的warnnings告警

      • code (不引入recwarn)

        import warnings
        
        
        def warn():
            warnings.warn('Deprecated function', DeprecationWarning)
        
        
        def test_warn():
            warn()
        
        • 1
        • 2
        • 3
        • 4
        • 5
        • 6
        • 7
        • 8
        • 9
      • 输出 可以看到有warnnings summary 告警

        ============================================================= test session starts ==============================================================
        platform darwin -- Python 3.7.3, pytest-4.6.5, py-1.11.0, pluggy-0.13.1
        rootdir: /Users/huangxiaonan/PycharmProjects/future/tests, inifile: pytest.ini
        plugins: anyio-3.5.0
        collected 1 item                                                                                                                               
        
        tests/test_assert.py .                                                                                                                   [100%]
        
        =============================================================== warnings summary ===============================================================
        test_assert.py::test_warn
          /Users/huangxiaonan/PycharmProjects/future/tests/test_assert.py:5: DeprecationWarning: Deprecated function
            warnings.warn('Deprecated function', DeprecationWarning)
        
        -- Docs: https://docs.pytest.org/en/latest/warnings.html
        ===================================================== 1 passed, 1 warnings in 0.05 seconds =====================================================
        
        • 1
        • 2
        • 3
        • 4
        • 5
        • 6
        • 7
        • 8
        • 9
        • 10
        • 11
        • 12
        • 13
        • 14
        • 15
      • code 引入recwarn

        import warnings
        
        
        def warn():
            warnings.warn('Deprecated function', DeprecationWarning)
        
        
        def test_warn(recwarn):
            warn()
        
        • 1
        • 2
        • 3
        • 4
        • 5
        • 6
        • 7
        • 8
        • 9

        此时将无告警,此时告警对象可在测试用例中通过recwarn.pop()获取到

      • code 以下方式也可以捕获warn

        def test_warn():
            with pytest.warns(None) as warnings:
                warn()
        
        • 1
        • 2
        • 3
    • monkeypatch
      按照理解来说这些函数的作用仅仅是在测试用例的作用域内有效,参见setenv

      • setattr(target, name, value, raising=True),设置属性;
      • delattr(target, name, raising=True),删除属性;
      • setitem(dic, name, value),字典添加元素;
      • delitem(dic, name, raising=True),字典删除元素;
      • setenv(name, value, prepend=None),设置环境变量;
        import os
        
        
        def test_config_monkeypatch(monkeypatch):
            monkeypatch.setenv('HOME', "./")
            import os
            print(f"monkeypatch: env={os.getenv('HOME')}")
        
        
        def test_config_monkeypat():
            import os
            print(f"env={os.getenv('HOME')}")
        
        • 1
        • 2
        • 3
        • 4
        • 5
        • 6
        • 7
        • 8
        • 9
        • 10
        • 11
        • 12
        输出 可以看到monkeypath给环境变量大的补丁只在定义的测试用例内部有效
        ============================================================= test session starts ==============================================================
        platform darwin -- Python 3.7.3, pytest-4.6.5, py-1.11.0, pluggy-0.13.1 -- /Library/Developer/CommandLineTools/usr/bin/python3
        cachedir: .pytest_cache
        rootdir: /Users/huangxiaonan/PycharmProjects/future/tests, inifile: pytest.ini
        plugins: anyio-3.5.0
        collected 2 items                                                                                                                              
        
        tests/test_assert.py::test_config_monkeypatch monkeypatch: env=./
        PASSED
        tests/test_assert.py::test_config_monkeypat env=/Users/huangxiaonan
        PASSED
        
        =========================================================== 2 passed in 0.06 seconds ===========================================================
        
        • 1
        • 2
        • 3
        • 4
        • 5
        • 6
        • 7
        • 8
        • 9
        • 10
        • 11
        • 12
        • 13
      • delenv(name, raising=True),删除环境变量;
      • syspath_prepend(path),添加系统路径;
      • chdir(path),切换目录。
  • 相关阅读:
    【分布式存储】聊一下分布式存储中分片机制
    MyBatis_MyBatis之查询返回对象集合
    word2016 电子签名
    nginx代理springboot前后端分离服务--接入cas客户端时内外网配置
    169. 多数元素
    2023中国(深圳)国际设备维护、状态监测及故障诊断维修展览会
    Android_JNI编程入门
    Docker安装以及修改Docker的默认存储路径
    js将html网页转换成PDF并解决了图表文字被切割的问题
    0815(031天 线程/进程02)
  • 原文地址:https://blog.csdn.net/weixin_43380311/article/details/124820485