目錄

名稱

Test::Tutorial - 撰寫真正基本測試的教學

說明

啊!不測試!什麼都可以,就是不要測試!打我、鞭我、把我送到底特律,但不要讓我寫測試!

*啜泣*

此外,我不知道如何撰寫該死的測試。

你也是這樣嗎?撰寫測試是不是跟撰寫文件和拔指甲一樣痛苦?你打開一個測試並讀到

######## We start with some black magic

並決定這樣對你來說已經足夠了?

沒問題。現在一切都結束了。我們已經為你完成了所有黑魔法。以下是這些技巧...

測試的基礎。

以下是基本測試程式。

#!/usr/bin/perl -w

print "1..1\n";

print 1 + 1 == 2 ? "ok 1\n" : "not ok 1\n";

因為 1 + 1 等於 2,它會列印

1..1
ok 1

這表示:1..1「我要執行一個測試。」[1] ok 1「第一個測試已通過」。這就是測試中的所有魔法。你的基本測試單位是ok。對於你測試的每件事,會列印一個ok。很簡單。 Test::Harness 會詮釋你的測試結果,以確定你是否成功或失敗(稍後會詳細說明)。

快速撰寫所有這些列印陳述會很快變得乏味。幸運的是,有 Test::Simple。它有一個函式,ok()

#!/usr/bin/perl -w

use Test::Simple tests => 1;

ok( 1 + 1 == 2 );

這與前一個程式碼的作用相同。ok() 是 Perl 測試的支柱,我們將從現在開始使用它,而不是自己撰寫。如果 ok() 獲得真值,則測試通過。如果為假,則測試失敗。

#!/usr/bin/perl -w

use Test::Simple tests => 2;
ok( 1 + 1 == 2 );
ok( 2 + 2 == 5 );

從中產生

1..2
ok 1
not ok 2
#     Failed test (test.pl at line 5)
# Looks like you failed 1 tests of 2.

1..2「我要執行兩個測試。」這個數字是一個計畫。它有助於確保你的測試程式完全執行,而且沒有中斷或略過某些測試。ok 1「第一個測試已通過。」not ok 2「第二個測試已失敗」。Test::Simple 會貼心地列印出一些關於你的測試的額外註解。

這並不可怕。來,牽著我的手。我們將提供測試模組的範例。對於我們的範例,我們將測試日期函式庫 Date::ICal。它在 CPAN 上,所以請下載一份副本並照著做。[2]

從何開始?

這是測試中最困難的部分,你從哪裡開始?人們常常會對測試整個模組這項任務的龐大性感到不知所措。開始的最佳位置是從頭開始。 Date::ICal 是個物件導向模組,這表示你從建立物件開始。測試 new()

#!/usr/bin/perl -w

# assume these two lines are in all subsequent examples
use strict;
use warnings;

use Test::Simple tests => 2;

use Date::ICal;

my $ical = Date::ICal->new;         # create an object
ok( defined $ical );                # check that we got something
ok( $ical->isa('Date::ICal') );     # and it's the right class

執行它,你應該會得到

1..2
ok 1
ok 2

恭喜!你已經寫了你的第一個有用的測試。

名稱

那個輸出並不是很具描述性,對吧?當你執行兩個測試時,你可以找出哪一個是 #2,但如果你有 102 個測試呢?

每個測試都可以給予一個簡短的描述性名稱,作為 ok() 的第二個引數。

use Test::Simple tests => 2;

ok( defined $ical,              'new() returned something' );
ok( $ical->isa('Date::ICal'),   "  and it's the right class" );

現在你會看到

1..2
ok 1 - new() returned something
ok 2 -   and it's the right class

測試手冊

建立一個不錯的測試套件最簡單的方法就是測試手冊中所述的功能。[3] 讓我們從 Date::ICal 中的「SYNOPSIS」 中取出一些內容,並測試其所有部分是否運作正常。

#!/usr/bin/perl -w

use Test::Simple tests => 8;

use Date::ICal;

$ical = Date::ICal->new( year => 1964, month => 10, day => 16,
                         hour => 16,   min   => 12, sec => 47,
                         tz   => '0530' );

ok( defined $ical,            'new() returned something' );
ok( $ical->isa('Date::ICal'), "  and it's the right class" );
ok( $ical->sec   == 47,       '  sec()'   );
ok( $ical->min   == 12,       '  min()'   );
ok( $ical->hour  == 16,       '  hour()'  );
ok( $ical->day   == 17,       '  day()'   );
ok( $ical->month == 10,       '  month()' );
ok( $ical->year  == 1964,     '  year()'  );

執行後,您會得到

1..8
ok 1 - new() returned something
ok 2 -   and it's the right class
ok 3 -   sec()
ok 4 -   min()
ok 5 -   hour()
not ok 6 -   day()
#     Failed test (- at line 16)
ok 7 -   month()
ok 8 -   year()
# Looks like you failed 1 tests of 8.

糟糕,失敗了![4] Test::Simple 會貼心地告訴我們失敗發生在哪一行,但不會提供太多其他資訊。我們應該得到 17,但我們沒有。我們得到了什麼?不知道。您可以在偵錯器中重新執行測試,或加入一些列印陳述式來找出答案。

改用 Test::More,取代 Test::SimpleTest::More 能執行 Test::Simple 的所有功能,還有更多!事實上, Test::More 執行事情的方式與 Test::Simple 完全 相同。您真的可以將 Test::Simple 換成 Test::More。我們就是要這麼做。

Test::More 的功能比 Test::Simple 多。目前最重要的差異在於,它提供了更多有意義的方式來說「ok」。雖然您可以使用一般性的 ok() 撰寫幾乎所有測試,但它無法告訴您出了什麼問題。is() 函數讓我們宣告某個東西應該與另一個東西相同

use Test::More tests => 8;

use Date::ICal;

$ical = Date::ICal->new( year => 1964, month => 10, day => 16,
                         hour => 16,   min   => 12, sec => 47,
                         tz   => '0530' );

ok( defined $ical,            'new() returned something' );
ok( $ical->isa('Date::ICal'), "  and it's the right class" );
is( $ical->sec,     47,       '  sec()'   );
is( $ical->min,     12,       '  min()'   );
is( $ical->hour,    16,       '  hour()'  );
is( $ical->day,     17,       '  day()'   );
is( $ical->month,   10,       '  month()' );
is( $ical->year,    1964,     '  year()'  );

$ical->sec 是 47 嗎?」「$ical->min 是 12 嗎?」使用 is() 之後,您會得到更多資訊

1..8
ok 1 - new() returned something
ok 2 -   and it's the right class
ok 3 -   sec()
ok 4 -   min()
ok 5 -   hour()
not ok 6 -   day()
#     Failed test (- at line 16)
#          got: '16'
#     expected: '17'
ok 7 -   month()
ok 8 -   year()
# Looks like you failed 1 tests of 8.

啊哈。$ical->day 回傳 16,但我們預期是 17。快速檢查後發現程式碼運作正常,我們在撰寫測試時犯了錯誤。將其變更為

is( $ical->day,     16,       '  day()'   );

... 一切就正常了。

任何時候您執行「這個等於那個」類型的測試時,請使用 is()。它甚至可以在陣列上運作。測試永遠在純量環境中,因此您可以用這種方式測試陣列中有多少個元素。[5]

is( @foo, 5, 'foo has 5 elements' );

有時測試是錯誤的

這點出了非常重要的教訓。程式碼有錯誤。測試是程式碼。因此,測試有錯誤。測試失敗可能表示程式碼有錯誤,但不要排除測試本身有錯的可能性。

另一方面,不要因為您在尋找錯誤時遇到困難,就急著宣告測試不正確。將測試視為不正確不是一件小事,不要將其用作逃避工作的藉口。

測試大量值

我們將要在此測試大量日期,嘗試用許多不同的邊界條件來欺騙程式碼。它在 1970 年之前有效嗎?2038 年之後呢?1904 年之前呢?10,000 年之後的年份會造成問題嗎?它能正確處理閏年嗎?我們可以一直重複上述程式碼,或者我們可以設定一個小型的嘗試/預期迴圈。

use Test::More tests => 32;
use Date::ICal;

my %ICal_Dates = (
        # An ICal string     And the year, month, day
        #                    hour, minute and second we expect.
        '19971024T120000' =>    # from the docs.
                            [ 1997, 10, 24, 12,  0,  0 ],
        '20390123T232832' =>    # after the Unix epoch
                            [ 2039,  1, 23, 23, 28, 32 ],
        '19671225T000000' =>    # before the Unix epoch
                            [ 1967, 12, 25,  0,  0,  0 ],
        '18990505T232323' =>    # before the MacOS epoch
                            [ 1899,  5,  5, 23, 23, 23 ],
);


while( my($ical_str, $expect) = each %ICal_Dates ) {
    my $ical = Date::ICal->new( ical => $ical_str );

    ok( defined $ical,            "new(ical => '$ical_str')" );
    ok( $ical->isa('Date::ICal'), "  and it's the right class" );

    is( $ical->year,    $expect->[0],     '  year()'  );
    is( $ical->month,   $expect->[1],     '  month()' );
    is( $ical->day,     $expect->[2],     '  day()'   );
    is( $ical->hour,    $expect->[3],     '  hour()'  );
    is( $ical->min,     $expect->[4],     '  min()'   );
    is( $ical->sec,     $expect->[5],     '  sec()'   );
}

現在我們可以透過將日期新增到 %ICal_Dates 來測試大量日期。由於現在使用更多日期進行測試的工作量較少,因此當你想到時,你會傾向於加入更多日期。唯一的問題是,每次我們新增時,我們都必須持續調整 use Test::More tests => ## 行。這可能會迅速變得令人厭煩。有方法可以讓這項工作做得更好。

首先,我們可以使用 plan() 函數動態計算計畫。

use Test::More;
use Date::ICal;

my %ICal_Dates = (
    ...same as before...
);

# For each key in the hash we're running 8 tests.
plan tests => keys(%ICal_Dates) * 8;

...and then your tests...

為了更具彈性,請使用 done_testing。這表示我們只是在執行一些測試,不知道有多少。[6]

use Test::More;   # instead of tests => 32

... # tests here

done_testing();   # reached the end safely

如果你未指定計畫,Test::More 會預期在你的程式退出之前看到 done_testing()。如果你忘記了,它會警告你。你可以給 done_testing() 一個你預期執行的測試數量,如果執行的數量不同,Test::More 會給你另一種警告。

具資訊性的名稱

看看這行

ok( defined $ical,            "new(ical => '$ical_str')" );

我們已在名稱中新增了更多關於我們正在測試的內容以及我們正在嘗試的 ICal 字串本身的詳細資料。因此,你會得到以下結果

ok 25 - new(ical => '19971024T120000')
ok 26 -   and it's the right class
ok 27 -   year()
ok 28 -   month()
ok 29 -   day()
ok 30 -   hour()
ok 31 -   min()
ok 32 -   sec()

如果其中的某個項目失敗,你將知道是哪一個,這將使追蹤問題變得更容易。嘗試在測試名稱中放入一些除錯資訊。

描述測試測試的內容,以便為你或執行測試的下一位人員除錯失敗的測試。

略過測試

在現有的 Date::ICal 測試中搜尋,我在 t/01sanity.t 中找到這個 [7]

#!/usr/bin/perl -w

use Test::More tests => 7;
use Date::ICal;

# Make sure epoch time is being handled sanely.
my $t1 = Date::ICal->new( epoch => 0 );
is( $t1->epoch, 0,          "Epoch time of 0" );

# XXX This will only work on unix systems.
is( $t1->ical, '19700101Z', "  epoch to ical" );

is( $t1->year,  1970,       "  year()"  );
is( $t1->month, 1,          "  month()" );
is( $t1->day,   1,          "  day()"   );

# like the tests above, but starting with ical instead of epoch
my $t2 = Date::ICal->new( ical => '19700101Z' );
is( $t2->ical, '19700101Z', "Start of epoch in ICal notation" );

is( $t2->epoch, 0,          "  and back to ICal" );

大多數非 Unix 作業系統的紀元開始時間不同 [8]。儘管 Perl 在大部分情況下會消除差異,但某些埠會以不同的方式執行。MacPerl 就是其中之一。[9]與在測試中放置註解並希望有人在除錯失敗時閱讀測試相比,我們可以明確表示它永遠不會起作用並略過測試。

use Test::More tests => 7;
use Date::ICal;

# Make sure epoch time is being handled sanely.
my $t1 = Date::ICal->new( epoch => 0 );
is( $t1->epoch, 0,          "Epoch time of 0" );

SKIP: {
    skip('epoch to ICal not working on Mac OS', 6)
        if $^O eq 'MacOS';

    is( $t1->ical, '19700101Z', "  epoch to ical" );

    is( $t1->year,  1970,       "  year()"  );
    is( $t1->month, 1,          "  month()" );
    is( $t1->day,   1,          "  day()"   );

    # like the tests above, but starting with ical instead of epoch
    my $t2 = Date::ICal->new( ical => '19700101Z' );
    is( $t2->ical, '19700101Z', "Start of epoch in ICal notation" );

    is( $t2->epoch, 0,          "  and back to ICal" );
}

這裡發生了一點點神奇的事。在任何作業系統上執行時,所有測試都會正常執行。但在 MacOS 上,skip() 會導致 SKIP 區塊的整個內容被跳過。它永遠不會執行。相反,skip() 會列印特殊輸出,告訴 Test::Harness 測試已略過。

1..7
ok 1 - Epoch time of 0
ok 2 # skip epoch to ICal not working on MacOS
ok 3 # skip epoch to ICal not working on MacOS
ok 4 # skip epoch to ICal not working on MacOS
ok 5 # skip epoch to ICal not working on MacOS
ok 6 # skip epoch to ICal not working on MacOS
ok 7 # skip epoch to ICal not working on MacOS

這表示你的測試不會在 MacOS 上失敗。這表示來自 MacPerl 使用者的電子郵件較少,告訴你關於你已知道永遠不會運作的失敗測試。你必須小心略過測試。這些測試是針對無法運作且永遠不會運作的測試。它不是用來略過真正的錯誤(我們將在稍後討論)。

測試完全略過。[10] 這會有效。

SKIP: {
    skip("I don't wanna die!");

    die, die, die, die, die;
}

待辦事項測試

在瀏覽 Date::ICal 手冊頁時,我遇到了這個

ical

    $ical_string = $ical->ical;

Retrieves, or sets, the date on the object, using any
valid ICal date/time string.

「擷取或設定」。嗯。我沒看到使用 ical() 在 Date::ICal 測試套件中設定日期的測試。所以我寫了一個

use Test::More tests => 1;
use Date::ICal;

my $ical = Date::ICal->new;
$ical->ical('20201231Z');
is( $ical->ical, '20201231Z',   'Setting via ical()' );

執行它。我看到

1..1
not ok 1 - Setting via ical()
#     Failed test (- at line 6)
#          got: '20010814T233649Z'
#     expected: '20201231Z'
# Looks like you failed 1 tests of 1.

糟糕!看起來它未實作。假設你沒有時間修正這個。[11] 一般來說,你只要將測試註解掉,然後在某個待辦事項清單中加上備註。相反地,透過將它包在 TODO 區塊中,明確地表示「這個測試會失敗」

use Test::More tests => 1;

TODO: {
    local $TODO = 'ical($ical) not yet implemented';

    my $ical = Date::ICal->new;
    $ical->ical('20201231Z');

    is( $ical->ical, '20201231Z',   'Setting via ical()' );
}

現在當你執行時,會有點不同

1..1
not ok 1 - Setting via ical() # TODO ical($ical) not yet implemented
#          got: '20010822T201551Z'
#     expected: '20201231Z'

Test::More 沒有說「看起來你 1 個測試中失敗了 1 個」。那個「# TODO」告訴 Test::Harness 「這應該會失敗」,並且它將失敗視為成功的測試。你可以在修正底層程式碼之前就撰寫測試。

如果 TODO 測試通過,Test::Harness 會回報它「意外成功」。當發生這種情況時,請使用 local $TODO 移除 TODO 區塊,並將它變成真正的測試。

使用 taint 模式進行測試。

Taint 模式是一件有趣的事。它是所有全域功能中最全域的。一旦你開啟它,它就會影響程式中的所有程式碼和所有已使用的模組(以及它們使用的所有模組)。如果單一程式碼片段不是 taint 乾淨的,整個東西就會爆炸。有鑑於此,確保你的模組在 taint 模式下運作非常重要。

讓你的測試在 taint 模式下執行非常簡單。只要在 #! 行中加入一個 -TTest::Harness 會讀取 #! 中的開關,並使用它們來執行你的測試。

#!/usr/bin/perl -Tw

...test normally here...

當你說 make test 時,它會在開啟 taint 模式的情況下執行。

腳註

  1. 第一個數字並沒有什麼特別的意義,但它必須是 1。重要的是第二個數字。

  2. 對於那些在家中追蹤的人來說,我使用的是 1.31 版。它有一些錯誤,這很好——我們會用我們的測試找出它們。

  3. 你實際上可以更進一步,測試手冊本身。看看 Test::Inline(以前的 Pod::Tests)。

  4. 是的,測試套件中有一個錯誤。什麼!我,捏造的?

  5. 我們稍後會測試清單的內容。

  6. 但是如果你的測試程式在中途死亡會怎樣?由於我們沒有說明要執行多少個測試,我們怎麼知道它失敗了?沒問題,Test::More 使用了一些魔法來捕捉那個死亡,並將測試變成失敗,即使每個測試都通過了那個點。

  7. 我把它清理了一下。

  8. 大多數作業系統會將時間記錄為自某個日期以來經過的秒數。這個日期就是紀元的開始。Unix 的紀元開始於 1970 年 1 月 1 日格林威治標準時間午夜。

  9. MacOS 的紀元開始於 1904 年 1 月 1 日午夜。VMS 的紀元開始於 1858 年 11 月 17 日午夜,但 vmsperl 模擬 Unix 紀元,因此這不是問題。

  10. 只要 SKIP 區塊內的程式碼至少編譯。請不要問如何。不,這不是一個篩選器。

  11. 不要想用 TODO 測試來避免修復簡單的錯誤!

作者

Michael G Schwern <schwern@pobox.com> 和 perl-qa 舞者!

維護者

Chad Granum <exodist@cpan.org>

版權

版權所有 2001 年 Michael G Schwern <schwern@pobox.com>。

此文件是免費的;您可以在與 Perl 相同的條款下重新分發或修改它。

不論其分發方式為何,這些檔案中的所有程式碼範例在此特此置於公有領域。您被允許且鼓勵在自己的程式中使用此程式碼,以供娛樂或營利,視您所見為宜。在程式碼中加上一個簡單的註解以表示感謝會很客氣,但不是必要的。