Daimon Blog

山在那里

Python测试技巧

Python的相关测试工具很多。这里介绍下我的测试方案。主要涉及到:doctest / unittest / coverage / pytest

参考文章:

doctest

doctest 与 pycharm 结合起来,非常好用。也是我测试用例的主力。 一边写程序,一边doctest就写好了。

有一些小技巧:

  • 模糊匹配输出

    def login(ent_id, username, tel):
    """
    >>> login(ent_id, username, '01062149131') # doctest: +ELLIPSIS
    {...成功...
    """
    pass
  • 代码中执行doctest

    import doctest
    doctest.testmod(<model>)
  • 终端中执行doctest

    python -m doctest <test_file_path>

unitest

这个用的太广了。涉及到一些复杂的测试用例,或者是不好写在doctest中的,我就用unitest。

目录结构

proj
├── ccuni
│   ├── __init__.py
│   └── ccuni.py
└── tests
    ├── __init__.py
    └── test_ccuni.py

测试代码

#!/usr/bin/env python
# -*- coding: utf-8 -*-

"""Tests for `ccuni` package."""

import unittest
from click.testing import CliRunner

# from ccuni import ccuni
from ccuni import cli


class TestCcuni(unittest.TestCase):
    """Tests for `ccuni` package."""

    def setUp(self):
        """Set up test fixtures, if any."""

    def tearDown(self):
        """Tear down test fixtures, if any."""

    def test_000_something(self):
        """Test something."""
        pass

    def test_command_line_interface(self):
        """Test the CLI."""
        runner = CliRunner()
        result = runner.invoke(cli.main)
        assert result.exit_code == 0
        assert 'ccuni.cli.main' in result.output
        help_result = runner.invoke(cli.main, ['--help'])
        assert help_result.exit_code == 0
        assert '--help  Show this message and exit.' in help_result.output

运行unitest我一般在pycharm中用鼠标操作。 命令行我很少用unitest自己的,主要用pytest来跑

pytest

命令行下我主要用pytest,把 unittest/doctest 整合起来跑

安装

pip install pytest

配置与运行

在项目的根目录下,创建 setup.cfg

[tool:pytest]
addopts = --doctest-modules
doctest_optionflags= NORMALIZE_WHITESPACE IGNORE_EXCEPTION_DETAIL ELLIPSIS

然后直接

pytest

coverage

coverage是看测试代码覆盖率的。

pytest这么结合起来

coverage run --source <package> -m py.test
coverage report

tox

涉及到多版本的python自动测试,用tox

项目根目录下创建tox.ini 其中flake8是代码规范检查。一会再说。

[tox]
envlist = py36, flake8

[travis]
python =
    3.6: py36
    3.5: py35
    3.4: py34

[testenv:flake8]
basepython = python
deps = flake8
commands = flake8 ccuni --ignore E501

[testenv]
setenv =
    PYTHONPATH = {toxinidir}

commands = pytest

flake8

代码的PEP08规范检查。使用很简单

flake8 <package> --ignore E501

最好把规则写在配置文件中。同样还是setup.cfg

[flake8]
ignore = E501
exclude = .git,__pycache__,docs,old,build,dist
max-complexity = 10

make

把以上都结合起来,写成自动化脚本,我用make

Makefile内容见下。这是参考pydanney的cookiecutter-pypackage脚本

.PHONY: clean clean-test clean-pyc clean-build docs help
.DEFAULT_GOAL := help

define BROWSER_PYSCRIPT
import os, webbrowser, sys

try:
  from urllib import pathname2url
except:
  from urllib.request import pathname2url

webbrowser.open("file://" + pathname2url(os.path.abspath(sys.argv[1])))
endef
export BROWSER_PYSCRIPT

define PRINT_HELP_PYSCRIPT
import re, sys

for line in sys.stdin:
  match = re.match(r'^([a-zA-Z_-]+):.*?## (.*)$$', line)
  if match:
    target, help = match.groups()
    print("%-20s %s" % (target, help))
endef
export PRINT_HELP_PYSCRIPT

BROWSER := python -c "$$BROWSER_PYSCRIPT"

help:
  @python -c "$$PRINT_HELP_PYSCRIPT" < $(MAKEFILE_LIST)

clean: clean-build clean-pyc clean-test ## remove all build, test, coverage and Python artifacts

clean-build: ## remove build artifacts
  rm -fr build/
  rm -fr dist/
  rm -fr .eggs/
  find . -name '*.egg-info' -exec rm -fr {} +
  find . -name '*.egg' -exec rm -f {} +

clean-pyc: ## remove Python file artifacts
  find . -name '*.pyc' -exec rm -f {} +
  find . -name '*.pyo' -exec rm -f {} +
  find . -name '*~' -exec rm -f {} +
  find . -name '__pycache__' -exec rm -fr {} +

clean-test: ## remove test and coverage artifacts
  rm -fr .tox/
  rm -f .coverage
  rm -fr htmlcov/
  rm -fr .pytest_cache

lint: ## check style with flake8
  flake8 ccuni tests --ignore E501

test: ## run tests quickly with the default Python
  pytest

test-all: ## run tests on every Python version with tox
  tox

coverage: ## check code coverage quickly with the default Python
  coverage run --source ccuni setup.py test
  coverage report -m
  coverage html
  $(BROWSER) htmlcov/index.html

docs: ## generate Sphinx HTML documentation, including API docs
  rm -f docs/ccuni.rst
  rm -f docs/modules.rst
  sphinx-apidoc -o docs/ ccuni
  $(MAKE) -C docs clean
  $(MAKE) -C docs html
  $(BROWSER) docs/_build/html/index.html

servedocs: docs ## compile the docs watching for changes
  watchmedo shell-command -p '*.rst' -c '$(MAKE) -C docs html' -R -D .

release: dist ## package and upload a release
  twine upload dist/* -r hxt

dist: clean ## builds source and wheel package
  python setup.py sdist
  python setup.py bdist_wheel
  ls -l dist

install: clean ## install the package to the active Python's site-packages
  python setup.py install

其它

代码中调用以上测试框架,可以参考下面代码。

import coverage
import doctest
import unittest
import os

# import test_module 
import my_module

cov = coverage.Coverage()
cov.start()

# running doctest by explicity naming the module
doctest.testmod(my_module)

# running unittests by just specifying the folder to look into
testLoad = unittest.TestLoader()
testSuite = testLoad.discover(start_dir=os.getcwd())
runner = unittest.TextTestRunner()
runner.run(testSuite)

cov.stop()
cov.save()
cov.html_report()
print("tests completed")

性能测试

除了cProfile以外,还有一个选择:yappi。这个与pycharm集成度较高。尤其是在监控:python -m xxx.xxx 的程序的时候。

pip install yappi

pytest

django中使用

pip install pytest-django

vi pytest.ini
[pytest]
DJANGO_SETTINGS_MODULE = config.settings
# -- recommended but optional:
python_files = tests.py test_*.py *_tests.py

然后与src目录并列建立tests文件夹

mkdir tests
vi tests/__init__.py
import sys
import os

myPath = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, myPath + '/../src')

使用pytest过程中,使用当前数据库

vi tests/conftest.py
import pytest


@pytest.fixture(scope='session')
def django_db_setup():
    from django.conf import settings
    settings.DATABASES['default'] = {
        'ENGINE': 'django.db.backends.mysql',
        'HOST': 'db.example.com',
        'NAME': 'external_db',
    }

mock数据

auth_file = MagicMock()
auth_file.auth_file.path = os.path.join(os.path.dirname(__file__), '20190129_md5.xlsx')
xxx = People()
mocker.patch.object(xxx, 'auth')
xxx.auth.path = 'kjkj'

文章分类目录