测试进阶:集成测试、代码覆盖与版本控制挂钩
一、集成测试与系统测试
1.1 测试冗余性
在测试过程中,冗余性是一个常见的现象。每个测试都应该是独立的,我们无需关心测试的运行顺序,也不必在意其他测试是否存在。这种独立性使得一些基础内容会被多次测试,在简单项目中,这种冗余性可能更为明显。
不过,冗余性并不是问题。所谓的 DRY(Don’t Repeat Yourself)原则并不特别适用于测试,多次测试某些内容并没有太大的弊端。但这并不意味着复制粘贴测试代码是个好主意。当看到测试之间存在相似性时,不必惊讶或担忧,但也不能以此为借口。
1.2 集成测试相关问题
以下是一些关于编写集成测试的常见问题:
1. 应该先编写哪些集成测试?
2. 当有一大块集成代码,但接下来要引入的部分完全没有集成测试时,会发生什么?
3. 编写检查代码块自身集成情况的测试有什么意义?
4. 什么是系统测试,它与集成测试有什么关系?
1.3 实践:为自己的程序编写集成测试
可以先为自己的一个程序绘制集成图,然后根据该图为代码编写集成测试。
二、其他测试工具和技术
2.1 代码覆盖
2.1.1 代码覆盖的概念
测试能告诉我们被测试的代码是否按预期工作,但无法告知未测试的代码情况。代码覆盖是一种解决这一缺陷的技术,代码覆盖工具会在测试运行时监控,并记录哪些代码行被执行,哪些未被执行。测试运行结束后,工具会生成一份报告,描述测试对整个代码体的覆盖程度。
虽然期望代码覆盖率接近 100%,但不能过度关注覆盖率数字,因为它可能具有一定的误导性。即使测试执行了程序中的每一行代码,也可能没有测试到所有需要测试的内容。所以,100% 的覆盖率并不意味着测试是完整的。另一方面,有些代码(如调试支持代码)确实不需要被测试覆盖,因此低于 100% 的覆盖率也是可以接受的。代码覆盖只是让我们了解测试的执行情况和可能遗漏的内容,而不是定义一个好的测试套件的标准。
2.1.2 coverage.py 工具
coverage.py 是一个用于 Python 的代码覆盖工具。由于它不是 Python 内置的,需要下载并安装。可以从 Python 包索引(http://pypi.python.org/pypi/coverage)下载最新版本。
- Python 2.6 或更高版本的用户可以通过以下步骤安装:
1. 解压存档。
2. 进入目录。
3. 输入命令:
$ python setup.py install --user
- 旧版本 Python 用户需要对系统范围的 site-packages 目录有写入权限,有此权限的用户可以输入命令:
$ python setup.py install
- Windows 用户还可以从 Python 包索引下载 Windows 安装程序文件并运行以安装 coverage.py。
2.1.3 使用 coverage.py 的步骤
以下是使用 coverage.py 的具体步骤:
1.
创建测试代码
:将以下测试代码放入 test_toy.py 文件中:
from unittest import TestCase
import toy
class test_global_function(TestCase):
def test_positive(self):
self.assertEqual(toy.global_function(3), 4)
def test_negative(self):
self.assertEqual(toy.global_function(-3), -2)
def test_large(self):
self.assertEqual(toy.global_function(2**13), 2**13 + 1)
class test_example_class(TestCase):
def test_timestwo(self):
example = toy.example_class(5)
self.assertEqual(example.timestwo(), 10)
def test_repr(self):
example = toy.example_class(7)
self.assertEqual(repr(example), '<example param="7">')
- 创建被测试代码 :将以下代码放入 toy.py 文件中:
def global_function(x):
r"""
>>> global_function(5)
6
"""
return x + 1
class example_class:
def __init__(self, param):
self.param = param
def timestwo(self):
return self.param * 2
def __repr__(self):
return '<example param="%s">' % self.param
if __name__ == '__main__':
import doctest
doctest.testmod()
- 运行 Nose 测试 :运行 Nose 测试,它应该能找到并运行测试,并报告一切正常。但实际上,有些代码并未被测试到。
- 使用 coverage.py 测量覆盖率 :再次运行 Nose 测试,并使用 coverage.py 测量覆盖率,命令如下:
$ nosetests --with-coverage --cover-erase
2.1.4 代码覆盖报告分析
运行上述命令后,覆盖率报告显示 toy 模块中 9/12(即 75%)的可执行语句在测试中被执行,缺失的行是第 16 行和第 19 - 20 行。第 16 行是
__repr__
方法,应该对其进行测试,覆盖率检查揭示了测试中的一个漏洞。而第 19 - 20 行是运行 doctest 的代码,在正常情况下不需要使用,因此可以忽略该覆盖率漏洞。
需要注意的是,代码覆盖在大多数情况下无法检测测试本身的问题。例如,在上述测试代码中,
timestwo
方法的测试违反了单元隔离原则,调用了
example_class
的两个不同方法。虽然其中一个方法是构造函数,这种情况可能可以接受,但覆盖率检查器无法发现这个潜在问题。覆盖率是有用的,但高覆盖率并不等同于好的测试。
2.2 版本控制挂钩
2.2.1 版本控制挂钩的概念
大多数版本控制系统都有能力在各种事件发生时运行用户编写的程序,这些程序通常被称为挂钩。版本控制系统用于跟踪源代码树的更改,即使这些更改由不同的人做出。它提供了整个项目的通用撤销历史和更改日志,还能方便地将不同人的工作整合到一个统一的实体中,并跟踪同一项目的不同版本。
我们可以利用版本控制挂钩,让版本控制程序在将代码的新版本提交到版本控制仓库时自动运行测试。这是一个不错的技巧,能减少测试破坏的 bug 悄无声息地进入仓库的可能性。但如果将其作为一项政策而不仅仅是让生活更轻松的工具,可能会带来问题。
在大多数系统中,编写挂钩使得无法提交破坏测试的代码,这乍一看似乎是个好主意,但实际上并非如此。一方面,版本控制系统的一个主要目的是开发者之间的沟通,干扰这一点从长远来看往往是低效的。另一方面,这会阻止任何人提交问题的部分解决方案,导致代码以大块形式被提交到仓库,这使得难以跟踪更改内容,增加了混乱。有更好的方法来确保始终有一个可工作的代码库,例如使用版本控制分支。
2.2.2 Bazaar 版本控制系统的挂钩
Bazaar 是一个分布式版本控制系统,每个用户可以独立管理自己的挂钩。以下是安装 Nose 作为 Bazaar 提交后挂钩的步骤:
1.
确定插件目录
:Bazaar 挂钩存放在插件目录中。在类 Unix 系统上,目录为
~/.bazaar/plugins/
;在 Windows 系统上,目录为
C:\Documents and Settings\<username>\Application Data\Bazaar\<version>\plugins\
。如果目录不存在,可能需要创建。
2.
编写挂钩代码
:将以下代码放入插件目录中的 run_nose.py 文件中:
from bzrlib import branch
from os.path import join, sep
from os import chdir
from subprocess import call
def run_nose(local, master, old_num, old_id, new_num, new_id):
try:
base = local.base
except AttributeError:
base = master.base
if not base.startswith('file://'):
return
try:
chdir(join(sep, *base[7:].split('/')))
except OSError:
return
call(['nosetests'])
branch.Branch.hooks.install_named_hook('post_commit',
run_nose,
'Runs Nose after each commit')
- 创建测试代码 :在工作文件中创建一个新目录,并将以下代码放入 test_simple.py 文件中:
from unittest import TestCase
class test_simple(TestCase):
def test_one(self):
self.assertNotEqual("Testing", "Hooks")
def test_two(self):
self.assertEqual("Same", "Same")
- 初始化仓库并提交测试 :在与 test_simple.py 相同的目录中,运行以下命令创建新仓库并提交测试:
$ bzr init
$ bzr add
$ bzr commit
- 验证挂钩效果 :提交通知后会出现 Nose 测试报告。从现在起,每次向 Bazaar 仓库提交代码时,Nose 都会搜索并运行该仓库中的所有测试。
2.2.3 Mercurial 版本控制系统的挂钩
Mercurial 也是一个分布式版本控制系统,用户可以独立管理自己的挂钩。Mercurial 挂钩可以存放在个人配置文件或仓库配置文件中。个人配置文件在类 Unix 系统上为
~/.hgrc
,在 Windows 系统上为
%USERPROFILE%\Mercurial.ini
;仓库配置文件存放在仓库的
.hg/hgrc
子目录中。
以下是安装 Nose 作为 Mercurial 提交后挂钩的步骤:
1.
初始化仓库
:在方便的位置创建一个新目录,并在其中执行以下命令:
$ hg init
-
创建挂钩配置文件
:进入
.hg子目录,创建一个名为 hgrc 的文本文件,内容如下:
[hooks]
commit = nosetests
-
创建测试代码
:在仓库目录(即
.hg目录的父目录)中创建一个名为 test_simple.py 的文件,内容如下:
from unittest import TestCase
class test_simple(TestCase):
def test_one(self):
self.assertNotEqual("Testing", "Hooks")
def test_two(self):
self.assertEqual("Same", "Same")
- 添加测试文件并提交 :运行以下命令添加测试文件并提交到仓库:
$ hg add
$ hg commit
- 验证挂钩效果 :提交触发了测试运行。由于将挂钩放在了仓库配置文件中,它仅对该仓库的提交生效。如果将其放在个人配置文件中,则每次提交到任何仓库时都会调用。
2.2.4 Git 版本控制系统的挂钩
Git 同样是一个分布式版本控制系统,用户可以独立控制自己的挂钩。Git 挂钩存放在仓库的
.git/hooks/
子目录中,每个挂钩存放在一个单独的文件中。
以下是安装 Nose 作为 Git 提交后挂钩的步骤:
1.
初始化仓库
:创建一个新目录作为 Git 仓库,并在其中执行以下命令:
$ git init
-
创建挂钩文件
:
-
在类 Unix 系统上,将以下两行代码放入
.git/hooks/子目录中的 post-commit 文件中,并使用chmod +x post-commit命令使其可执行:
-
在类 Unix 系统上,将以下两行代码放入
#!/bin/sh
nosetests
- 在 Windows 系统上,将以下代码放入 `.git\hooks\` 子目录中的 post-commit.bat 文件中:
@echo off
nosetests
-
创建测试代码
:在仓库目录(即
.git目录的父目录)中创建一个名为 test_simple.py 的文件,内容如下:
from unittest import TestCase
class test_simple(TestCase):
def test_one(self):
self.assertNotEqual("Testing", "Hooks")
def test_two(self):
self.assertEqual("Same", "Same")
- 添加测试文件并提交 :运行以下命令添加测试文件并提交到仓库:
$ git add test_simple.py
$ git commit -a
综上所述,通过集成测试、代码覆盖和版本控制挂钩等技术,可以提高代码的质量和可维护性。代码覆盖帮助我们了解测试的完整性,而版本控制挂钩则确保每次代码提交时都能自动运行测试,减少潜在的 bug 进入仓库的风险。不同的版本控制系统都提供了灵活的挂钩机制,方便开发者根据自己的需求进行定制。
2.3 不同版本控制系统挂钩对比
为了更清晰地了解在不同版本控制系统中设置 Nose 作为提交后挂钩的差异,我们可以通过以下表格进行对比:
| 版本控制系统 | 挂钩存放位置 | 挂钩编写方式 | 设置步骤 |
| — | — | — | — |
| Bazaar | 插件目录(Unix:
~/.bazaar/plugins/
;Windows:
C:\Documents and Settings\<username>\Application Data\Bazaar\<version>\plugins\
) | Python 函数 | 1. 确定插件目录;2. 编写 Python 挂钩函数并保存为文件;3. 创建测试代码;4. 初始化仓库并提交测试 |
| Mercurial | 个人配置文件(Unix:
~/.hgrc
;Windows:
%USERPROFILE%\Mercurial.ini
)或仓库配置文件(
.hg/hgrc
) | 命令行命令 | 1. 初始化仓库;2. 创建挂钩配置文件并设置命令;3. 创建测试代码;4. 添加测试文件并提交 |
| Git | 仓库的
.git/hooks/
子目录 | 可执行程序(Unix: 脚本;Windows: 批处理文件) | 1. 初始化仓库;2. 创建挂钩文件并编写代码;3. 创建测试代码;4. 添加测试文件并提交 |
2.4 总结与建议
在软件开发过程中,测试是确保代码质量的重要环节。集成测试帮助我们验证不同模块之间的交互是否正常,而代码覆盖则让我们了解测试的完整性。通过使用代码覆盖工具(如 coverage.py),我们可以发现测试中可能遗漏的代码部分,从而有针对性地补充测试用例。
版本控制挂钩则为我们提供了一种自动化测试的手段,确保每次代码提交时都能自动运行测试,减少潜在的 bug 进入仓库的风险。不同的版本控制系统都提供了灵活的挂钩机制,开发者可以根据自己的使用习惯和项目需求选择合适的版本控制系统,并设置相应的挂钩。
以下是一些使用这些技术的建议:
-
集成测试
:在编写集成测试时,应确保每个测试的独立性,避免测试之间的相互依赖。同时,根据集成图的顺序编写测试,逐步验证系统的各个部分。
-
代码覆盖
:虽然追求高覆盖率是一个目标,但不要过分依赖覆盖率数字。应结合代码的实际功能和逻辑,编写全面的测试用例。
-
版本控制挂钩
:将版本控制挂钩作为辅助工具,而不是严格的政策。允许开发者提交部分解决方案,同时使用版本控制分支来管理不同的开发阶段。
2.5 流程图总结
下面是一个 mermaid 格式的流程图,总结了使用代码覆盖和版本控制挂钩的整体流程:
graph LR
classDef process fill:#E5F6FF,stroke:#73A6FF,stroke-width:2px
A(编写代码):::process --> B(编写测试用例):::process
B --> C(运行测试):::process
C --> D{是否使用代码覆盖?}:::process
D -- 是 --> E(使用 coverage.py 测量覆盖率):::process
D -- 否 --> F(继续开发):::process
E --> G(分析覆盖率报告):::process
G --> H{是否有未覆盖代码?}:::process
H -- 是 --> I(补充测试用例):::process
I --> B
H -- 否 --> F
F --> J{是否使用版本控制?}:::process
J -- 是 --> K(设置版本控制挂钩):::process
K --> L(提交代码):::process
L --> M(自动运行测试):::process
M --> N{测试是否通过?}:::process
N -- 是 --> O(代码入库):::process
N -- 否 --> P(修复代码):::process
P --> B
J -- 否 --> F
这个流程图展示了从代码编写到测试、代码覆盖分析、版本控制挂钩设置以及最终代码入库的整个过程。通过这个流程,我们可以确保代码的质量和可维护性,同时提高开发效率。
2.6 常见问题解答
在使用上述测试技术和版本控制挂钩的过程中,可能会遇到一些常见问题,以下是一些解答:
1.
代码覆盖工具能否检测测试用例的质量?
代码覆盖工具主要关注代码的执行情况,在大多数情况下无法检测测试用例本身的质量。例如,测试用例可能违反单元隔离原则,但代码覆盖工具只能看到代码被执行,而无法判断测试的合理性。
2.
版本控制挂钩阻止提交破坏测试的代码是否合理?
虽然这乍一看是个好主意,但实际上可能会带来问题。版本控制系统的一个重要目的是开发者之间的沟通,阻止提交破坏测试的代码可能会干扰这种沟通,同时也会阻止开发者提交部分解决方案。建议将版本控制挂钩作为辅助工具,而不是严格的政策。
3.
如何选择合适的版本控制系统和挂钩设置?
可以根据项目的规模、团队的使用习惯和需求来选择合适的版本控制系统。对于分布式开发团队,Bazaar、Mercurial 和 Git 都是不错的选择。在设置挂钩时,应根据版本控制系统的特点和文档进行操作。
通过掌握集成测试、代码覆盖和版本控制挂钩等技术,开发者可以提高代码的质量和可维护性,减少潜在的 bug,从而提高软件开发的效率和可靠性。希望本文介绍的内容能对大家在软件开发过程中的测试工作有所帮助。
超级会员免费看
712

被折叠的 条评论
为什么被折叠?



