Biopython 測試框架

Biopython 有一個回歸測試框架 (檔案 run_tests.py),基於 unittest,這是 Python 的標準單元測試框架。為模組提供全面的測試是確保 Biopython 程式碼在發布前盡可能沒有錯誤的最重要方面之一。這也往往是貢獻中最被低估的方面之一。本章旨在使執行 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 版本上執行測試,前提是這些版本已安裝在您的系統中。

我們不會在我們的程式碼庫中提供組態檔 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 的執行檔名稱對於 Python 3.8 是「python3.8」,依此類推。

撰寫測試

假設您想為名為 Biospam 的模組撰寫一些測試。這可以是您撰寫的模組,或是不包含任何測試的現有模組。在以下範例中,我們假設 Biospam 是一個執行簡單數學運算的模組。

每個 Biopython 測試都包含一個包含測試本身的腳本,以及一個可選的目錄,其中包含測試使用的輸入檔案

  1. test_Biospam.py – 您模組的實際測試程式碼。

  2. 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-框架自版本 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 以避免因四捨五入誤差而導致測試失敗;請參閱 Python 文件中的 unittest 章節,以瞭解詳細資訊和 unittest 中可用的其他功能 (線上參考)。

這些是基於 unittest 的測試的重點

  • 測試案例儲存在繼承自 unittest.TestCase 的類別中,並涵蓋您程式碼的一個基本方面

  • 您可以將方法 setUptearDown 用於任何重複的程式碼,這些程式碼應在每個測試方法之前和之後執行。例如,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 的一部分包含 doctest。 run_tests.py 腳本負責執行 doctest。為此,在 run_tests.py 腳本的頂部有一個手動編譯的要跳過的模組列表,這在可能未安裝的可選外部相依性(例如 Reportlab 和 NumPy 函式庫)時非常重要。因此,如果您已在 Biopython 模組的 docstring 中新增了一些 doctest,為了將它們排除在 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",
            # ...
        ]
    )

請注意,我們主要將 doctest 視為文件,因此您應該堅持使用典型用法。一般來說,處理錯誤條件等複雜範例最好留給專用的單元測試。

請注意,如果您想撰寫涉及檔案剖析的 doctest,定義檔案位置會使事情複雜化。理想情況下,應使用相對路徑,假設程式碼將從 Tests 目錄執行,請參閱 Bio.SeqIO doctest 以取得範例。

若要僅執行 docstring 測試,請使用:

$ python run_tests.py doctest

請注意,doctest 系統很脆弱,需要小心確保您的輸出在 Biopython 支援的所有不同 Python 版本中都匹配(例如,浮點數的差異)。

在教學文件中撰寫 doctest

您正在閱讀的本教學文件包含許多程式碼片段,這些片段通常格式化為 doctest。我們在檔案 test_Tutorial.py 中有自己的系統,允許將教學文件原始碼中的程式碼片段標記為 Python doctest 來執行。這是透過在每個 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 資料夾。

在目錄參數之後,您可以指定任何為了執行測試而必須存在的 Python 相依性,方法是新增 lib:XXX 來表示 import XXX 必須有效,例如:.. doctest examples lib:numpy

您可以透過以下方式執行教學文件的 doctest:

$ python test_Tutorial.py

$ python run_tests.py test_Tutorial.py