Ch6 unit testing

阅读量:

摘要

基本用法

pip install pytest

保持目录以及文件名上的一致,使代码库更易读。例如,mylib/foobar.py 的测试文件应为 mylib/tests/test_foobar.py。pytest 会自动识别所有以 test_ 开头的 python 文件以及文件中所有以 test_ 开头的函数。

  • 使用 pytest -v test_true.py 运行指定文件测试,-v 为给出详细测试结果。若以文件夹为参数,pytest 会遍历文件夹并自动运行所有以 test_ 开头的文件。
  • 使用参数 -k 运行测试文件中的指定函数,pytest -v test_true.py -k test_false
  • 使用参数 -m 筛选标记的测试,例如,@pytest.mark.selfdefine,之后在命令行中运行 pytest -v test_true.py -m selfdefine,也可进行反选 pytest -v test_true.py -m 'not selfdefine'。如果没有在 pytest.inipyproject.toml 中注册自定义的名称,则会报 warning。
  • 使用参数 -n 设置并行进程数,需要安装 pytest-xdist,也可使用 -n auto 自动设置进程数。
  • 使用 assert 对语句进行判断。
  • 使用 pytest.skip()@pytest.mark.skip()@pytest.mark.skipif() 跳过指定测试。

      @pytest.mark.skip("Do not run")
      def test_false():
          assert False
    	
    	
      @pytest.mark.skipif(mylib is None, reason="mylib not avaliable")
      def test_mylib():
          assert mylib.foobar()
    	
      def test_skip_at_runtime():
          if True:
              pytest.skip("Skip at runtime")
    
  • 使用 @pytest.fixture 来预定义一些参数、文件、数据库等,并在测试结束后清除,这样不需要重复写测试所需初始化的代码。通过使用 yield 来实现在测试函数运行结束后执行 yield 后的代码。需要注意的是,fixture 会在每个测试函数运行前初始化,并在其结束后清除,如果需要重复使用多次,可指定 @pytest.fixture(scope="module"),在整个 module 运行结束后才会对 fixture 函数进行清除。

      @pytest.fixture
      def database():
          db = <database>
          yield db
          db.close()
    
      def test_insert(database):
          database.insert(123)
    

    可设置自动运行,@pytest.fixture(autouse=True),会在整个 module 中对每个测试函数自动运行。但注意不要滥用,可能会降低运行效率。

    可使用不同参数对函数进行测试,@pytest.fixture(params=["param1","param2"]),测试函数会自动运行两次,对两个参数分别进行测试

Mock

Python 3.3 以前的版本通过 import mock 引入,之后的版本通过 from unittest import mock 引入。

  • 创建 mock 实例

      from unittest import mock
      m = mock.Mock()
    
  • 添加属性

      m.some_attribute = "hello world"
    
      >>> m.some_attribute
      hello world
    
  • 添加函数,设置返回值

      m.some_method.return_value = 42
    
      >>> m.some_method()
      42
      >>> m.some_method("argument")
      42
    
  • 添加函数,运行指定代码

      def print_hello():
          print("hello world")
          return 43
    
      m.some_method.side_effect = print_hello
    	
      >>> m.some_method()
      hello world
      43
    
      >>> m.some_method.call_count
      1
    

    call_count 可用来统计该函数调用的次数

  • 检查传入参数

      m.some_method("foo","bar")
    	
      >>> m.some_method.assert_called_once_with('foo','bar')
      None
      >>> m.some_method.assert_called_once_with('foo',mock.ANY)
      None
      >>> m.some_method.assert_called_once_with('foo','baz')
      ...
      AssertionError: expected call not found.
      Expected: some_method('foo', 'baz')
      Actual: some_method('foo', 'bar')
    
  • 替换函数,并运行指定代码

      def fake_os_unlink(path):
          raise IOError("Testing")
    
      with mock.patch('os.unlink', fake_os_unlink):
          os.unlink('foobar')
    

    也可使用装饰器的方式,以下两种均可

      @mock.patch("os.unlink", fake_os_unlink)
      def test_unlink():
          os.unlink("foobar")
    
      @mock.patch("os.unlink")
      def test_unlink(mock_unlink):
          # set return_value
          mock_unlink.return_value = False
          mock_unlink.side_effect = [False]
          assert os.unlink("foobar")
    
  • 未被测试覆盖的代码 安装 pytest-cov,运行 pytest --cov=<packagename> path/to/unit/tests/folder

    • 设置参数 --cov-report=html,将在 htmlcov 文件夹下生成 HTML 页面。
    • 设置参数 --cov-fail-under=50,若覆盖率小于 50%,测试将失败。

虚拟环境

解决不同开发环境所需版本冲突问题

  • 创建虚拟环境 python3 -m venv <venv_name>
  • 激活虚拟环境 source <venv_name>/bin/activate
  • 退出虚拟环境 deactivate
  • 使用 tox 自动化和标准化测试。
    • 安装 tox,在 setup.py 所在目录下创建 tox.ini 文件。
    • tox.ini 中设置依赖和测试
      [testenv]
      deps=pytest
      commands=pytest
    
    • 参数 -e 可选择不同的测试环境,例如,py26, py27, py36 等。
    • tox.ini 中设置多个测试环境,需要系统中存在对应的 Python 版本
      [tox]
      envlist=py35,py36,pypy
    
    • 使用 detox 可并行对不同的测试环境进行测试。 #待整理笔记

反向链接

到头儿啦~

局部关系图