Biopython 测试框架
Biopython 拥有一个基于 unittest(Python 的标准单元测试框架)的回归测试框架(文件 run_tests.py
)。为模块提供全面的测试是确保 Biopython 代码在发布前尽可能没有 bug 的最重要方面之一。它往往也是贡献中被低估的方面之一。本章旨在让运行 Biopython 测试和编写良好的测试代码变得尽可能容易。理想情况下,每个进入 Biopython 的模块都应该有一个测试(并且还应该有文档!)。我们所有的开发者,以及任何从源代码安装 Biopython 的人,都强烈建议运行单元测试。
运行测试
当你下载 Biopython 源代码或从我们的源代码仓库检出时,你应该会发现一个名为 Tests
的子目录。它包含关键脚本 run_tests.py
、许多名为 test_XXX.py
的单独脚本,以及许多其他包含测试套件输入文件的子目录。
作为构建和安装 Biopython 的一部分,你通常会在命令行中从 Biopython 源代码的顶层目录运行完整的测试套件,使用以下命令
$ python setup.py test
这实际上等同于进入 Tests
子目录并运行
$ python run_tests.py
你通常只想运行部分测试,这可以通过以下方式完成
$ python run_tests.py test_SeqIO.py test_AlignIO.py
在给出测试列表时,.py
扩展名是可选的,所以你也可以只输入
$ python run_tests.py test_SeqIO test_AlignIO
要运行 docstring 测试(见 编写 doctest 部分),你可以使用
$ python run_tests.py doctest
你还可以跳过任何设置了显式在线组件的测试,方法是在命令中添加 --offline
,例如
$ python run_tests.py --offline
默认情况下,run_tests.py
运行所有测试,包括 docstring 测试。
如果单个测试失败,你也可以尝试直接运行它,这可能会提供更多信息。
基于 Python 标准 unittest
框架的测试将 import unittest
,然后定义 unittest.TestCase
类,每个类都包含一个或多个子测试作为方法,这些方法以 test_
开头,并检查代码的某些特定方面。
使用 Tox 运行测试
与大多数 Python 项目一样,你也可以使用 Tox 在多个 Python 版本上运行测试,前提是这些 Python 版本已安装在你的系统中。
我们没有在代码库中提供配置文件 tox.ini
,因为难以确定用户特定的设置(例如,Python 版本的可执行文件名称)。你可能也只对测试针对我们支持的 Python 版本子集的 Biopython 感兴趣。
如果你有兴趣使用 Tox,你可以从下面的示例 tox.ini
开始
[tox]
envlist = pypy,py38,py39
[testenv]
changedir = Tests
commands = {envpython} run_tests.py --offline
deps =
numpy
reportlab
使用上面的模板,执行 tox
将在 PyPy、Python 3.8 和 3.9 上测试你的 Biopython 代码。它假设这些 Python 的可执行文件分别名为“python3.8”、“python3.9”等等。
编写测试
假设你想要为一个名为 Biospam
的模块编写一些测试。这可以是你编写的模块,也可以是现有但还没有测试的模块。在下面的示例中,我们假设 Biospam
是一个执行简单数学运算的模块。
每个 Biopython 测试都包含一个包含测试本身的脚本,以及一个可选的包含测试使用的输入文件的目录
test_Biospam.py
- 模块的实际测试代码。Biospam
[可选] - 任何必要的输入文件将位于其中的目录。如果你有任何应该手动检查的输出文件,请将它们输出到这里(但不鼓励这样做),以防止堵塞主 Tests 目录。一般来说,请使用临时文件/文件夹。
在 Tests
目录中,任何以 test_
为前缀的脚本都将被 run_tests.py
找到并运行。下面,我们展示了一个示例测试脚本 test_Biospam.py
。如果你将此脚本放在 Biopython 的 Tests
目录中,那么 run_tests.py
将找到它并执行其中包含的测试
$ python run_tests.py
test_Ace ... ok
test_AlignIO ... ok
test_BioSQL ... ok
test_BioSQL_SeqIO ... ok
test_Biospam ... ok
test_CAPS ... ok
test_Clustalw ... ok
...
----------------------------------------------------------------------
Ran 107 tests in 86.127 seconds
使用 unittest
编写测试
unittest
框架从 Python 2.1 版本开始包含在 Python 中,并在 Python 库参考中进行了说明(我知道你把它放在枕头下面,正如推荐的那样)。还有一个 unittest 的在线文档。如果你熟悉 unittest
系统(或类似的系统,如 nose 测试框架),你应该没有问题。你可能也会发现查看 Biopython 中的现有示例很有帮助。
以下是一个 Biospam
的最小 unittest
风格的测试脚本,你可以复制粘贴它来开始
import unittest
from Bio import Biospam
class BiospamTestAddition(unittest.TestCase):
def test_addition1(self):
result = Biospam.addition(2, 3)
self.assertEqual(result, 5)
def test_addition2(self):
result = Biospam.addition(9, -1)
self.assertEqual(result, 8)
class BiospamTestDivision(unittest.TestCase):
def test_division1(self):
result = Biospam.division(3.0, 2.0)
self.assertAlmostEqual(result, 1.5)
def test_division2(self):
result = Biospam.division(10.0, -2.0)
self.assertAlmostEqual(result, -5.0)
if __name__ == "__main__":
runner = unittest.TextTestRunner(verbosity=2)
unittest.main(testRunner=runner)
在除法测试中,我们使用 assertAlmostEqual
而不是 assertEqual
来避免测试因舍入误差而失败;有关详细信息和 unittest
中可用的其他功能(在线参考),请参阅 Python 文档中的 unittest
章节。
这些是 unittest
风格测试的关键点
测试用例存储在从
unittest.TestCase
派生的类中,并涵盖代码的一个基本方面你可以使用
setUp
和tearDown
方法来执行在每个测试方法之前和之后应该运行的任何重复代码。例如,setUp
方法可以用来创建要测试的对象的实例,或打开文件句柄。tearDown
应该进行任何“整理”,例如关闭文件句柄。测试以
test_
为前缀,每个测试都应该涵盖你要测试内容的特定部分。你可以在一个类中拥有任意数量的测试。在测试脚本的末尾,你可以使用
if __name__ == "__main__": runner = unittest.TextTestRunner(verbosity=2) unittest.main(testRunner=runner)
当脚本被单独运行(而不是从
run_tests.py
导入)时,执行测试。如果你运行此脚本,你会看到类似以下的内容$ python test_BiospamMyModule.py test_addition1 (__main__.TestAddition) ... ok test_addition2 (__main__.TestAddition) ... ok test_division1 (__main__.TestDivision) ... ok test_division2 (__main__.TestDivision) ... ok ---------------------------------------------------------------------- Ran 4 tests in 0.059s OK
为了更清楚地表明每个测试的作用,你可以在每个测试中添加 docstring。这些会在运行测试时显示,如果测试失败,这将是有用的信息。
import unittest from Bio import Biospam class BiospamTestAddition(unittest.TestCase): def test_addition1(self): """An addition test""" result = Biospam.addition(2, 3) self.assertEqual(result, 5) def test_addition2(self): """A second addition test""" result = Biospam.addition(9, -1) self.assertEqual(result, 8) class BiospamTestDivision(unittest.TestCase): def test_division1(self): """Now let's check division""" result = Biospam.division(3.0, 2.0) self.assertAlmostEqual(result, 1.5) def test_division2(self): """A second division test""" result = Biospam.division(10.0, -2.0) self.assertAlmostEqual(result, -5.0) if __name__ == "__main__": runner = unittest.TextTestRunner(verbosity=2) unittest.main(testRunner=runner)
现在运行脚本将显示
$ python test_BiospamMyModule.py An addition test ... ok A second addition test ... ok Now let's check division ... ok A second division test ... ok ---------------------------------------------------------------------- Ran 4 tests in 0.001s OK
如果你的模块包含 docstring 测试(见 编写 doctest 部分),你可能想要将这些测试包含在要运行的测试中。你可以通过修改 if __name__ == "__main__":
下的代码来做到这一点,使其看起来像这样
if __name__ == "__main__":
unittest_suite = unittest.TestLoader().loadTestsFromName("test_Biospam")
doctest_suite = doctest.DocTestSuite(Biospam)
suite = unittest.TestSuite((unittest_suite, doctest_suite))
runner = unittest.TextTestRunner(sys.stdout, verbosity=2)
runner.run(suite)
这只有在你想要在执行 python test_Biospam.py
时运行 docstring 测试时才相关,前提是它有一些复杂的运行时依赖性检查。
通常,请通过将 docstring 测试添加到 run_tests.py
中来包含它们,如下所示。
编写 doctest
Python 模块、类和函数支持使用 docstring 进行内置文档化。doctest 框架(包含在 Python 中)允许开发者在 docstring 中嵌入工作示例,并自动测试这些示例。
目前只有 Biopython 的一部分包含 doctests。 run_tests.py
脚本负责运行 doctests。为此,在 run_tests.py
脚本的顶部,有一个手动编译的模块跳过列表,这在可能未安装的可选外部依赖项(例如 Reportlab 和 NumPy 库)中很重要。因此,如果您在 Biopython 模块的文档字符串中添加了一些 doctests,为了让它们在 Biopython 测试套件中被排除,您必须更新 run_tests.py
以包含您的模块。目前,run_tests.py
的相关部分如下所示
# Following modules have historic failures. If you fix one of these
# please remove here!
EXCLUDE_DOCTEST_MODULES = [
"Bio.PDB",
"Bio.PDB.AbstractPropertyMap",
"Bio.Phylo.Applications._Fasttree",
"Bio.Phylo._io",
"Bio.Phylo.TreeConstruction",
"Bio.Phylo._utils",
]
# Exclude modules with online activity
# They are not excluded by default, use --offline to exclude them
ONLINE_DOCTEST_MODULES = ["Bio.Entrez", "Bio.ExPASy", "Bio.TogoWS"]
# Silently ignore any doctests for modules requiring numpy!
if numpy is None:
EXCLUDE_DOCTEST_MODULES.extend(
[
"Bio.Affy.CelFile",
"Bio.Cluster",
# ...
]
)
请注意,我们将 doctests 主要视为文档,因此您应该坚持使用典型用法。一般来说,处理错误条件等的复杂示例最好留给专门的单元测试。
请注意,如果您想编写涉及文件解析的 doctests,定义文件位置会使事情变得复杂。理想情况下,使用相对路径假设代码将从 Tests
目录运行,请参阅 Bio.SeqIO
doctests 以了解此示例。
要仅运行文档字符串测试,请使用
$ python run_tests.py doctest
请注意,doctest 系统很脆弱,需要小心以确保您的输出在 Biopython 支持的所有不同版本的 Python 上匹配(例如,浮点数的差异)。
在教程中编写 doctests
您正在阅读的本教程包含许多代码片段,这些代码片段通常格式类似于 doctest。我们在文件 test_Tutorial.py
中有自己的系统,允许将教程源代码中的代码片段标记为 Python doctests 运行。这是通过在每个 Python 控制台 (pycon) 块之前添加特殊的 .. doctest
注释行来实现的,例如
.. doctest
.. code:: pycon
>>> from Bio.Seq import Seq
>>> s = Seq("ACGT")
>>> len(s)
4
通常,代码示例不是自包含的,而是从前面的 Python 块继续。这里我们使用神奇注释 .. cont-doctest
,如下所示
.. cont-doctest
.. code:: pycon
>>> s == "ACGT"
True
特殊的 .. doctest
注释行可以采用工作目录(相对于 Doc/
文件夹),如果您有任何示例数据文件,则使用该目录,例如 .. doctest examples
将使用 Doc/examples
文件夹,而 .. doctest ../Tests/GenBank
将使用 Tests/GenBank
文件夹。
在目录参数之后,您可以通过添加 lib:XXX
来指定任何必须存在的 Python 依赖项才能运行测试,以指示 import XXX
必须工作,例如 .. doctest examples lib:numpy
您可以通过以下方式运行教程 doctests
$ python test_Tutorial.py
或者
$ python run_tests.py test_Tutorial.py