本文将进入单元测试的部分,这也是基础知识中最后一个大块。本文将重点讲述 Python 和 OpenStack 中的单元测试的生态环境。
通过 demo 学习 OpenStack 开发——单元测试 单元测试的重要性 单元测试工具 unittest mock testtools fixtures testscenarios subunit testrepository coverage tox 单元测试工具小结 Keystone 的单元测试框架 使用 tox 进行测试环境管理 使用 testrepository 管理测试的运行 单元测试用例的代码架构 总结 系列后记
单元测试的重要性
GitHub 上有个人画了一些不同语言的学习曲线图: Learning Curves (for different programming languages) ,虽然有些恶搞的倾向,不过确实说明了问题。这里贴一下 Python 的部分:
这个图说明了,会单元测试对于提高 Python 生产力的重要性,这主要是因为 Python 是个动态语言,很多问题都无法通过静态编译检查来发现,因此单元测试就成了一个重要的确保质量的手段。OpenStack 的核心项目都对单元测试有极高的要求,以保证项目的高质量。
单元测试工具
Python 的单元测试工具很多,为单元测试提供不同方面的功能。OpenStack 的项目也基本把现在流行的单元测试工具都用全了。单元测试可以说是入门 OpenStack 开发的最难的部分,也是最后一公里。本章,我们就介绍一下在 OpenStack 中会用到的单元测试的工具。由于数量很多,不可能详细介绍,因此主要做一些概念和用途上的介绍。
unittest
unittest 是 Python 的标准库,提供了最基本的单元测试功能,包括单元测试运行器(简称 runner)和单元测试框架。项目的单元测试代码的测试类可以继承unittest.TestCase
类,这样这个类就能够被 runner 发现并且执行。同时,unittest.TestCase
这个类还定义了setUp()
,tearDown()
,setUpClass()
和tearDownClass()
方法,是用来运行单元测试前的设置工作代码和单元测试后的清理工作代码,这个也是所有 Python 代码遵守的规范,所以第三方的单元测试库和框架也都遵循这个规范。
unittest 库也提供了一个 runner,可以使用$ python -m unittest test_module
的命令来执行某个模块的单元测试。另外,在 Python 中指定要运行的单元测试用例的完整语法是:path.to.your.module:ClassOfYourTest.test_method。
unittest 是学习 Python 单元测试最基本也最重要的一个库,完整的说明请查看官方文档。
mock
mock 也是另一个重要的单元测试库,在 Python 2 中是作为一个第三方库被使用的,到 Python 3 时,就被纳入了标准库,可见这个库的重要性。简单的说,mock 就是用来模拟对象的行为,这样在进行单元测试的时候,可以指定任何对象的返回值,便于测试对外部接口有依赖的代码。关于 mock 的使用,可以查看我之前写的这篇文章 Python Mock 的入门。
testtools
testtools 是个 unittest 的扩展框架,主要是在 unittest 的基础上提供了更好的 assert 功能,使得写单元测试更加方便。具体可以查看文档。
fixtures
fixture 的意思是固定装置,在 Python 的单元测试中,是指某段可以复用的单元测试setUp
和tearDown
代码组合。一个 fixture 一般用来实现某个组件的 setUp 和 tearDown 逻辑,比如测试前要先创建好某些数据,测试后要删掉这些数据,这些操作就可以封装到一个 fixture 中。这样不同的测试用例就不用重复写这些代码,只要使用 fixture 即可。fixtures 模块是一个第三方模块,提供了一种简单的创建 fixture 类和对象的机制,并且也提供了一些内置的 fixture。具体的使用方法可以查看官方文档。
testscenarios
testscenarios 模块满足了场景测试的需求。它的基本用法是在测试类中添加一个类属性scenarios
,该属性是一个元组,定义了每一种场景下不同的变量的值。比如说你测试一段数据访问代码,你需要测试该代码在使用不同的驱动时,比如 MongoDB、SQL、File,是否都能正常工作。我们有三种办法:
- 最笨的办法是为不同的驱动把同一个测试用例编写 3 遍。
- 比较好的办法是,编写一个统一的非测试用例方法,接收 driver 作为参数,执行测试逻辑,然后再分别编写三个测试用例方法去调用这个非测试用例方法。
- 更好的办法就是使用 testscenarios 模块,定义好 scenarios 变量,然后实现一个测试用例方法。
testscenarios 模块在 OpenStack Ceilometer 中被大量使用。更多的信息可以查看文档。
subunit
subunit 是一个用于传输单元测试结果的流协议。一般来说,运行单元测试的时候是把单元测试的结果直接输出到标准输出,但是如果运行大量的测试用例,这些测试结果就很难被分析。因此就可以使用 python-subunit 模块来运行测试用例,并且把测试用例通过 subunit 协议输出,这样测试结果就可以被分析工具聚合以及分析。python-subunit 模块自带了一些工具用来解析 subunit 协议,比如你可以这样运行测试用例:$ python -m subunit.run test_module | subunit2pyunit
,subunit2pyunit
命令会解析 subunit 协议,并且输出到标准输出。关于 subunit 的更多信息,请查看官方文档。
testrepository
OpenStack 中使用 testrepository 模块管理单元测试用例。当一个项目中的测试用例很多时,如何更有效的处理单元测试用例的结果就变得很重要。testrepository 的出现就是为了解决这个问题。testrepository 使用 python-subunit 模块来运行测试用例,然后分析 subunit 的输出并对测试结果进行记录(记录到本地文件)。举例来说,testrepository 允许你做这样的事情:
- 知道哪些用例运行时间最长
- 显示运行失败的用例
- 重新运行上次运行失败的用例
testrepository 的更多信息,请查看官方文档。
coverage
coverage 是用来计算代码运行时的覆盖率的,也就是统计多少代码被执行了。它可以和 testrepository 一起使用,用来统计单元测试的覆盖率,在运行完单元测试之后,输出覆盖率报告。具体的使用方法可以查看官方文档。
tox
tox 是用来管理和构建虚拟环境 (virtualenv) 的。对于一个项目,我们需要运行 Python 2.7 的单元测试,也需要运行 Python 3.4 的单元测试,还需要运行 PEP8 的代码检查。这些不同的任务需要依赖不同的库,所以需要使用不同的虚拟环境。使用 tox 的时候,我们会在 tox 的配置文件tox.ini中指定不同任务的虚拟环境名称,该任务在虚拟环境中需要安装哪些包,以及该任务执行的时候需要运行哪些命令。更多信息,请查看官方文档。
单元测试工具小结
本章介绍了 OpenStack 中常用的单元测试工具的基本用途,希望大家对这些工具有个大概的认识。这里我们可以按照类别总结一下这些工具:
- 测试环境管理: tox
使用 tox 来管理测试运行的虚拟环境,并且调用 testrepository 来执行测试用例。
-
测试用例的运行和管理: testrepository, subunit, coverage
testrepository 调用 subunit 来执行测试用例,对测试结果进行聚合和管理;调用 coverage 来执行代码覆盖率的计算。 -
测试用例的编写: unittest, mock, testtools, fixtures, testscenarios
使用 testtools 作为所有测试用例的基类,同时应用 mock, fixtures, testscenarios 来更好的编写测试用例。
在 _The Hacker’s Guide to Python_(《Python 高手之路》)一书中,也有专门的一章介绍了各种单元测试工具及其用法,读者也可以参考一下。下一章,我们来分析 Keystone 项目的单元测试框架,可以让你看到在 OpenStack 的实际项目中,这些工具是如何被使用的。
Keystone 的单元测试框架
现在,我们以 Keystone 项目为例,来看下真实项目中的单元测试是如何架构的。我们采用自顶向下的方式,先从最上层的部分介绍起。
使用 tox 进行测试环境管理
大部分情况下,我们都是通过tox命令来执行单元测试的,并且传递环境名称给 tox 命令:
~/openstack/env/p/keystone git:(master) ✗ $ tox -e py27
tox 命令首先会读取项目根目录下的 _tox.ini_ 文件,获取相关的信息,然后根据配置构建 virtualenv,保存在 _.tox/_ 目录下,以环境名称命名:
~/openstack/env/p/keystone git:(master) ✗ $ ls .tox log pep8 py27
除了 _log_ 目录,其他的都是普通的 virtualenv 环境,你可以自己查看一下内容。我们来看下 _py27_ 这个环境的相关配置(在 tox.ini)中,我直接在内容上注释一些配置的用途:
[tox] minversion = 1.6 skipsdist = True # envlist 表示本文件中配置的环境都有哪些 envlist = py34,py27,pep8,docs,genconfig,releasenotes # testenv 是默认配置,如果某个配置在环境专属的 section 中没有,就从这个 section 中读取 [testenv] # usedevelop 表示安装 virtualenv 的时候,本项目自己的代码采用开发模式安装, 也就是不会拷贝代码到 virtualenv 目录中,只是做个链接 usedevelop = True # install_command 表示构建环境的时候要执行的命令,一般是使用 pip 安装 install_command = pip install -U {opts} {packages} setenv = VIRTUAL_ENV={envdir} # deps 指定构建环境的时候需要安装的依赖包,这个就是作为 pip 命令的参数 # keystone 这里使用的写法比较特殊一点,第二行的.[ldap,memcache,mongodb] 是两个依赖, 第一个点'.'表示当前项目的依赖,也就是 requirements.txt,第二个部分 [ldap,memcache,mongodb] 表示 extra,是在 setup.cfg 文件中定义的一个段的名称,该段下定义了额外的依赖,这些可以查看 PEP0508 # 一般的项目这里会采用更简单的方式来书写,直接安装两个文件中的依赖: # -r{toxinidir}/requirements.txt # -r{toxinidir}/test-requirements.txt deps = -r{toxinidir}/test-requirements.txt .[ldap,memcache,mongodb] # commands 表示构建好 virtualenv 之后要执行的命令,这里调用了 tools/pretty_tox.sh 来执行测试 commands = find keystone -type f -name "*.pyc" -delete bash tools/pretty_tox.sh '{posargs}' whitelist_externals = bash find passenv = http_proxy HTTP_PROXY https_proxy HTTPS_PROXY no_proxy NO_PROXY PBR_VERSION # 这个 section 是为 py34 环境定制某些配置的,没有定制的配置,从 [testenv] 读取 [testenv:py34] commands = find keystone -type f -name "*.pyc" -delete bash tools/pretty_tox_py3.sh
上面提到的 PEP-0508 是依赖格式的完整说明。setup.cfg 的 _extra_ 部分如下:
[extras] ldap = python-ldap>=2.4:python_version=='2.7' # PSF ldappool>=1.0:python_version=='2.7' # MPL memcache = python-memcached>=1.56 # PSF mongodb = pymongo!=3.1,>=3.0.2 # Apache-2.0 bandit = bandit>=0.17.3 # Apache-2.0
使用 testrepository 管理测试的运行
上面我们看到 _tox.ini_ 文件中的commands
参数中执行的是 _tools/pretty_tox.sh_ 命令。这个脚本的内容如下:
#!/usr/bin/env bash set -o pipefail TESTRARGS=$1 # testr 和 setuptools 已经集成,所以可以通过 setup.py testr 命令来执行 # --testr-args 表示传递给 testr 命令的参数,告诉 testr 要传递给 subunit 的参数 # subunit-trace 是 os-testr 包中的命令(os-testr 是 OpenStack 的一个项目),用来解析 subunit 的输出的。 python setup.py testr --testr-args="--subunit $TESTRARGS" | subunit-trace -f retval=$? # NOTE(mtreinish) The pipe above would eat the slowest display from pbr's testr # wrapper so just manually print the slowest tests. echo -e "\nSlowest Tests:\n" # 测试结束后,让 testr 显示出执行时间最长的那些测试用例 testr slowest exit $retval
too 就是从tools/pretty_tox.sh
这个命令开始调用 testr 来执行单元测试的。testr 本身的配置是放在项目根目录下的.testr.conf 文件:
[DEFAULT] test_command= ${PYTHON:-python} -m subunit.run discover -t ./ ${OS_TEST_PATH:-./keystone/tests/unit} $LISTOPT $IDOPTION test_id_option=--load-list $IDFILE test_list_option=--list group_regex=.*(test_cert_setup) # NOTE(morganfainberg): If single-worker mode is wanted (e.g. for live tests) # the environment variable ``TEST_RUN_CONCURRENCY`` should be set to ``1``. If # a non-default (1 worker per available core) concurrency is desired, set # environment variable ``TEST_RUN_CONCURRENCY`` to the desired number of # workers. test_run_concurrency=echo ${TEST_RUN_CONCURRENCY:-0}
这个文件中的配置项可以从 testr 官方文档中找到。其中test_command
命令表示要执行什么命令来运行测试用例,这里使用的是subunit.run
,这个我们在上面提到过了。
到目前为止的流程就是:
- tox 建好 virtualenv
- tox 调用 testr
- testr 调用 subunit 来执行测试用例
每个 OpenStack 项目基本上也都是这样。如果你自己在开发一个 Python 项目,你也可以参考这个架构。
单元测试用例的代码架构
下面我们来看一下 Keystone 的单元测试代码是如何写的,主要是看一下其层次结构。每个 OpenStack 项目的单元测试代码结构可能都不一样,不过你了解完 Keystone 的结构之后,看其他项目的就会比较快了。
我们以一个测试类为例来分析测试代码的结构:keystone.tests.unit.test_v3_assignment:AssignmentTestCase
。下面是这个类的继承结构,同一级别的缩进表示多重继承,增加缩进表示父类,这里删掉了不必要的路径前缀(从 unit 目录开始,如下所示:
# 这个测试类是测 RoleAssignment 的 API 的 unit.test_v3_assignment.RoleAssignmentBaseTestCase -> unit.test_v3.AssignmentTestMixin 这个类包含了一下测试 Assignment 的工具函数 -> unit.test_v3.RestfulTestCase 这个类是进行 V3 REST API 测试的基类,实现了 V3 API 的请求发起和校验 -> unit.core.SQLDriverOverride 用于修改各个配置的 driver 字段为 sql -> unit.test_v3.AuthTestMixin 包含创建认证请求的辅助函数 -> unit.rest.RestfulTestCase 这个类是进行 RESP API 测试的基类,V2 和 V3 的 API 测试 都是以这个类为基类,这个类的 setUp 方法会初始化数据库,创建好 TestApp。 -> unit.TestCase 这个类是 Keystone 中所有单元测试类的基类,它主要初始化配置,以及初始化 log -> unit.BaseTestCase 这个类主要是配置测试运行的基本环境,修改一些环境变量,比如 HOME 等。 -> oslotest.BaseTestCase 这个是在 oslotest 中定义的基类,原来所有的 OpenStack 项目的单元测试都继承自这个基类。 不过,这个继承在 Keystone 中已经被删除了,Keystone 自己在 unit.BaseTestCase 中做了差不多的事情。 这个是 2016-02-17 做的变更,具体的可以查看这个 revision 262d0b66c3bcb82eadb663910ee21ded63e77a78。 -> testtools.TestCase 使用 testtools 作为测试框架 -> unittest.TestCase testtools 本身是 unittest 的扩展
从上面的层次结构可以看出,OpenStack 中的大项目,由于单元测试用例很多(Keystone 现在有超过 6200 个单元测试用例),所以其单元测试架构也会比较复杂。要写好单元测试,需要先了解一下整个测试代码的架构。
总结
本文我们了解了 Python 中的单元测试的概念和工具,并且通过 Keystone 项目了解了实际项目中的单元测试的架构,希望有助于各位读者更好的掌握 OpenStack 项目的单元测试基础。 webdemo 项目目前没有单元测试的代码,有兴趣的读者可以自己 fork 然后参考 Keystone 的架构为其增加完整的单元测试架构。
系列后记
这个系列我打算就此结束,到目前为止一共写了 8 篇文章,写写停停,前后写了 9 个月。这里也做个小结。
一开始写这个系列的文章是因为我自己在学习 OpenStack 开发的过程中遇到很多困难,很难找到所需的入门文章。所以打算写点文章,既能作为自己的总结,也能为其他人提供些帮助。如果这些文章能帮到你,我就非常的开心。当然,这些文章的质量肯定有好有坏,欢迎大家提意见,如果有时间,我会继续修改。
然后,我想说一下写这类文章的难点,主要是要保证细节都是正确的,然后又不能太啰嗦。
- 细节都是正确的。举个例子,大学的很多数据结构教材中的代码,你直接贴到电脑上,然后编译,大部分是编译不通过的。这个会让初学者非常沮丧。所以我希望能够保证这些文章里的细节都是正确的,包括一些工具的配置,如果觉得有必要,我也会描述下配置的作用,以及要去哪里找更多的信息。如果这方面有遗漏,请和我说。
- 不能太啰嗦。这 8 篇文章里涉及的库有好几十个,每个库如果都讲仔细了,那就会让文章显得非常啰嗦。但是又不能直接让读者去看库的官方文档,所以权衡内容也是很麻烦的。如果各位有这方面的建议,也请和我说。
这个系列的文章是关于 OpenStack 的基础知识,其实 OpenStack 开发还要涉及到很多其他的知识,比如消息队列、非阻塞 IO 等,而且还要了解整个 OpenStack 的开发生态,包括 Gerrit 评审系统、Zuul 持续集成、devstack 开发环境、oslo 项目等。
感谢魏星对本文的审校。
给InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家通过新浪微博( @InfoQ , @丁晓昀),微信(微信号: InfoQChina )关注我们。
评论