开发人员最讨厌的东西除了编写文档外,估计就是写测试用例了。
由于任务需求,需要搭建python的单元测试框架,并编写测试用例。但之前几乎没有接触过,所了解的也就只有自带的unittest框架了。
经过一段时间的研究,发现python的测试生态挺优秀的,个人记性也不好,因此记录下来,供自己和网友学习回顾。
单元测试,是个老生常谈的问题。我们都知道python自带了unittest框架,支持单元测试的编写。但深入研究发现,python支撑单元测试的东东还是很多的。因为python有多个版本,第三方库包也是有很多版本的,不同的依赖包版本都可能影响自己项目程序的正常运行;同时,一般来说,单元测试可能会依赖外部环境的参数变量,或者涉及到外部系统组件的交互,此时,仅仅只有unittest,是远远不够的。下面,介绍一些单元测试常用的python工具,以满足各种系统项目的测试需求。
测试环境管理工具 – tox
点击进入tox官方文档
基于官档的介绍,tox工具的主要目标是自动化和标准化python测试。
tox是通用的虚拟环境管理和测试命令行工具。配合virtualenv和pip工具,tox能够让我们在同一个Host上自定义出多套相互独立且隔离的python环境,每套虚拟环境中可能使用了不同的 Python 拦截器/环境变量设置/第三方依赖包。所以 tox 最典型的应用就是用于测试 Python 程序的兼容性了。tox 虽然不提供但是可以集成源码发布打包(sdist)、单元测试(UT)、测试覆盖率(coverage)、代码格式检查(pep8,flake) 等功能。说白了,tox就是用于构建测试需要的环境(python版本、第三方依赖、系统环境变量等),使测试用例可以在不同条件下运行。
上述为tox的工作流程示意图。一般的配置运行步骤流程为:
- 加载配置 tox.ini(放置在setup.py 同级目录),合并cli命令行和系统环境变量参数
- 打包,一般使用 python setup.py sdist 命令执行,可以在配置中忽略打包操作
- 配置运行环境,对在tox中配置每一个环境(py27,py36),使用virtualenv创建一个新的虚拟环境,然后在环境中安装依赖package(使用pip),执行(如nose,stestr,pytest等)命令运行测试用例
- 报告每个环境的输出结果
tox.ini这个配置文件的一个简单编写如下:
# content of: tox.ini , put in same dir as setup.py
[tox]
# 设置运行的python版本
envlist = py27,py36
skipsdist = True # 跳过项目打包环节
[testenv]
deps = stestr # install pytest in the virtualenv where commands will be executed
setenv =
LANG=en_US.UTF-8
# 白名单,使用到的但没有通过deps安装的,否则会报warning
whitelist_externals = stestr
# 环境部署完毕后执行的命令,因此不一定是测试命令,也可以写pep8/pylint的代码检测或者其它的运行命令
commands =
# whatever extra steps before testing might be necessary
stestr # or any other test runner that you might use
# 测试覆盖率统计
[testenv:cover]
# 配置环境变量参数
setenv =
{[testenv]setenv} # 引用[testenv]的环境变量设置
PYTHON=coverage run --source demo --parallel-mode
commands =
stestr {postargs}
coverage combine
coverage html -d cover
coverage xml -o cover/coverage.xml
配置文件好了后,输入tox
,就可以运行默认的**[testenv]**区域的配置,如果希望运行代码覆盖率检测,可以执行 tox -e cover
。
测试脚本运行工具 – Test Runner
当只有一个测试用例时,我们可能只要执行python -munittest test_module.TestClass
,就能成功地运行测试用例。但是如果是成百上千上万甚至海量的的测试用例,我们将会面临一个问题,如何把这么多的用例自动运行起来。 Test Runner (测试运行工具)就是为了解决这样的事情的。目前市面上比较流行的 runner 有 nose、pytest、testrepository等等。它们各有特色,下面逐一进行介绍。
nose
nose是一个非常流行的 Test Runner,很多主流的测试应用工具都支持它,比如jenkins、tappy、coverage等。nose安装很方便,好像系统源就有其rpm包,同时其参考文档和应用案例很多,和各种插件(如jenkins)的配置使用等百度一下就有,非常方便。由于nose很通用,支持插件很多,也导致了使用过程中会配置很多,使用起来也较为很复杂,对于一些较为复杂的测试需求可能难以实现。而且,非常不幸的是,nose已经没有开发团队对其进行维护了,官方推荐新的项目使用nose2或者pytest,或者直接使用unittest。而nose2其实也一直没什么发展,所以目前更多的是使用pytest或者testrepository。虽然如此,对于小项目来说,使用nose还是很方便的。下面就简单介绍一下nose的使用方法。
nose工具会在指定目录搜索所有以test_ 开头的文件,然后执行其中所有以 test_开头的函数。nose在搜寻测试用例的时候,会基于__init__.py
文件层层构建,确保所有测试用例以top-level(根模块)进行导入。我们可以通过配置文件,或者直接使用命令行参数方式,控制测试用例的运行。下面以配置文件方式进行介绍:
[nosetests]
# 统计测试覆盖率的模块
cover-package=demo
# tests path
tests=demo/tests
# log level
logging-level=WARN
# 统计测试用例的代码覆盖率
with-coverage=1
# 覆盖率输出xml格式
cover-xml=1
# 测试结果以junitxml格式输出
with-xunit=1
如上所示,nose是一个标准的配置文件格式,以[nosetests]
作为section标识,上面的配置了测试用例的搜寻路径、测试结果输出格式、测试代码覆盖率统计和输出格式,以及打印日志的等级过滤等。nose在命令行的执行名称是nosetests,其运行测试用例命令为:nosetests -c nose.conf
即可,具体参数可以执行 nosetests --help
查看说明。
pytest
pytest也是一个很流行的测试框架,使用也挺简单的,使用其编写小型测试用例特别简单。详细看看官方文档即可。但是基于pytest框架的测试用例与我们往常的unittest编写习惯有些区别,风格迥异,要不要使用pytest主要看个人习惯吧。pytest框架下可以快速写些小型的测试。也是很常用的一种跑测试的runner,应该是在nose不维护后目前使用率较高的一个工具,很多其它工具都支持它。tappy提供pytest的插件支持。
simple Testrepository / (testrepository的分支)
TestRepository 是一个基于 subunit 测试流的并发执行工具,非常使用于大型应用的测试用例执行。而 Simple Testrepository 工具则是其fork的另一个分支,简化了TestRepository的使用操作。目前openstack组件单元测试基本使用stestr运行。关于stestr的资料几乎找不到,因此只能看官方文档了。而且非常不幸的是,如果需要构建自动化集成CI的话,jenkins不支持subunit格式的输出报告,需要通过命令subunit2junitxml将subunit流转化为junitxml格式的测试报告(subunit流和junitxml的区别,后续给出)。
使用stestr很简单,通过pip安装对应包及依赖后,在执行目录创建一个 .stestr.conf
配置文件,其内容如下:
[DEFAULT]
test_path=./project_source_dir/tests
如上,test_path
指搜寻的测试用例路径,然后执行 stestr run
就能够执行测试用例了。它会在当前目录创建一个**.stestr**的目录,用于存放测试结果。其后,可以基于测试结果运行上一次failed的测试用例等操作。
测试框架 – Framework
测试框架影响的是我们如何(编写/组织)测试用例,和上述的Test Runner不冲突。
unittest
这个是python标准库的内置框架,其使用如下:
import unittest
class TestStringMethods(unittest.TestCase):
def test_upper(self):
self.assertEqual('foo'.upper(), 'FOO')
def test_isupper(self):
self.assertTrue('FOO'.isupper())
self.assertFalse('Foo'.isupper())
def test_split(self):
s = 'hello world'
self