微信扫一扫

028-83195727 , 15928970361
business@forhy.com

Openstack_单元测试流程

openstack,单元测试,unit testing,软件开发,测试2016-10-30

目录

单元测试

单元测试(unit testing), 是指对软件中的最小可测试单元进行检查和验证. 其中单元是指 人为规定的最小被测功能模块. 单元测试是在软件开发过程中要进行的最起码的测试活动, 软件的独立单元将在与程序的其他部分相隔离的情况下进行测试. 本文主要以一个例子来介绍 Openstack 中的单元测试方法.

单元测试的重要性

Python 是一种动态语言, 很多问题是无法通过静态编译检查来发现的, 因此单元测试就成为了一个重要的确保质量的手段. 在 Openstack 中, 所有的 Project 都有着非常严格的单元测试步骤, 以此来保证 Project 的质量.

单元测试的工具

  • unittest: 是 Python 的标准库,提供了最基本的单元测试功能,包括 单元测试运行器(runner)单元测试框架。项目的单元测试代码的测试类可以继承 unittest.TestCase 类,这样这个测试子类就能够被 runner 发现并且执行。同时, unittest.TestCase 这个类还定义了 setUp() , tearDown() , setUpClass() 和 tearDownClass() 方法,是用来运行单元测试前的设置工作代码和单元测试后的清理工作代码。可以使用 $ python -m unittest test_module 的命令来执行某个模块的单元测试。另外,在 Python 中指定要运行的单元测试用例的完整语法是: path.to.your.module:ClassOfYourTest.test_method

  • mock: 是另一个重要的单元测试库,mock 就是用来模拟对象的行为,从而在进行单元测试时,可以指定(替换)任何对象的返回值(一般是外部接口对象),便于测试对外部接口有依赖的代码。

  • testtools: 是个 unittest 的扩展框架,在 unittest 的基础上提供了更好的 assert 功能,使得写单元测试更加方便。

  • fixtures 是指某段可以复用的单元测试 setUp 和 tearDown 代码组合。一个 fixture 一般用来实现某个组件的 setUp 和 tearDown 逻辑,比如测试前要先创建好某些数据,测试后要删掉这些数据,这些操作就可以封装到一个 fixture 中。这样不同的测试用例就不用重复写这些代码,只要使用 fixture 即可。

  • testscenarios: 满足了场景测试的需求。它的基本用法是在测试类中添加一个类属性 scenarios ,该属性是一个元组,定义了每一种场景下不同的变量的值。比如说你测试一段数据访问代码,你需要测试该代码在使用不同的驱动时,比如MongoDB、SQL、File,是否都能正常工作。我们有三种办法:

    • 最笨的办法是为不同的驱动把同一个测试用例编写3遍。
    • 比较好的办法是,编写一个统一的非测试用例方法,接收 driver 作为参数,执行测试逻辑,然后再分别编写三个测试用例方法去调用这个非测试用例方法。
    • 更好的办法就是使用 testscenarios 模块,定义好 scenarios 变量,然后实现一个测试用例方法。
  • subunit: 是一个用于传输单元测试结果的流协议。一般来说,运行单元测试的时候是把单元测试的结果直接输出到标准输出,但是如果运行大量的测试用例,这些测试结果就很难被分析。因此就可以使用 python-subunit 模块来运行测试用例,并且把测试用例通过 subunit 协议输出,这样测试结果就可以被分析工具聚合以及分析。python-subunit 模块自带了一些工具用来解析 subunit 协议,比如你可以这样运行测试用例: $ python -m subunit.run test_module | subunit2pyunitsubunit2pyunit 命令会解析 subunit 协议,并且输出到标准输出。

  • testrepository: OpenStack 中使用 testrepository 模块管理单元测试用例。当一个项目中的测试用例很多时,如何更有效的处理单元测试用例的结果就变得很重要。testrepository 的出现就是为了解决这个问题。testrepository 使用 python-subunit 模块来运行测试用例,然后分析 subunit 的输出并将测试结果进行记录到本地文件。举例来说,testrepository 允许你做这样的事情:

    • 知道哪些用例运行时间最长
    • 显示运行失败的用例
    • 重新运行上次运行失败的用例
  • coverage: 是用来计算代码运行时的覆盖率的,也就是统计多少代码被执行了。它可以和 testrepository 一起使用,用来统计单元测试的覆盖率,在运行完单元测试之后,输出覆盖率报告。

  • tox: 是用来管理和构建虚拟环境(virtualenv)的。对于一个项目,我们需要运行 Python 2.7 的单元测试,也需要运行 Python 3.4 的单元测试,还需要运行 PEP8 的代码检查。这些不同的任务需要依赖不同的库,所以需要使用不同的虚拟环境。使用 tox 的时候,我们会在 tox 的配置文件 tox.ini 中指定不同任务的虚拟环境名称,该任务在虚拟环境中需要安装哪些包,以及该任务执行的时候需要运行哪些命令。

测试过程

  1. 设置虚拟环境: 使用 tox 来管理测试运行的虚拟环境。
  2. 执行单元测试: tox 调用 testrepository(testr) 来执行单元测试用例。
  3. 测试结果管理: testrepository(testr) 调用 subunit 来执行测试用例,对测试结果进行聚合和管理。
  4. 计算执行代码量: testrepository 调用 coverage 来执行代码覆盖率的计算。

tox 构建虚拟环境

大部分情况下,我们都是通过 tox 命令来执行单元测试的,并且传递环境名称给tox命令, EXAMPLE:

fanguiju@fanguiju:/opt/stack/keystone$ tox -e pep8
fanguiju@fanguiju:/opt/stack/keystone$ tox -e py27

tox 命令首先会读取项目根目录下的 tox.ini 文件(/opt/stack/keystone/tox.ini),获取相关的信息,然后根据配置构建 virtualenv, EXAMPLE:

[tox]      
minversion = 1.6
skipsdist = True
envlist = py27,py34,pep8,docs,genconfig,releasenotes 

这段配置文件列出了该项目执行单元测试时, 所能构建的虚拟环境, 并且 tox 会将构建好的虚拟环境保存在 .tox/ 目录下,以环境名称命名, EXAMPLE:

fanguiju@fanguiju:/opt/stack/keystone/.tox$ pwd
/opt/stack/egis/.tox
fanguiju@fanguiju:/opt/stack/keystone/.tox$ ll
total 20
drwxr-xr-x  5 root     root     4096 Oct 26 09:37 ./
drwxrwxr-x 12 fanguiju fanguiju 4096 Oct 13 19:53 ../
drwxr-xr-x  2 root     root     4096 Oct 26 09:37 log/
drwxr-xr-x  9 root     root     4096 Oct 13 19:53 pep8/
drwxr-xr-x  9 root     root     4096 Oct 13 19:52 py27/

除了 log目录, 上面的 pep8py27 目录在执行了之前的单元测试指令后生成, 作为一个虚拟环境.

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之后要执行的命令,这里调用了来执行测试
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

setup.cfg 文件中的 extras 依赖:

[extras]
ldap =
  python-ldap>=2.4:python_version=='2.7'
  ldappool>=1.0:python_version=='2.7' # MPL
memcache =
  python-memcached>=1.56
mongodb=
  pymongo>=3.0.2                                                           
bandit =
  bandit>=0.13.2

testrepository 管理测试执行

tox 从 tools/pretty_tox.sh 这个脚本开始调用 testr 来执行单元测试. testr 本身的配置文件为 .testr.conf .

tools/pretty_tox.sh 脚本:

#!/usr/bin/env bash

set -o pipefail

TESTRARGS=$1

python setup.py testr --testr-args="--subunit $TESTRARGS" | subunit-trace -f
# testr 和 setuptools 已经集成,所以可以通过 setup.py testr 命令来执行
# --testr-args 表示传递给 testr 命令的参数,告诉 testr 要传递给 subunit 的参数
# subunit-trace 是 os-testr 包中的命令(os-testr 是 OpenStack 的一个项目),用来解析 subunit 的输出。

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

.testr.conf: testr 配置文件:

[DEFAULT]
test_command=
    OS_STDOUT_CAPTURE=${OS_STDOUT_CAPTURE:-1} \
    OS_STDERR_CAPTURE=${OS_STDERR_CAPTURE:-1} \
    OS_LOG_CAPTURE=${OS_LOG_CAPTURE:-1} \
    ${PYTHON:-python} -m subunit.run discover -t ./ ${OS_TEST_PATH:-./keystone/tests/unit} $LISTOPT $IDOPTION
# test_command 设置了要执行什么命令来执行测试用例, 这里使用的是 subunit.run

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}