目錄

名稱

Test::Harness::Beyond - 超越 make test

超越 make test

Test::Harness 負責執行測試腳本、分析其輸出並回報成功或失敗。當我為模組輸入 make test (或 ./Build test) 時,通常會使用 Test::Harness 來執行測試(並非所有模組都使用 Test::Harness,但大多數都使用)。

要開始探索 Test::Harness 的一些功能,我需要從 make test 切換到 prove 命令(隨附 Test::Harness)。對於以下範例,我也需要安裝最新版本的 Test::Harness;在我撰寫本文時,目前的版本為 3.14。

對於範例,我假設我們正在使用「一般」的 Perl 模組發行版。特別是,我假設輸入 make./Build 會導致已建置、準備安裝的模組程式碼可於 ./blib/lib 和 ./blib/arch 下使用,且有一個名為「t」的目錄包含我們的測試。Test::Harness 沒有硬性設定為該組態,但這可讓我不必為每個範例說明哪些檔案放在哪裡。

回到 prove;它就像 make test 一樣執行測試套件,但它提供更多控制權,可控制執行哪些測試、以什麼順序執行,以及如何回報其結果。通常,make test 會執行「t」目錄下的所有測試腳本。若要使用 prove 執行相同操作,我輸入

prove -rb t

此處的開關為 -r,會遞迴進入「t」資料夾下的任何資料夾,而 -b 會將 ./blib/lib 和 ./blib/arch 加入 Perl 的包含路徑,讓測試可以找到要測試的程式碼。如果我要測試的模組已經安裝了較早版本,我必須小心包含路徑,以確保我執行的不是安裝版本,而是我正在開發的新版本。

make test 不同,輸入 prove 不會自動重新建置我的模組。如果我在 prove 之前忘記 make,我將會測試這些檔案的舊版本,這必定會造成混淆。我必須養成輸入的習慣

make && prove -rb t

或者,如果我沒有需要建置的 XS 程式碼,我會使用 lib 下方的模組

prove -Ilib -r t

到目前為止,我所展示的內容,make test 都做得到。讓我們來修正這一點。

已儲存狀態

如果我在包含許多指令碼且執行時間超過幾秒鐘的測試套件中發現測試失敗,重複執行整個測試套件會很快變得乏味,因為我必須追蹤問題。

我可以指示 prove 只執行失敗的測試,如下所示

prove -b t/this_fails.t t/so_does_this.t

這可以加快速度,但我必須記錄失敗的測試,並確保我執行這些測試。相反地,我可以使用 prove 的 --state 開關,讓它為我追蹤失敗的測試。首先,我執行測試套件的完整執行,並指示 prove 儲存結果

prove -rb --state=save t

這會將測試執行的機器可讀摘要儲存在目前資料夾中名為「.prove」的檔案中。如果我有失敗,我可以只執行失敗的指令碼,如下所示

prove -b --state=failed

我也可以指示 prove 再次儲存結果,以便更新它對哪些測試失敗的想法

prove -b --state=failed,save

一旦我的一個失敗測試通過,它就會從失敗測試清單中移除。最後,我修復了所有測試,prove 找不到任何失敗的測試可以執行

Files=0, Tests=0, 0 wallclock secs ( 0.00 usr + 0.00 sys = 0.00 CPU)
Result: NOTESTS

當我處理模組的特定部分時,涵蓋該程式碼的測試最有可能會失敗。我想執行整個測試套件,但讓它優先執行這些「熱門」測試。我可以指示 prove 執行此操作

prove -rb --state=hot,save t

所有測試都會執行,但最近失敗的測試會優先執行。如果自從我開始儲存狀態後沒有任何測試失敗,所有測試都會以其正常順序執行。這結合了完整的測試涵蓋範圍和早期通知失敗。

--state 開關支援許多選項;例如,要先執行失敗的測試,然後再執行所有其他測試(依測試指令碼的時間戳排序),並儲存結果,我可以使用

prove -rb --state=failed,new,save t

請參閱證明文件 (輸入 prove --man) 以取得狀態選項的完整清單。

當我告訴 prove 儲存狀態時,它會在目前目錄中寫入一個名為 '.prove'(Windows 上為 '_prove')的檔案。它是一個 YAML 文件,因此撰寫處理已儲存測試狀態的工具非常容易 - 但格式並未正式記錄,因此未來可能會在沒有(太多)警告的情況下變更。

平行測試

如果我的測試執行時間過長,我可能會透過平行執行多個測試腳本來加快執行速度。如果測試受 I/O 限制或我有許多 CPU 核心,這特別有效。我告訴 prove 以這種方式平行執行我的測試

prove -rb -j 9 t

-j 開關啟用平行測試;其後的數字是平行執行的最大測試數量。有時,順序執行時通過的測試在平行執行時會失敗。例如,如果兩個不同的測試腳本使用相同的暫存檔或嘗試在同一個 socket 上偵聽,我將無法平行執行它們。如果我看到意外的失敗,我需要檢查我的測試以找出哪些測試會踐踏相同的資源,並適當重新命名暫存檔或新增鎖定。

為了獲得最佳效能,我希望執行時間最長的測試腳本最先啟動 - 否則,在所有其他測試完成後,我將等待執行時間將近一分鐘的測試。我可以使用 --state 開關以最慢到最快的順序執行測試

prove -rb -j 9 --state=slow,save t

非 Perl 測試

測試任何事協定 (http://testanything.org/) 不僅適用於 Perl。幾乎任何語言都可以用來撰寫輸出 TAP 的測試。有針對 C、C++、PHP、Python 等語言的 TAP 基礎測試函式庫。如果我找不到適合我選擇語言的 TAP 函式庫,產生有效的 TAP 非常容易。它看起來像這樣

1..3 
ok 1 - init OK 
ok 2 - opened file 
not ok 3 - appended to file

第一行是計畫 - 它指定我將執行的測試數量,以便於檢查測試腳本是否在執行所有預期的測試之前就結束。後續行是測試結果 - 'ok' 表示通過,'not ok' 表示失敗。每個測試都有編號,並可選擇提供說明。就這樣。任何可以在 STDOUT 上產生類似輸出的語言都可以用來撰寫測試。

最近,我重新燃起對 Forth 長達二十年的興趣。顯然,我有一種受虐狂傾向,即使 Perl 也無法滿足。我想用 Forth 撰寫測試,並使用 prove 執行它們(您可以在 https://svn.hexten.net/andy/Forth/Testing/ 找到我的 gforth TAP 實驗)。我可以使用 --exec 開關告訴 prove 使用 gforth 執行測試,如下所示

prove -r --exec gforth t

或者,如果用於撰寫我的測試的語言允許使用 shebang 行,我可以使用它來指定詮譯器。以下是使用 PHP 撰寫的測試

#!/usr/bin/php 
<?php
  print "1..2\n"; 
  print "ok 1\n"; 
  print "not ok 2\n";
?>

如果我將其儲存為 t/phptest.t,則 shebang 行將確保它與我的所有其他測試一起正確執行。

混合

測試程式之間的微妙相互依賴性可能會掩蓋問題,例如,較早的測試可能會忽略移除臨時檔案,而這會影響較後測試的行為。為了找出此類問題,我使用 --shuffle 和 --reverse 選項以隨機或反向順序執行我的測試。

自行開發

如果我需要 prove 未提供的功能,我可以輕鬆自行撰寫。

通常,您會想要變更 TAP 將輸入傳入解析器和將輸出傳出解析器的方式。App::Prove 支援任意外掛程式,而 TAP::Harness 支援自訂格式化程式來源處理程式,您可以使用 proveModule::Build 載入這些程式;有許多範例可供我參考。有關更多詳細資訊,請參閱 App::ProveTAP::Parser::SourceHandlerTAP::Formatter::Base

如果撰寫外掛程式還不夠,您可以撰寫自己的測試架構;Test::Harness 3.00 改寫的動機之一是讓它更容易進行子類化和擴充。

Test::Harness 模組是 TAP::Harness 周圍的相容性包裝器。對於新的應用程式,我應該直接使用 TAP::Harness。正如我們將看到的,prove 使用 TAP::Harness。

當我執行 prove 時,它會處理其引數,找出要執行的測試指令碼,然後將控制權傳遞給 TAP::Harness 以執行測試、解析、分析和呈現結果。透過 TAP::Harness 的子類化,我可以自訂測試執行的許多面向。

我想將我的測試結果記錄在資料庫中,以便我可以追蹤它們的變化。為此,我覆寫 TAP::Harness 中的 summary 方法。我從一個簡單的原型開始,將結果轉儲為 YAML 文件

package My::TAP::Harness;

use base 'TAP::Harness';
use YAML;

sub summary {
  my ( $self, $aggregate ) = @_; 
  print Dump( $aggregate );
  $self->SUPER::summary( $aggregate );
}

1;

我需要告訴 prove 使用 My::TAP::Harness。如果 My::TAP::Harness 在 Perl 的 @INC 包含路徑中,我可以

prove --harness=My::TAP::Harness -rb t

如果我在 @INC 上未安裝 My::TAP::Harness,則需要在執行 prove 時提供正確的路徑給 perl

perl -Ilib `which prove` --harness=My::TAP::Harness -rb t

我可以將這些選項納入我自己的 prove 版本。這很簡單。prove 的大部分工作是由 App::Prove 處理的。prove 中重要的程式碼只有

use App::Prove;

my $app = App::Prove->new; 
$app->process_args(@ARGV); 
exit( $app->run ? 0 : 1 );

如果我撰寫 App::Prove 的子類別,我可以自訂測試執行器的任何面向,同時繼承 prove 的所有行為。以下是 myprove

#!/usr/bin/env perl use lib qw( lib );      # Add ./lib to @INC
use App::Prove;

my $app = App::Prove->new;

# Use custom TAP::Harness subclass
$app->harness( 'My::TAP::Harness' );

$app->process_args( @ARGV ); exit( $app->run ? 0 : 1 );

現在,我可以像這樣執行我的測試

./myprove -rb t

更深入的自訂

現在我知道如何建立 TAP::Harness 的子類別並取代它,我可以取代 Harness 的任何其他部分。為此,我需要知道哪些類別負責哪些功能。以下是簡短的導覽;每個元件的預設類別顯示在括號中。通常,我編寫的任何替換都會是這些預設類別的子類別。

當我執行測試時,TAP::Harness 會建立一個排程器 (TAP::Parser::Scheduler) 來制定測試的執行順序,一個聚合器 (TAP::Parser::Aggregator) 來收集和分析測試結果,以及一個格式化器 (TAP::Formatter::Console) 來顯示這些結果。

如果我並行執行測試,也可能有一個多工器 (TAP::Parser::Multiplexer) - 允許多個測試同時執行的元件。

建立這些輔助程式後,TAP::Harness 會開始執行測試。對於每個測試,它會建立一個新的解析器 (TAP::Parser),負責執行測試腳本並分析其輸出。

若要取代這些元件中的任何一個,我會呼叫這些 Harness 方法之一,並指定替換類別的名稱

aggregator_class 
formatter_class 
multiplexer_class 
parser_class
scheduler_class

例如,若要取代聚合器,我會

$harness->aggregator_class( 'My::Aggregator' );

或者,我可以將替換類別的名稱提供給 TAP::Harness 建構函式

my $harness = TAP::Harness->new(
  { aggregator_class => 'My::Aggregator' }
);

如果我需要更深入地了解 Harness 的內部結構,我可以取代 TAP::Parser 用來執行測試腳本並將其輸出符號化的類別。在執行測試腳本之前,TAP::Parser 會建立一個語法 (TAP::Parser::Grammar) 將原始 TAP 解碼為符號,一個結果工廠 (TAP::Parser::ResultFactory) 將解碼的 TAP 結果轉換為物件,以及,根據它執行的是測試腳本還是從檔案讀取 TAP,一個純量或陣列來源或一個反覆運算器 (TAP::Parser::IteratorFactory)。

這些物件的每個都可以透過呼叫這些解析器方法之一來取代

source_class
perl_source_class 
grammar_class 
iterator_factory_class
result_factory_class

回呼

作為對需要變更元件建立子類別的替代方案,我可以將回呼附加到預設類別。TAP::Harness 公開這些回呼

parser_args      Tweak the parameters used to create the parser 
made_parser      Just made a new parser 
before_runtests  About to run tests 
after_runtests   Have run all tests 
after_test       Have run an individual test script

TAP::Parser 也支援回呼;bailout、comment、plan、test、unknown、version 和 yaml 會針對對應的 TAP 結果類型呼叫,ALL 會針對所有結果呼叫,ELSE 會針對未安裝命名回呼的所有結果呼叫,而 EOF 會在每個 TAP 串流結束時呼叫一次。

若要安裝回呼,我會將回呼名稱和子常式參考傳遞給 TAP::Harness 或 TAP::Parser 的回呼方法

$harness->callback( after_test => sub {
  my ( $script, $desc, $parser ) = @_;
} );

我也可以將回呼傳遞給建構函式

  my $harness = TAP::Harness->new({
    callbacks => {
	    after_test => sub {
        my ( $script, $desc, $parser ) = @_; 
        # Do something interesting here
	    }
    }
  });

在變更測試架構的行為方面,有許多方法可以做到。哪種方法最好取決於我的需求。一般來說,如果我只想觀察測試執行,而不變更架構的行為(例如將測試結果記錄到資料庫),我會選擇回呼。如果我想讓架構有不同的行為,建立子類別可以給我更多控制權。

剖析 TAP

也許我不需要完整的測試架構。如果我已經有需要剖析的 TAP 測試記錄,我只需要 TAP::Parser 和它所依賴的各種類別。以下是執行測試並剖析其 TAP 輸出的程式碼

use TAP::Parser;

my $parser = TAP::Parser->new( { source => 't/simple.t' } );
while ( my $result = $parser->next ) {
  print $result->as_string, "\n";
}

或者,我可以傳遞開啟的檔案句柄作為來源,讓剖析器從該來源讀取,而不是嘗試執行測試指令碼

open my $tap, '<', 'tests.tap' 
  or die "Can't read TAP transcript ($!)\n"; 
my $parser = TAP::Parser->new( { source => $tap } );
while ( my $result = $parser->next ) {
  print $result->as_string, "\n";
}

如果我需要將基於 TAP 的測試結果轉換成其他表示形式,這種方法很有用。請參閱 TAP::Convert::TET (http://search.cpan.org/dist/TAP-Convert-TET/) 以了解此方法的範例。

取得支援

Test::Harness 開發人員會在 tapx-dev 郵件清單[1] 中討論。對於一般、與語言無關的 TAP 問題,可以使用 tap-l[2] 清單。最後,有一個 wiki 專門用於 Test Anything Protocol[3]。歡迎對 wiki、修補程式和建議做出貢獻。

[1] http://www.hexten.net/mailman/listinfo/tapx-dev [2] http://testanything.org/mailman/listinfo/tap-l [3] http://testanything.org/