我们的第一次迭代是为所有后续迭代做好准备,以及在项目最初完成 bug 修复、维护、新功能请求等之后的任何开发工作。这类准备工作将需要为任何新的开发工作在某种预期的复杂程度上进行,但它可能不会被分解成自己的迭代。创建许多基础结构可以作为其他迭代的一部分进行管理;例如,在需要的第一次开发开始时创建项目结构。采用这种方法的代价是,随着后期开发的展开,早期定义工作很可能会发生重大变化,因为原始结构无法适应多个 Python 虚拟环境,或者无法向系统的代码库添加新项目。
Having some standard structural definitions, like the ones from Chapter 6, Development Tools and Best Practices, will minimize a fair number of these concerns going forward, but may not prevent them.
本章将介绍大多数项目常见的设置和准备项目:
- 源代码管理(单片机)
- 项目组织
- 单元测试结构
- 构建和部署流程
因此,本次迭代的可交付成果主要集中在以下方面:
- 主存储库,存储在 Git 服务器或服务(例如,本地服务器、GitHub 或 Bitbucket)中,其中包含系统及其组件项目的完整、空的项目结构
- 系统中每个可部署类库或应用程序的组件项目
- 可以执行的单元测试套件,其执行通过系统中的每个组件项目
- 每个组件项目的构建过程——也是可执行的——会产生一个可部署的包,即使该包开始时基本上是无用的
开发人员的需求也可以表示为故事,其中包含要执行的任务。这些基本故事可以在多个项目中重用,如果是,则可能会随着时间的推移而演变,以便更好地捕获跨开发工作的共同需求和目标,即使是对于完全不同的系统。这些应该足以作为现在的起点:
-
作为一名开发人员,我需要知道如何管理和控制系统的源代码,以便能够适当地保存/存储我编写的代码:
- 为系统创建空白 SCM 存储库-
hms_sys - 使用持续使用所需的基线信息和文档填充存储库
- 建立并分发开发团队成员访问存储库所需的任何凭据
- 为系统创建空白 SCM 存储库-
-
作为一名开发人员,我需要知道系统的完整结构是什么样的,至少在高层次上是这样的,这样我才能编写适合该结构的代码。这将涉及:
- 分析用例以及逻辑和物理架构,以定义组件项目的需求及其结构
- 为确定的每个组件项目建立标准项目起点
- 为完成源代码包构建的每个组件项目实现最小的
setup.py - 确定是否为组件项目使用 Python 虚拟环境,实现它们,并记录如何复制它们
-
作为开发人员,我需要知道如何以及在何处为代码库编写单元测试,以便在编写代码后创建单元测试。我还需要确保代码经过彻底测试:
- 定义单元测试标准/需求(覆盖率、按类型划分的标准值等)
- 实施执行这些标准的机制
- 定义单元测试代码将驻留在组件项目结构中的位置
- 对执行时没有任何故障的每个组件项目实施基本的顶级测试
-
作为开发人员,我需要知道如何将组件项目的单元测试集成到该组件项目的构建过程中,以便构建可以自动执行单元测试,这包括:
- 确定如何将单元测试集成到构建过程中;和
- 确定如何处理不同环境的构建/测试集成
由于本次迭代中需要进行的活动的平衡最终需要存储在 SCM 中,因此将要执行的列表中的第一个故事及其任务如下:
-
作为一名开发人员,我需要知道如何管理和控制系统的源代码,以便能够适当地保存/存储我编写的代码:
- 为系统创建一个空白的 SCM 存储库-
hms_sys - 使用持续使用所需的基线信息和文档填充存储库
- 建立并分发开发团队成员访问存储库所需的任何凭据
- 为系统创建一个空白的 SCM 存储库-
hms_sys的代码将存在于 Bitbucket(中 https://bitbucket.org ),在 Git 存储库中,所以第一步是在那里建立一个新的存储库:
新存储库的设置如下所示:
-
所有者:拥有存储库的用户。如果多个用户可以通过 Bitbucket 帐户访问存储库,或者存在与其关联的组,则这些用户和组将作为此设置的选项提供。
-
存储库名称:存储库的(必需)名称。理想情况下,存储库名称应该很容易与它所包含的系统或项目相关联,并且因为
hms_sys是整个项目的名称,并且它还没有被采用,所以使用了它。 -
访问级别:确定存储库是公共的还是私有的。由于
hms_sys不打算公开阅读或分发,因此该存储库已被私有化。 -
包括自述文件?:系统是否将创建
README文件作为创建过程的一部分。方案如下:- 否:以后需要手动创建文件(如果需要)。
- 是的,使用模板:使用最少的信息创建基本文件。选择此选项是为了创建一个基本的
README文件。 - 是的,有教程(针对初学者)。
-
版本控制系统:允许存储库使用 Git 或 Mercurial 作为其 SCM 引擎。选择 Git 是因为我们决定使用它。
必须扩展高级设置才能使用,如下所示:
-
说明:如果选择了“是”,则此处提供的任何说明都将添加到
README文件中,并带有模板选项。 -
分叉:控制是否允许/如何从存储库中分叉。方案如下:
- 允许分叉:任何具有访问权限的人都可以分叉存储库
- 只允许私人叉子
- 没有叉子
-
项目管理:允许将问题跟踪和 wiki 系统与存储库集成。
-
语言:为存储库中的代码指定主编程语言。此设置除了按存储库的主要语言对存储库进行分类外,不做任何事情,至少在最初是这样。不过,一些 SCM 提供程序将使用语言设置来使用通常被忽略的文件模式预先填充 Git 的
.gitignore文件,因此,如果可能的话,最好指定它。
单击“创建存储库”按钮后,将创建存储库:
在任何存储库的概述页面中,都可以使用 HTTPS 和 SSH 选项连接到存储库并克隆/拉取存储库,任何具有必要权限的人都可以将其克隆(无论首选何种方式)到本地副本以使用它:
There are several ways to initialize a new Git repository. This process, starting at the repository's provider, assures that the repository is well-formed and accessible, as well as allowing for some initial configuration and documentation setup that won't have to be done by hand later.
此时,故事中的两个任务已解决:
-
为系统创建一个空白的 SCM 回购-
hms_sys。 -
建立并分发开发团队成员访问存储库所需的任何凭据。由于存储库是通过外部服务提供商的界面创建的,因此访问所需的凭据将在那里进行管理,并且其用户帐户与存储库的帐户或组关联的任何人都可以获得他们所需的访问权限,或者可以通过提供商系统中的用户管理获得访问权限。
剩余的任务包含基线信息和持续使用所需的文档,与项目结构有关联,但尚未解决,但仍有一些项目可以解决,这些项目独立于此。
首先是在顶级存储库目录中创建和记录基本组件项目。最初,创建一个包含整个系统代码库的顶级项目可能是一个好主意。这将提供一个单独的项目,可用于组织跨两个或多个组件项目的项目,以及包含整个系统的任何项目。
在 Geany 中,这是通过使用 Project 来实现的→ 新建,提供项目名称、项目文件路径和项目的基本路径:
由于 Geany 项目文件存储的文件系统路径可能因机器而异,因此需要将这些路径添加到 Git 的.gitignore文件中:
# .gitignore for hms_sys project
# Geany project-files
*.geanyThe .gitignore file is, ultimately, a list of files and/or folders that Git will ignore when committing or pushing code to the central repository. Any file or folder that matches one of the paths in .gitignore will not be tracked by the SCM.
此外,创建本地hms_sys.geany文件的说明可能应该记录在案,以便任何其他需要创建本地hms_sys.geany文件的开发人员可以根据需要创建本地hms_sys.geany文件。此类信息可以放入README.md文件中,并在添加系统的组件项目时进行类似的工作:
# hms_sys
The system-level repository for the hms_sys project, from "Hands On
Software Engineering with Python," published by Packt.
## Geany Project Set-up
Geany project-files (`*.geany`) are in the `.gitignore` for the entire
repository, since they have filesystem-specific paths that would break
as they were moved from one developer's local environment to another.
Instructions for (re-)creating those projects are provided for each.
### HMS System (Overall) -- `hms_sys.geany`
This is an over-arching project that encompasses *all* of the component
projects. It can be re-created by launching Geany, then using
Project → New and providing:
* *Name:* HMS System (Overall)
* *Filename:* `[path-to-git-repo]/hms_sys/hms_sys.geany`
* *Base path:* `[path-to-git-repo]/hms_sys`一旦这些更改被转移、本地提交并推送到主存储库,应该出现的是一个修订的README.md文件和一个新的.gitignore,而不是hms_sys.geany项目文件:
随着组件项目被添加到代码库中,应该遵循相同的文档和设置,从而产生类似的结果。在这一点上,第一个故事的最终任务是尽可能完整的,因此,如果它被判断为完整和批准,它将被审查和关闭。
那么,请转到下一个故事:
-
作为一名开发人员,我需要知道系统的完整结构是什么样的,至少在较高的层次上是这样的,以便我能够编写适合该结构的代码:
- 分析用例、逻辑和物理架构,以定义组件项目的需求及其结构
- 为确定的每个组件项目建立标准项目起点
- 为完成源代码包构建的每个组件项目实现最低限度的
setup.py
逻辑架构以及第 6 章、开发工具和最佳实践中的用例图指出了需要考虑的三个明显的组件项目,其中一个用于以下方面:
- 工匠应用
- Artisan 网关
- 审核/管理申请
反过来,这些组件项目中的每一个都需要访问一些常见的对象类型,它们都需要能够处理产品实例,并且其中大多数还需要能够处理Artisan和Order实例:
很可能还有其他业务对象没有从这次突破中立即显现出来,但事实上,有任何业务对象都是一个很好的迹象,表明可能需要第四个组件项目来收集提供这些业务对象及其功能的代码。考虑到这一点,最初的组件项目结构可以归结为:
-
HMS 核心
hms-core:一个类库,收集所有基线业务对象定义,提供对象表示,如工匠、产品和订单 -
中央办公室应用程序(
hms-co-app):提供一个可执行的应用程序,允许中央办公室工作人员执行需要与技工就产品、订单以及其他项目进行沟通的各种任务 -
Artisan 应用程序
hms-artisan:提供一个可执行的本地应用程序,允许Artisan管理产品和订单,并根据需要与中央办公室进行通信 -
HMS Artisan Gateway(
hms-gateway):提供Artisan应用程序和中央办公室应用程序用于在 Artisan 和中央办公室之间来回发送信息的可执行服务
关于hms-core代码将如何包含在其他需要它的项目的发行版中,稍后将不得不做出一些决定,但在达成这些决定之前,不需要解决这些问题,因此暂时将其搁置。同时,下一步是为每个组件项目设置起点项目结构。目前,所有四个组成项目的基本结构都是相同的;唯一的区别在于不同文件和目录的名称。
以hms-core为例,因为这是开始工作的第一个逻辑代码集,所以项目结构如下所示:
为一个项目设置最低标准的 Python 打包,并提供基本的构建过程,对前面讨论过的基线setup.py和Makefile文件只做很少的更改。在开始编写代码之前,只有几个细节可用:setup.py将使用的主包的包名和顶级目录,以及可以添加到Makefile的setup.py文件本身。Makefile更改是最简单的:
# Makefile for the HMS Core (hms-core) project
main: test setup
# Doesn't (yet) do anything other than running the test and
# setup targets
setup:
# Calls the main setup.py to build a source-distribution
# python setup.py sdist
test:
# Executes the unit-tests for the package, allowing the build-
# process to die and stop the build if a test failssetup.py文件虽然已经填充了一些起始数据和信息,但仍然与我们之前看到的基本起始点文件基本相同:
#!/usr/bin/env python
from setuptools import setup
# The actual setup function call:
setup(
name='HMS-Core',
version='0.1.dev0',
author='Brian D. Allbee',
description='',
package_dir={
'':'src',
# ...
},
# Can also be automatically generated using
# setuptools.find_packages...
packages=[
'hms_core',
# ...
],
package_data={
# 'hms_core':[
# 'filename.ext',
# # ...
# ]
},
entry_points={
# 'console_scripts':[
# 'executable_name = namespace.path:function',
# # ...
# ],
},
)这个结构将不会获取核心包之外的任何目录和文件,目前也没有迹象表明需要它们中的任何一个,因此它们的包含将被保留,直到有实际需要为止。即使没有这些,setup.py文件也可以成功构建和安装源分发包,尽管它在构建过程中会抛出一些警告,并且安装的包还没有提供任何功能:
在大型(或至少更正式的结构)开发车间中,组件项目的构建/打包过程可能需要适应不同环境的不同构建:
- 本地环境,例如开发人员的本地机器
- 共享开发环境,所有开发人员的本地代码更改首先混合在一起
- 用于 QA 和更广泛的集成测试的共享测试服务器
- 用户验收测试服务器,使用真实的、类似生产的数据,可用于向需要最终批准将更改升级到实时环境或构建的用户演示功能
- 可以完全访问生产数据完整副本的暂存环境,着眼于能够执行需要访问该数据集的负载和其他测试
- 实时环境/构建代码库
至少有一些可能需要在这些不同构建之间进行重大区分(local、dev、test、stage和live,假设用户验收构建暂时与阶段构建相同)。然而,在开发工作的这一点上,确实没有什么可以区分的,所以最好的办法是围绕需要时会发生什么进行规划。
在任何给定环境需要完全不同的包结构之前,当前的setup.py文件将保持不变。不太可能存在并非在所有环境中都常见的特定于环境的需求。如果确实出现这种需求,那么方法将是为具有任何不同需求的每个环境创建不同的setup.py,并手动或通过Makefile执行特定的setup.py。经过一定的谨慎和思考,这应该允许以合理的标准方式将任何特定于环境的差异包含在单个位置中。
这反过来意味着必须对Makefile进行更改。具体来说,每个特定于环境的构建过程都需要有一个目标(dev到live再次),以及管理特定于其中一个环境的文件的某种方式。由于make流程可以操作文件、创建目录等,因此将使用的策略是执行以下操作:
- 通过在特定于环境的文件前面加上与之相关的生成目标/环境名称来标识这些文件。例如,代码库中有一个
dev-setup.py文件,还有一个test-setup.py文件,依此类推。 - 修改
Makefile以复制项目代码树中的所有相关文件,这些文件可以在不影响核心项目文件的情况下进行修改(和销毁)。 - 添加一个进程,该进程将根据特定环境的生成的需要在临时副本中查找并重命名所有特定于环境的文件,并从临时树中删除与生成无关的所有特定于环境的文件。
- 正常执行
setup.py文件。
对Makefile所做的更改看起来是这样的,至少作为一个起点。
首先,定义一个公共的临时构建目录,本地构建将是默认的,并将简单地执行标准的setup.py文件,就像原始流程所做的那样:
# Makefile for the HMS Core (hms-core) project
TMPDIR=/tmp/build/hms_core_build
local: setup
# Doesn't (yet) do anything other than running the test and
# setup targets
setup:
# Calls the main setup.py to build a source-distribution
~/py_envs/hms/core/bin/python setup.py sdist
unit_test:
# Executes the unit-tests for the package, allowing the build-
# process to die and stop the build if a test fails
~/py_envs/hms/core/bin/python setup.py test创建一个新目标build_dir,以创建临时生成目录,并将任何生成的所有项目文件复制到其中:
build_dir:
# Creates a temporary build-directory, copies the project-files
# to it.
# Creating "$(TMPDIR)"
mkdir -p $(TMPDIR)
# Copying project-files to $(TMPDIR)
cp -R bin $(TMPDIR)
cp -Ret cetera$(TMPDIR)
cp -R src $(TMPDIR)
cp -R var $(TMPDIR)
cp setup.py $(TMPDIR)将写入每个环境的准备目标以及每个环境的最终目标,以便根据需要重命名和删除文件,并执行临时构建目录中的setup.py文件:
dev_prep:
# Renames any dev-specific files so that they will be the "real"
# files included in the build.
# At this point, there are none, so we'll just exit
dev: unit_test build_dir dev_prep
# A make-target that generates a build intended to be deployed
# to a shared development environment.
cd $(TMPDIR);~/py_envs/hms/core/bin/python setup.py sdist因此,当针对该Makefile执行make dev时,dev目标运行unit_test目标,然后使用build_dir目标创建项目的临时副本。之后,dev_prep用于处理文件名更改和从其他环境中删除文件。然后,也只有到那时,它才会执行剩余的setup.py。
要解决的最后一项任务是确定是否为各种组件项目使用 Python 虚拟环境,在需要时创建它们,并记录如何创建它们,以便其他开发人员能够在需要时复制它们。
考虑到组件项目的结构、对它们的了解以及它们安装的代码与其他系统成员的交互方式,显然不需要不同的环境,甚至建立它们也没有明显的优势。如果在开发过程中采取了足够的谨慎和纪律,确保将依赖关系添加到每个组件项目的setup.py或其他构建过程工件或配置中,可能出现的最坏情况是,在执行测试安装的过程中会发现缺少的依赖项。在一个没有 bug 的实时安装中,hms-gateway项目可能会出现一些微不足道的低效,例如,可能会安装它不需要或不使用的数据库或 GUI 库,或者这两个组件项目可能都有其他用户安装但不需要的消息系统库。
这些都不会对单个组件项目安装的运行造成任何迫在眉睫的威胁,但它们确实会向安装中抛出不必要的代码。如果不仔细观察和管理,不必要的库安装可能会严重爬行,这可能是未来安全问题的一个载体。更糟糕的是,任何潜在的安全问题都可能因此不可见;如果没有人真正意识到某个给定的程序安装了一些不需要的东西,那么它可能无法得到修复,直到为时已晚。
One of the first best steps that can be taken to keep systems secure is to assure that they only have exactly what they need to function installed. That won't cover every possibility, but it will reduce the bandwidth needed to keep current with patches and security issues.
跟踪各个项目之间的依赖关系是虚拟环境可以发挥作用的事情。这一点有利于为每个项目单独设置它们。支持这种做法的另一点是,一些平台,如各种公共云,将需要能够在其部署过程中包含依赖包,而虚拟环境将使这些包与核心系统安装包集很好地分离。在这方面,虚拟环境也是一种经得起未来考验的环境。
在开发hms_sys的背景下,我们将为每个组件项目建立一个单独的虚拟环境。如果以后证明它们是不必要的,则始终可以删除它们。创建、激活和取消激活这些命令的过程非常简单,可以在任何方便的地方创建这些命令—实际上没有任何标准位置—尽管命令因操作系统而异,如下所示:
| 虚拟环境活动 | 操作系统 |
| Linux/MacOS/Unix | 窗户 |
| 创建 | python3 -m venv ~/path/to-myenv | c:\>c:\Python3\python -m venv c:\path\to\myenv |
| 激活 | source ~/path/to-myenv/bin/activate | C:\> c:\path\to\myenv\Scripts\activate.bat |
| 停用 | deactivate | C:\> c:\path\to\myenv\Scripts\deactivate.bat |
Once a virtual environment is created and activated, packages can be installed in it with pip (or pip3), just like outside the virtual environment's context. Installed packages are stored in the virtual environment's libraries, instead of in the global system libraries.
记录哪些虚拟环境与哪些组件项目关联只是将创建虚拟环境所需的命令复制到某个项目级文档中。对于hms_sys,这些将存储在每个组件项目的README.md文件中。
让我们回顾一下本故事的任务:
- 分析用例以及逻辑和物理架构,以定义组件项目需求和结构-完成
- 为确定的每个组件项目建立标准项目起点-完成
- 为完成源代码包构建的每个组件项目实现一个最小的
setup.py文件—完成 - 确定是否将 Python 虚拟环境用于组件项目,实现它们,并记录如何复制它们-完成
- 提供单元测试结构
在上一章的末尾,有人指出,尽管已经设定了一个期望,即所有代码都将进行单元测试,模块和类的所有公共成员都将遵守该要求,但也有人指出,尚未定义测试策略细节,这是本次迭代中单元测试故事的重要部分:
-
作为开发人员,我需要知道如何以及在何处为代码库编写单元测试,以便在编写代码后创建单元测试。我还需要确保代码经过彻底测试:
- 定义单元测试标准/需求(覆盖率、按类型划分的标准值等)
- 实施执行这些标准的机制
- 定义单元测试代码将驻留在组件项目结构中的位置
- 对执行时没有任何故障的每个组件项目实施基本的顶级测试
The bulk of this unit testing material was adapted and converted into Python 3 from Python 2.7.x code and a discussion of this is on the author's blog (starting at bit.ly/HOSEP-IDIC-UT). Though that code was written for an older version of Python, there may be additional insights to be gained from the unit testing articles there.
可以说,所有成员,而不仅仅是公众成员,都应该接受测试。毕竟,如果相关代码在任何地方都被使用,那么就可预测的行为而言,它也应该遵守相同的标准,是吗?从技术上讲,没有理由不能做到这一点,尤其是在 Python 中,受保护和私有类成员不是真正受保护的或私有的。在早期版本的 Python 中,受保护的成员是可访问的,私有成员(前缀为两个下划线:__private_member)在派生类中无法直接访问,除非通过其损坏的名称调用它们。在 Python3 中,名义上受保护的作用域或私有作用域没有语言级别的强制,即使名称 mangling 仍然存在。这很快就得到了证明。考虑下面的类定义:
class ExampleParent:
def __init__(self):
pass
def public_method(self, arg, *args, **kwargs):
print('%s.public_method called:' % self.__class__.__name__)
print('+- arg ...... %s' % arg)
print('+- args ..... %s' % str(args))
print('+- kwargs ... %s' % kwargs)
def _protected_method(self, arg, *args, **kwargs):
print('%s._protected_method called:' % self.__class__.__name__)
print('+- arg ...... %s' % arg)
print('+- args ..... %s' % str(args))
print('+- kwargs ... %s' % kwargs)
def __private_method(self, arg, *args, **kwargs):
print('%s.__private_method called:' % self.__class__.__name__)
print('+- arg ...... %s' % arg)
print('+- args ..... %s' % str(args))
print('+- kwargs ... %s' % kwargs)
def show(self):
self.public_method('example public', 1, 2, 3, key='value')
self._protected_method('example "protected"', 1, 2, 3, key='value')
self.__private_method('example "private"', 1, 2, 3, key='value')如果我们创建一个ExampleParent实例,并调用其show方法,我们希望看到所有三组输出,这就是实际情况:
如果使用dir(ExampleParent)检查ExampleParent类结构,则可以看到所有三种方法:[’_ExampleParent__private_method,…,“_protected_method,“public_method,…]。在 Python 的早期版本中,从ExampleParent派生的类仍然可以访问public_method和_protected_method,但如果使用该名称调用__private_method,则会引发错误。在 Python3(以及 Python2.7.x 的一些更高版本)中,情况不再如此。
class ExampleChild(ExampleParent):
passCreating an instance of this class, and calling its show method yields the same results:
从技术上讲,Python 类的所有成员都是公共的。
那么,如果所有类成员都是公共的,那么从定义单元测试策略的角度来看,这意味着什么呢?如果遵守公共/受保护/私人公约,则以下规定适用:
- 公共成员应在与其所定义的类(其原始类)相对应的测试套件中进行测试
- 大多数受保护的成员可能是由派生类继承的,应该在与它们所定义的类相对应的测试套件中进行深入测试
- 私有成员应被视为在其原始类之外根本不可访问的私有成员,或者被视为在没有警告的情况下进行破坏性更改的实现细节
- 继承的成员不需要再次进行任何测试,因为它们将根据其原始类进行测试
- 从父类重写的成员将在与其重写的类相关的套件中进行测试
建立一个应用所有这些规则的单元测试过程是可能的,尽管它相当复杂,足够重要,能够将它封装在某种可重用的函数或类中是非常有利的,这样就不必在每个测试过程中重新创建它,或者,如果测试策略发生更改,则可以跨数十个或数百个副本进行维护。最终的目标是拥有一个可重复的测试结构,该结构可以快速、容易地实现,这意味着它也可以像模块和包头在早期一样被模板化。
不过,首先,我们需要一些测试。具体地说,我们需要具有属于前面提到的类别的方法的类:
- 局部定义
- 继承自父类
- 从父类重写
这涵盖了所有公共/受保护/私有选项。虽然前面没有特别提到,但我们也应该包括一个至少有一个抽象方法的类;他们只是还没有被解决。虽然它们应该返回可测试的值,但它们不需要非常复杂来说明测试过程。考虑到所有这些,下面是一组简单的类,我们将使用这些类进行测试并生成核心测试过程:
These files are in the hms_sys code base, in the top-level scratch-space directory.
import abc
class Showable(metaclass=abc.ABCMeta):
@abc.abstractmethod
def show(self):
pass
class Parent(Showable):
_lead_len = 33
def __init__(self, arg, *args, **kwargs):
self.arg = arg
self.args = args
self.kwargs = kwargs
def public(self):
return (
('%s.arg [public] ' % self.__class__.__name__).ljust(
self.__class__._lead_len, '.') + ' %s' % self.arg
)
def _protected(self):
return (
('%s.arg [protected] ' % self.__class__.__name__).ljust(
self.__class__._lead_len, '.') + ' %s' % self.arg
)
def __private(self):
return (
('%s.arg [private] ' % self.__class__.__name__).ljust(
self.__class__._lead_len, '.') + ' %s' % self.arg
)
def show(self):
print(self.public())
print(self._protected())
print(self.__private())
class Child(Parent):
pass
class ChildOverride(Parent):
def public(self):
return (
('%s.arg [PUBLIC] ' % self.__class__.__name__).ljust(
self.__class__._lead_len, '.') + ' %s' % self.arg
)
def _protected(self):
return (
('%s.arg [PROTECTED] ' % self.__class__.__name__).ljust(
self.__class__._lead_len, '.') + ' %s' % self.arg
) def __private(self):
return (
('%s.arg [PRIVATE] ' % self.__class__.__name__).ljust(
self.__class__._lead_len, '.') + ' %s' % self.arg
)创建每个具体类的快速实例,并调用每个实例的show方法,显示预期结果:
内置的unittest模块支持 Python 中的单元测试。可能还有其他模块也提供单元测试功能,但unittest随时可用,默认情况下安装在 Python 虚拟环境中,并提供我们所需的所有测试功能,至少作为起点。前面的类的初始测试模块非常简单,即使它只定义应用于被测试代码的测试用例类:
#!/usr/bin/env python
import unittest
class testShowable(unittest.TestCase):
pass
class testParent(unittest.TestCase):
pass
class testChild(unittest.TestCase):
pass
class testChildOverride(unittest.TestCase):
pass
unittest.main()每个以test开头的类(以及从unittest.TestCase派生的类)都将通过模块末尾的unittest.main()调用实例化,并且将执行那些名称也以test开头的类中的每个方法。如果我们向其中一个添加测试方法,例如testParent ,并按如下方式运行测试模块:
class testParent(unittest.TestCase):
def testpublic(self):
print('### Testing Parent.public')
def test_protected(self):
print('### Testing Parent._protected')
def test__private(self):
print('### Testing Parent.__private')试验方法的执行情况如下所示:
如果将print()调用替换为 pass,如下面的代码所示,则输出会更简单,为每个测试用例的测试方法打印一个周期,该周期执行时不会引发错误:
class testParent(unittest.TestCase):
def testpublic(self):
pass
def test_protected(self):
pass
def test__private(self):
passWhen executed, this yields the following:
到目前为止,一切顺利;我们有可以执行的测试,所以下一个问题是如何应用我们想要应用的测试策略规则。第一个策略,每个源模块都有一个测试模块,是项目结构的一个方面,而不是与测试执行过程相关的方面。为了解决这个问题,我们真正需要做的就是定义测试代码在任何给定项目中的位置。因为我们知道我们将要在以后的构建过程中处理运行测试的问题,所以我们需要有一个公共测试目录,一个可以按需运行所有项目测试的文件(称为run_tests.py),以及一个可以访问该文件的测试目录和文件结构,对于hms_core组件项目,其结果如下所示:
前面提到的测试目标的平衡都要求能够检查正在测试的代码,以便识别需要测试的模块成员以及这些成员的成员。这听起来可能令人望而生畏,但 Python 提供了一个专门用于此目的的模块:inspect。它提供了一个非常健壮的函数集合,可用于在运行时检查 Python 代码,可利用这些函数生成成员名称集合,这些成员名称可用于确定高级测试覆盖率是否符合我们正在建立的标准。
为了便于说明,我们需要测试的前面的类将保存在一个名为me.py的模块中,这使它们可以导入,并且演示查找me模块所需信息的过程的每个步骤都将收集在inspect_me.py中,如图所示。相应的测试用例将存在于test_me.py中,它将作为一个几乎为空的文件开始,首先不会在那里定义测试用例类。
第一步是确定me的目标成员,我们需要为其提供测试用例类。目前,我们只需要目标模块中的类列表,可以按如下方式检索这些类:
#!/usr/bin/env python
import inspect
import me as target_module
target_classes = set([
member[0] for member in
inspect.getmembers(target_module, inspect.isclass)
])
# target_classes = {
# 'Child', 'ChildOverride', 'Parent', 'Showable'
# } at this point一步一步地,发生的事情是:
-
inspect模块正在导入。 -
me模块正在被导入,使用target_module作为其默认模块名称的覆盖。我们希望能够保持导入的模块名称可预测且相对恒定,以便于后续重用,从这里开始。 -
对
target_module调用inspect的getmembers函数,使用isclass作为过滤谓词。这将返回类似于('ClassName', <class object>)的元组列表。这些结果通过列表理解来提取类名,然后将该列表交给 Pythonset来生成一组正式的类名。
Python's set type is a very useful basic data type it provides an iterable collection of values that are distinct (never repeated in the set), and that can be merged with other sets (with union), have its members removed from other sets (with difference), and a host of other operations that would be expected from standard set theory.
使用这些名称,创建一组预期的测试用例类名非常简单:
expected_cases = set([
'test%s' % class_name
for class_name in target_classes
]
)
# expected_cases = {
# 'testChild', 'testShowable', 'testChildOverride',
# 'testParent'
# } at this point这只是另一个列表理解,它从目标类名集中构建了一组以test开头的类名。可以使用与收集目标模块中的类名类似的方法来查找test_me.py模块中存在的测试用例类:
import unittest
import test_me as test_module
test_cases = set([
member[0] for member in
inspect.getmembers(test_module, inspect.isclass)
if issubclass(member[1], unittest.TestCase)
])
# test_cases, before any TestCase classes have been defined,
# is an empty set除了对找到的每个成员进行issubclass检查,将集合的成员限制为从unittest.TestCase派生的类的名称之外,这与构建初始target_classes集合的过程相同。现在我们有了收集预期内容和实际定义内容的集合,确定需要创建哪些测试用例类只需从预期的集合中删除已定义的测试用例名称:
missing_tests = expected_cases.difference(test_cases)
# missing_tests = {
# 'testShowable', 'testChild', 'testParent',
# 'testChildOverride'
# }如果missing_tests不为空,则其名称集合表示需要创建的测试用例类名,以满足“所有成员都将被测试”策略的第一部分。现在只需简单打印一下结果即可:
if missing_tests:
print(
'Test-policies require test-case classes to be '
'created for each class in the code-base. The '
'following have not been created:\n * %s' %
'\n * '.join(missing_tests)
)确定了需要创建的缺少的测试用例类项后,可以将它们添加到test_me.py:
#!/usr/bin/env python
import unittest
class testChild(unittest.TestCase):
pass
class testChildOverride(unittest.TestCase):
pass
class testParent(unittest.TestCase):
pass
class testShowable(unittest.TestCase):
pass
if __name__ == '__main__':
unittest.main()一旦它们被添加(并且一旦子类从unittest.TestCase派生,因为在识别实际测试用例类之前执行了检查),就没有需要处理的缺失测试用例。
可以采用类似的方法来识别模块级功能,这些功能也应该进行测试。毕竟,它们也是模块的公共成员,这就是策略所关注的模块的公共成员。针对函数或任何其他可调用元素的测试的实际实现将遵循稍后为类方法建立的结构和过程。
实际上,在这种流程中,唯一不容易识别的公共成员是在模块级别创建的非托管属性模块常量或变量。虽然这些仍然可以测试,而且可以说应该测试,但事实上它们是非托管的,可以在运行时进行更改,而无需进行任何检查以确保它们不会在某个方面出现问题,这很可能会使围绕它们的任何正式测试策略只不过是浪费时间。也就是说,测试它们并没有什么害处,只要确保对它们的更改(无论是有意的还是意外的)不会被忽略,并在以后引发问题和 bug。
前面用于标识模块中类的inspect.getmembers函数也可以用于标识其他目标元素的其他成员类型,例如类的属性和方法。识别其中一个的过程类似于已经显示的识别模块中的类的过程,如下所示(对于属性):
target_class = target_module.Parent
target_properties = set([
member[0] for member in
inspect.getmembers(target_class, inspect.isdatadescriptor)
])
# target_properties = {'__weakref__'}与在模块中查找类的过程相比,这里唯一显著的区别是正在检查的目标(在本例中,target_class,我们将其设置为Parent类)和谓词(inspect.isdatadescriptor,后者将结果过滤为数据描述符、托管属性或形式属性。
在第 6 章中开发工具和最佳实践中,在讨论和定义各种内部代码标准时,使用托管属性/属性的一个方面对于单元测试非常重要:能够知道对任何给定属性使用哪种类型的值进行测试。这是采用这种方法的另一个优点:使用内置property()函数定义的类属性可以被检测为需要测试的类成员。非托管属性虽然可以很好地检测到,但可能不容易识别为需要测试的类的成员,而且这种识别几乎肯定不是可以自动化的。
类似的inspect.getmembers调用可用于识别类方法:
target_functions = set([
member[0] for member in
inspect.getmembers(target_class, inspect.isfunction)
])
target_methods = set([
member[0] for member in
inspect.getmembers(target_class, inspect.ismethod)
])
target_methods = target_methods.union(target_functions)
# target_methods = {
# '_Parent__private', 'public', 'show',
# '_protected', '__init__'
# }这两个成员名称集合都包含测试策略不需要测试的项,尽管__weakref__属性是所有类的内置属性,_Parent__private方法条目与原始__private方法关联,并且这两个都不需要包含在我们所需测试方法的列表中。一些基本的过滤可以通过简单地在属性列表名称中添加一个前导__的检查来完成(因为根据我们的测试策略,我们永远不会测试私有属性)。这将从测试列表中删除__weakref__,并允许公共和受保护的属性出现。
在向父级添加属性声明(prop)并添加过滤条件后,我们将得到以下结果:
target_properties = set([
member[0] for member in
inspect.getmembers(target_class, inspect.isdatadescriptor)
if not member[0].startswith('__')
])
# target_properties = {'prop'}但是,同样的方法在寻找需要测试的类方法时不起作用;一些常用的方法,例如__init__,具有基于名称的筛选将被删除的名称,但我们希望确保成员具有所需的测试。这种简单的基于名称的过滤也不包括存在于类中但未在该类中定义的成员名称,就像Child类的所有属性和成员一样。虽然基于名称的过滤是朝着正确方向迈出的一步,但感觉是时候退一步,看看更广泛的解决方案了,这个解决方案确实考虑了成员的定义位置。
这包括以更复杂的方式构建测试名称列表,并注意每个类的方法解析顺序(MRO),可以在类内置的__mro__属性中找到。我们将首先定义一个空集并获取该类的 MRO,然后是目标类中可用的相同属性名称列表:
property_tests = set()
sourceMRO = list(target_class.__mro__)
sourceMRO.reverse()
# Get all the item's properties
properties = [
member for member in inspect.getmembers(
target_class, inspect.isdatadescriptor)
if member[0][0:2] != '__'
]
# sourceMRO = [
# <class 'object'>, <class 'me.Showable'>,
# <class 'me.Parent'>
# ]我们还需要跟踪在哪里可以找到属性的定义,也就是说,它起源于哪个类,以及属性的实际实现。我们希望从每个类的完整数据结构开始,最终将名称与源类和实现关联起来,但首先要用None值进行初始化。这将允许最终结构在填充后用于标识未在其中定义的类成员:
propSources = {}
propImplementations = {}
for name, value in properties:
propSources[name] = None
propImplementations[name] = None
# Populate the dictionaries based on the names found
for memberName in propSources:
implementation = target_class.__dict__.get(memberName)
if implementation and propImplementations[memberName] != implementation:
propImplementations[memberName] = implementation
propSources[memberName] = target_class
# propImplementations = {
# "prop": <property object at 0x7fa2f0edeb38>
# }
# propSources = {
# "prop": <class 'me.Parent'>
# }
# If the target_class is changed to target_module.Child:
# propImplementations = {
# "prop": None # Not set because prop originates in Parent
# }
# propSources = {
# "prop": None # Also not set for the same reason
# }有了这些数据,所需特性测试方法列表的生成类似于前面所示的所需测试用例类列表:
property_tests = set(
[
'test%s' % key for key in propSources
if propSources[key] == target_class
]
)
# property_tests = {'testprop'}
# If the target_class is changed to target_module.Child:
# property_tests = set()获取和筛选类的方法成员的过程看起来几乎相同,尽管我们将包括所有成员,甚至是名称以__开头的成员,并获取函数或方法成员,只是为了确保我们将包括类的类和静态方法:
method_tests = set()
sourceMRO = list(target_class.__mro__)
sourceMRO.reverse()
# Get all the item's methods
methods = [
member for member in inspect.getmembers(
target_class, inspect.isfunction)
] + [
member for member in inspect.getmembers(
target_class, inspect.ismethod)
]构建用于跟踪方法源和实现的dict项的过程可以主动跳过本地、私有成员和任何定义为抽象的内容:
methSources = {}
methImplementations = {}
for name, value in methods:
if name.startswith('_%s__' % target_class.__name__):
# Locally-defined private method - Don't test it
continue
if hasattr(value, '__isabstractmethod__') and value.__isabstractmethod__:
# Locally-defined abstract method - Don't test it
continue
methSources[name] = None
methImplementations[name] = None测试名称列表生成的平衡是相同的,不过:
method_tests = set(
[
'test%s' % key for key in methSources
if methSources[key] == target_class
]
)
# method_tests = {
# 'testpublic', 'test__init__', 'test_protected',
# 'testshow'
# }
# If the target_class is changed to target_module.Child:
# method_tests = set()
# If the target_class is changed to target_module.Showable:
# method_tests = set()那么,这些探索的收获是什么呢?简而言之,它们如下:
- 可以自动化检测模块的哪些成员需要创建测试用例的过程
- 可以自动化验证与给定源模块对应的测试模块中是否存在这些所需测试用例的过程,尽管仍然需要一些规程来确保创建测试模块
- 可以自动化检测任何给定测试用例/源类组合需要哪些测试方法的过程,并且不需要测试私有和抽象成员,这两种方法在我们希望建立的测试策略上下文中都没有多大意义
不过,这是相当多的代码。80 多行,没有一些实际测试的班级成员和公告的问题,并剥离后的所有意见。这比复制和粘贴的代码多得多,特别是对于具有单元测试过程所具有的高破坏潜力或影响的过程。如果能把所有的东西都放在一个地方就好了。幸运的是,unittest模块的类提供了一些选项,可以让创建模块代码覆盖率测试变得非常简单,尽管它首先需要一些设计和实现。
一个好的单元测试框架不仅允许为代码元素的成员创建测试,还将提供在任何测试运行之前以及在所有测试成功与否执行之后执行代码的机制。Python 的unittest模块在单独的TestCase类中处理该问题,这允许该类实现setUpClass和tearDownClass方法,分别处理测试前和测试后的设置和拆卸。
因此,这意味着可以创建一个测试类,该测试类可以导入、使用特定于模块的属性进行扩展,并添加到一个测试模块中,该测试模块可以利用刚才显示的所有功能执行以下操作:
- 查找目标模块中的所有类和函数
- 确定测试模块中需要存在哪些测试用例类,并测试它们以确保它们存在
- 确定,对于每个源模块成员的测试用例类,为了满足我们的单元测试策略和标准,需要存在哪些测试
- 测试这些测试方法是否存在
代码覆盖率测试用例类需要知道要检查哪个模块才能找到所有这些信息,但它应该能够自己管理其他所有信息。最终,它将只定义一个自己的测试,并执行该测试,以确保源模块中的每个类或函数在测试模块中都有相应的测试用例类:
def testCodeCoverage(self):
if not self.__class__._testModule:
return
self.assertEqual([], self._missingTestCases,
'unit testing policies require test-cases for all classes '
'and functions in the %s module, but the following have not '
'been defined: (%s)' % (
self.__class__._testModule.__name__,
', '.join(self._missingTestCases)
)
)它还需要能够提供一种机制,允许检查属性和方法测试方法。如果能够实现的话,在完全自动化的基础上这样做是很有诱惑力的,但在某些情况下,这样做可能会带来麻烦,而不值得。至少在目前,这些测试的添加将通过创建一些装饰器来实现,这些装饰器将使将这些测试附加到任何给定的测试用例类变得容易。
Python's decorators are a fairly detailed topic in their own right. For now, don't worry about how they work just be aware of what using them looks like and trust that they do work.
我们的起点只是一个从unittest.TestCase派生的类,它定义了前面提到的setUpClass类方法,并对定义的类级别_testModule属性进行了一些初始检查。如果没有测试模块,那么所有测试都应该跳过或通过,因为没有测试:
class ModuleCoverageTest(unittest.TestCase):
"""
A reusable unit-test that checks to make sure that all classes in the
module being tested have corresponding test-case classes in the
unit-test module where the derived class is defined.
"""
@classmethod
def setUpClass(cls):
if not cls._testModule:
cls._missingTestCases = []
returnThe @classmethod line is a built-in class method decorator.
我们需要首先查找目标模块中可用的所有类和函数:
cls._moduleClasses = inspect.getmembers(
cls._testModule, inspect.isclass)
cls._moduleFunctions = inspect.getmembers(
cls._testModule, inspect.isfunction)我们将跟踪被测试模块的名称,作为类和函数成员的附加检查标准,以防万一:
cls._testModuleName = cls._testModule.__name__跟踪类和函数测试的机制类似于初始探索中的源代码和实现词典:
cls._classTests = dict(
[
('test%s' % m[0], m[1])
for m in cls._moduleClasses
if m[1].__module__ == cls._testModuleName
]
)
cls._functionTests = dict(
[
('test%s' % m[0], m[1])
for m in cls._moduleFunctions
if m[1].__module__ == cls._testModuleName
]
)所需测试用例类名列表是所有类和函数测试用例类名的汇总列表:
cls._requiredTestCases = sorted(
list(cls._classTests.keys()) + list(cls._functionTests.keys())
)实际测试用例类的集合稍后将用于测试:
cls._actualTestCases = dict(
[
item for item in
inspect.getmembers(inspect.getmodule(cls),
inspect.isclass)
if item[1].__name__[0:4] == 'test'
and issubclass(item[1], unittest.TestCase)
]
)接下来,我们将生成类testCodeCoverage测试方法使用的缺失测试用例名称列表:
cls._missingTestCases = sorted(
set(cls._requiredTestCases).difference(
set(cls._actualTestCases.keys())))在这一点上,这个单独的测试方法将能够执行,并且要么通过,要么失败,并输出一个指示缺少哪些测试用例的输出。如果我们写出test_me.py模块如下:
from unit_testing import ModuleCoverageTest
class testmeCodeCoverage(ModuleCoverageTest):
_testModule = me
if __name__ == '__main__':
unittest.main()然后在执行之后,我们会得到以下结果:
要使顶级代码覆盖率测试通过,只需添加缺少的测试用例类:
class testmeCodeCoverage(ModuleCoverageTest):
_testModule = me
class testChild(unittest.TestCase):
pass
class testChildOverride(unittest.TestCase):
pass
class testParent(unittest.TestCase):
pass
class testShowable(unittest.TestCase):
pass
if __name__ == '__main__':
unittest.main()这种方法,以一种积极的姿态确保以这种方式覆盖代码,有助于减少单元测试的麻烦。如果编写测试的过程从一个公共测试开始,该测试将告诉测试开发人员在过程中的每一步都缺少什么,那么整个编写测试的过程实际上变成了重复以下步骤,直到没有测试失败:
- 执行测试套件
- 如果有失败的测试,进行任何需要的代码更改以使最后一个测试通过:
- 如果缺少测试失败,请添加必要的测试类或方法
- 如果是源代码导致的故障,则在验证故障中涉及的测试值应已通过后,相应地更改该代码
向前的!
为了能够在测试模块中的所有测试用例类中测试缺少的属性和方法测试,我们需要找到所有这些测试用例类,并在类的基础上跟踪它们。这与我们之前发现的过程基本相同,但是存储的值必须可以通过类名检索,因为我们希望单个覆盖率测试实例检查所有的源类和测试用例类,所以我们将它们存储在两个字典中,propSources用于每个源类,以及propImplementations中对于实际功能对象:
cls._propertyTestsByClass = {}
for testClass in cls._classTests:
cls._propertyTestsByClass[testClass] = set()
sourceClass = cls._classTests[testClass]
sourceMRO = list(sourceClass.__mro__)
sourceMRO.reverse()
# Get all the item's properties
properties = [
member for member in inspect.getmembers(
sourceClass, inspect.isdatadescriptor)
if member[0][0:2] != '__'
]
# Create and populate data-structures that keep track of where
# property-members originate from, and what their implementation
# looks like. Initially populated with None values:
propSources = {}
propImplementations = {}
for name, value in properties:
propSources[name] = None
propImplementations[name] = None
for memberName in propSources:
implementation = sourceClass.__dict__.get(memberName)
if implementation \
and propImplementations[memberName] != implementation:
propImplementations[memberName] = implementation
propSources[memberName] = sourceClass
cls._propertyTestsByClass[testClass] = set(
[
'test%s' % key for key in propSources
if propSources[key] == sourceClass
]
)方法测试的获取以相同的方式进行,并使用与先前勘探相同的方法:
cls._methodTestsByClass = {}
for testClass in cls._classTests:
cls._methodTestsByClass[testClass] = set()
sourceClass = cls._classTests[testClass]
sourceMRO = list(sourceClass.__mro__)
sourceMRO.reverse()
# Get all the item's methods
methods = [
member for member in inspect.getmembers(
sourceClass, inspect.ismethod)
] + [
member for member in inspect.getmembers(
sourceClass, inspect.isfunction)
]
# Create and populate data-structures that keep track of where
# method-members originate from, and what their implementation
# looks like. Initially populated with None values:
methSources = {}
methImplementations = {}
for name, value in methods:
if name.startswith('_%s__' % sourceClass.__name__):
# Locally-defined private method - Don't test it
continue
if hasattr(value, '__isabstractmethod__') \
and value.__isabstractmethod__:
# Locally-defined abstract method - Don't test it
continue methSources[name] = None
methImplementations[name] = None
for memberName in methSources:
implementation = sourceClass.__dict__.get(memberName)
if implementation \
and methImplementations[memberName] != implementation:
methImplementations[memberName] = implementation
methSources[memberName] = sourceClass
cls._methodTestsByClass[testClass] = set(
[
'test%s' % key for key in methSources
if methSources[key] == sourceClass
]
)一旦执行了最后两个块,代码覆盖率测试类将拥有测试模块中每个测试用例类所需的所有测试方法的完整分支。属性测试集合(cls._propertyTestsByClass是稀疏的,因为只有一个属性与任何类关联,Parent.prop:
{
"testChild": set(),
"testChildOverride": set(),
"testParent": {"testprop"},
"testShowable": set()
}不过,方法测试结构(cls._methodTestsByClass有更多的内容,准确地表示ChildOverride类中的public和_protected方法需要自己的测试方法,而Showable中的抽象show方法不需要测试:
{
"testChild": set(),
"testChildOverride": {
"test_protected", "testpublic"
},
"testParent": {
"test__init__", "test_protected",
"testpublic", "testshow"
},
"testShowable": set()
}这些数据是处理所需属性和方法测试所需的全部数据。剩下的就是找到一种将它们附加到每个测试用例类的方法。
装饰器可以被认为是一个函数,它接受另一个函数作为参数,并围绕装饰函数扩展或包装其他功能,而不实际修改它。任何可调用的函数、类的实例方法或(在本例中)属于类的类方法都可以用作装饰函数。在本例中,代码覆盖率测试用例类将使用装饰器函数结构定义两个类方法(AddPropertyTesting和AddMethodTesting,以便向使用它们装饰的任何类添加新方法(testPropertyCoverage和testMethodCoverage。由于这两个方法是主代码覆盖率类的嵌套成员,因此它们可以访问类中的数据,特别是生成的所需属性和方法测试名称的列表。此外,由于它们本身是装饰函数的嵌套成员,因此它们可以访问这些方法中的变量和数据。
这两个 decorator 方法几乎是相同的,除了它们的名称、它们的消息传递以及它们在哪里查找它们的数据之外,因此只会详细介绍第一个方法AddMethodTesting。该方法首先检查以确保它是扩展ModuleCoverageTest类的类的成员。这确保它将要查看的数据仅限于与组合源和测试模块相关的数据:
@classmethod
def AddMethodTesting(cls, target):
if cls.__name__ == 'ModuleCoverageTest':
raise RuntimeError('ModuleCoverageTest should be extended '
'into a local test-case class, not used as one directly.')
if not cls._testModule:
raise AttributeError('%s does not have a _testModule defined '
'as a class attribute. Check that the decorator-method is '
'being called from the extended local test-case class, not '
'from ModuleCoverageTest itself.' % (cls.__name__))在函数开头传入的target参数是一个unittest.TestCase类(尽管它没有显式地进行类型检查)。
它还需要确保将要使用的数据可用。如果不是,无论出于何种原因,都可以通过显式调用刚刚定义的类setUpClass方法来补救:
try:
if cls._methodTestsByClass:
populate = False
else:
populate = True
except AttributeError:
populate = True
if populate:
cls.setUpClass()下一步是定义一个函数实例来实际执行测试。此函数被定义为类的成员,因为它将在装饰过程完成时成为类的成员,但由于它嵌套在装饰器方法中,因此它可以访问并保留装饰器方法中迄今为止定义的所有变量和参数的值。其中最重要的是target,因为这是要装饰的课程。该target值本质上附属于正在定义/创建的函数:
def testMethodCoverage(self):
requiredTestMethods = cls._methodTestsByClass[target.__name__]
activeTestMethods = set(
[
m[0] for m in
inspect.getmembers(target, inspect.isfunction)
if m[0][0:4] == 'test'
]
)
missingMethods = sorted(
requiredTestMethods.difference(activeTestMethods)
)
self.assertEquals([], missingMethods,
'unit testing policy requires test-methods to be created for '
'all public and protected methods, but %s is missing the '
'following test-methods: %s' % (
target.__name__, missingMethods
)
)测试方法本身非常简单:它创建一组活动的测试方法名称,这些名称在它所附加的测试用例类中定义,从它从覆盖率测试类检索的测试用例类所需的测试方法中删除这些名称,如果有剩余,测试将失败并宣布缺少的内容。
剩下要做的就是将函数附加到目标并返回目标,以便不中断对它的访问:
target.testMethodCoverage = testMethodCoverage
return target一旦定义了这些装饰器,它们就可以应用于单元测试代码,如下所示:
class testmeCodeCoverage(ModuleCoverageTest):
_testModule = me
@testmeCodeCoverage.AddPropertyTesting
@testmeCodeCoverage.AddMethodTesting
class testChild(unittest.TestCase):
pass
@testmeCodeCoverage.AddPropertyTesting
@testmeCodeCoverage.AddMethodTesting
class testChildOverride(unittest.TestCase):
pass
@testmeCodeCoverage.AddPropertyTesting
@testmeCodeCoverage.AddMethodTesting
class testParent(unittest.TestCase):
pass
@testmeCodeCoverage.AddPropertyTesting
@testmeCodeCoverage.AddMethodTesting
class testShowable(unittest.TestCase):
pass并且,在它们就位后,测试运行将开始报告缺少的内容:
刚才显示的测试集合的基本起点将作为与单个模块相关的任何其他测试集合的起点。然而,hms_sys的预期代码结构包括整个代码包,并且可能包括这些包中的包。我们还不知道,因为我们还没走那么远。这将对最终的单元测试方法以及模板文件的创建产生影响,从而使这些测试模块的创建更快、更不容易出错。
主要影响集中在我们希望能够通过一次调用执行整个项目的所有测试的想法上,同时,如果感兴趣的是针对包结构中更深层次的内容运行的一个或多个测试,则不需要执行组件项目测试套件中的每个测试。因此,在测试包的组织结构相同的情况下分解测试是有意义的,并且允许任何级别的测试模块在被模块树上的上级调用或导入子测试时导入子测试。
为此,单元测试的模板模块需要容纳与主代码库相同类型的导入功能,同时跟踪由测试运行产生的任何导入过程产生的所有测试。幸运的是,unittest模块还提供了可用于管理该需求的类,例如TestSuite类,它是可执行的测试集合,可以根据需要向其中添加新的测试。最终的测试模块模板看起来很像我们之前创建的模块模板,尽管它以一些搜索和替换样板注释开始:
#!/usr/bin/env python
# Python unit-test-module template. Copy the template to a new
# unit-test-module location, and start replacing names as needed:
#
# PackagePath ==> The path/namespace of the parent of the module/package
# being tested in this file.
# ModuleName ==> The name of the module being tested
#
# Then remove this comment-block
"""
Defines unit-tests for the module at PackagePath.ModuleName.
"""
#######################################
# Any needed from __future__ imports #
# Create an "__all__" list to support #
# "from module import member" use #
#######################################与提供应用程序功能的包和模块不同,单元测试模块模板不期望或不需要以**all**条目的方式提供太多内容,只提供驻留在模块本身中的测试用例类和任何子测试模块:
__all__ = [
# Test-case classes
# Child test-modules
]在所有测试模块中都会出现一些标准导入,也有可能出现第三方导入,尽管这可能并不常见:
#######################################
# Standard library imports needed #
#######################################
import os
import sys
import unittest
#######################################
# Third-party imports needed #
#######################################
#######################################
# Local imports needed #
#######################################
from unit_testing import *
#######################################
# Initialization needed before member #
# definition can take place #
#######################################所有测试模块将定义一个名为LocalSuite的unittest.TestSuite实例,该实例包含所有本地测试用例,需要时可在父模块中按名称导入:
#######################################
# Module-level Constants #
#######################################
LocalSuite = unittest.TestSuite()
#######################################
# Import the module being tested #
#######################################
import PackagePath.ModuleName as ModuleName我们还将定义定义代码覆盖率测试用例类的样板代码:
#######################################
# Code-coverage test-case and #
# decorator-methods #
#######################################
class testModuleNameCodeCoverage(ModuleCoverageTest):
_testModule = ModuleName
LocalSuite.addTests(
unittest.TestLoader().loadTestsFromTestCase(
testModuleNameCodeCoverage
)
)从这一点开始,所有不属于模块__main__执行的部分都应该是测试用例类的定义:
#######################################
# Test-cases in the module #
#######################################
#######################################
# Child-module test-cases to execute #
#######################################如果以后需要导入子测试模块,则用于导入的代码结构如下所示,已注释掉,可以根据需要进行复制、粘贴、取消注释和重命名:
# import child_module
# LocalSuite.addTests(child_module.LocalSuite._tests)按照标准模块和包模板的组织结构,还有更多标准模块部分:
#######################################
# Imports to resolve circular #
# dependencies. Avoid if possible. #
#######################################
#######################################
# Initialization that needs to #
# happen after member definition. #
#######################################
#######################################
# Code to execute if file is called #
# or run directly. #
#######################################最后,还提供了一些直接执行模块、运行测试以及在无故障发生时显示和写出报告的规定:
if __name__ == '__main__':
import time
results = unittest.TestResult()
testStartTime = time.time()
LocalSuite.run(results)
results.runTime = time.time() - testStartTime
PrintTestResults(results)
if not results.errors and not results.failures:
SaveTestReport(results, 'PackagePath.ModuleName',
'PackagePath.ModuleName.test-results')该模板提供了一些在首次复制到最终测试模块时可以找到并替换的项目:
-
PackagePath:被测试模块的完整名称空间,减去模块本身。例如,如果为一个完整名称空间为hms_core.business.processes.artisan的模块创建测试模块,PackagePath将为hms_core.business.processes -
ModuleName:被测试模块的名称(artisan,使用上例)
搜索和替换操作还将为嵌入模板中的ModuleCoverageTest子类定义提供唯一名称。一旦这些替换完成,测试模块就可以运行,如前一个示例所示,并将开始报告丢失的测试用例和方法。
遵循此结构的每个测试模块在unittest.TestSuite对象中跟踪其本地测试,该对象可由父测试模块导入,并可根据需要从子TestSuite实例添加测试。注释掉的示例将取代模板文件:
# import child_module
# LocalSuite.addTests(child_module.LocalSuite._tests)最后,模板文件利用自定义unit_testing模块中定义的一些显示和报告功能,将摘要测试结果数据写入控制台,并(当测试无故障运行时)写入本地文件,如果需要,可以在源代码管理中跟踪。
只有一个故事/任务集剩余,即如何将单元测试与组件项目的任何构建过程集成:
- 作为开发人员,我需要知道如何将组件项目的单元测试集成到该组件项目的构建过程中,以便构建可以自动执行单元测试:
- 确定如何将单元测试集成到构建过程中
- 确定如何处理不同环境的构建/测试集成
有了组件项目中刚刚定义的单元测试结构,将它们集成到构建过程中相对容易实现。在基于setup.py文件的构建中,可以在setup函数本身的test_suite参数中指定测试模块,并且可以通过执行python setup.py test来运行测试。在hms_sys组件项目中,有必要将单元测试标准代码的路径添加到setup.py中:
#!/usr/bin/env python
# Adding our unit testing standards
import sys
sys.path.append('../standards')
from setuptools import setup
# The actual setup function call:
setup(
name='HMS-Core',
version='0.1.dev0',
author='Brian D. Allbee',
description='',
package_dir={
'':'src',
# ...
},
# Can also be automatically generated using
# setuptools.find_packages...
packages=[
'hms_core',
# ...
],
package_data={
# 'hms_core':[
# 'filename.ext',
# # ...
# ]
},
entry_points={
# 'console_scripts':[
# 'executable_name = namespace.path:function',
# # ...
# ],
},
# Adding the test suite for the project
test_suite='tests.test_hms_core',
)如果需要基于 Makefile 的构建过程,则可以简单地将对setup.py test的特定调用包含在任何相关的 Make 目标中:
# Makefile for the HMS Core (hms-core) project
main: test setup
# Doesn't (yet) do anything other than running the test and
# setup targets
setup:
# Calls the main setup.py to build a source-distribution
# python setup.py sdist
test:
# Executes the unit-tests for the package, allowing the build-
# process to die and stop the build if a test fails
python setup.py. test从setup.py内执行的测试套件将返回适当的值,以便在出现错误或出现故障时停止生成过程。
除非成立新团队或新业务,否则这些流程和政策很可能早在项目开始之前就制定好了,通常是在团队承担的第一个项目之前或期间。大多数开发商店和团队都已经发现了本章中介绍的各种解决方案的需求,并将根据这些需求采取行动。
所有这些项目都设置好并提交给 SCM,为后续迭代的所有开发工作奠定了基础。第一个“真正的”迭代将处理基本业务对象的定义和实现。
















