錯誤所帶來的麻煩 軟件 開發 人員通常都低估了 軟件測試 的重要性。這一現象的根本原因很簡單:處理錯誤很困難!因為錯誤往往暴露了代碼的根本 缺陷 ,所以有時候開發人員甚至會為了幾個錯誤而從頭開始重新編寫項目的主要部分。 我認為,調試如此重要,以至
錯誤所帶來的麻煩 軟件
開發人員通常都低估了
軟件測試的重要性。這一現象的根本原因很簡單:處理錯誤很困難!因為錯誤往往暴露了代碼的根本
缺陷,所以有時候開發人員甚至會為了幾個錯誤而從頭開始重新編寫項目的主要部分。
我認為,調試如此重要,以至於至少要為其分配整個項目 30% 的時間。額外的調試時間將導致更好的產品。另一方面,如果為了更快地推出軟件而縮短調試時間,那麼在軟件生成後,您將花上雙份的時間來修復那些稍後暴露出的問題。
有三種基本類型的錯誤:編碼錯誤、文檔錯誤和
需求錯誤。需求錯誤通常由於需求不嚴密或缺少需求而導致。文檔錯誤存在於手冊或聯機幫助中。編碼錯誤是由
程序員在實現需求時的錯誤而引起的。不幸的是,需求錯誤和文檔錯誤不在本文范圍之內,因此,我們只好只討論如何“檢測”、“解決”和 “修復”編碼錯誤了。
調試的基本概念 我們已經將編碼錯誤定義成程序員在實現需求時產生的錯誤。編碼錯誤會導致不正確的程序行為(偏離需求的行為)。因此,程序員在編寫或調試程序之前首先應該知道的是程序需求。
調試與狩獵沒什麼不同。第一步是檢測錯誤(通過觀察錯誤的行為並確認其模式)。在這個階段,錯誤只是一些症狀。
第二步是解決錯誤。因為必須要在源代碼中消除錯誤,所以,應該有一個精通程序的人來檢查錯誤,並知道這些錯誤的根本原因。如果代碼理解起來更容易,並且現在的代碼沒有比當初錯誤版本中的代碼更多,則您可能做對了。
第三步,也是最後一步,是修復錯誤(請注意“修復”與“解決”是有區別的)。調試程序將源代碼更改放入“現場”的生產過程,然後檢查它是否正確。如果代碼不正確,則表明您沒有解決錯誤,甚至更糟糕的是,可能還引入了新的錯誤。既然解決錯誤的目的不應該是引入新錯誤,請確保在解決錯誤之後修復每個錯誤。
要確保迅速找到錯誤並很好地理解它們,您應該對調試過程中程序使用模塊和類在每個主要分支處的操作非常清楚。當然,這要求您對編寫代碼所用的語言(在我們的示例中是 Perl)有深入的了解。因為存在所有這些需求,所以很難找到好的軟件
測試人員。
Perl 調試器 Perl 程序員的第一個資源是 Perl 所帶的調試器。如您所見,著手使用該調試器是非常容易的。
用調試器運行一個腳本
perl -d program.pl
Perl 調試器自帶幫助('h' 或 'h h' 分別用於詳細和簡短的幫助屏幕)。perldoc perldebug 頁面(在命令提示窗口輸入 "perldoc perldebug")有更完整的 Perl 調試器描述。
現在,讓我們從一個有錯誤的程序著手,看一下 Perl 調試器是如何工作的。首先,它將嘗試打印一個文件的前 20 行。
buggy.pl
#!/usr/bin/perl -w
use strict;
foreach (0..20)
{
my $line = <>;
print "$_ : $line";
}
當它獨自運行時,buggy.pl 將失敗,並給出消息:"Use of uninitialized value in concatenation (.) at ./buggy.pl line 8, <> line 9."。更神秘的是,它還自己在一行上打印 "9:" 並等待用戶輸入。
那意味著什麼?如果調用了 Perl 調試器,您可能已經找到問題所在了。
首先,讓我們證實這個錯誤是可以重復的。我們將在第 8 行設置一個操作來打印發生錯誤的 $line,然後再運行程序。
buggy.pl 調試器命令
> perl -d ./buggy.pl buggy.pl
Default die handler restored.
Loading DB routines from perl5db.pl version 1.07
Editor support available.
Enter h or `h h' for help, or `man perldebug' for more help.
main::(./buggy.pl:5): foreach (0..20)
main::(./buggy.pl:6): {
DB<1> use Data::Dumper
DB<2> a 8 print 'The line variable is now ', Dumper $line
裝入了 Data::Dumper 模塊,以便自動操作可以使用一種美觀的輸出格式。自動操作被設置成每次到達第 8 行時都執行打印語句。現在,讓我們演示一下。
buggy.pl 調試器命令,第 2 部分
DB<3> c
The line variable is now $VAR1 = '#!/usr/bin/perl -w
';
0 : #!/usr/bin/perl -w
The line variable is now $VAR1 = '
';
1 :
The line variable is now $VAR1 = 'use strict;
';
2 : use strict;
The line variable is now $VAR1 = '
';
3 :
The line variable is now $VAR1 = 'foreach (0..20)
';
4 : foreach (0..20)
The line variable is now $VAR1 = '{
';
5 : {
The line variable is now $VAR1 = ' my $line = <>;
';
6 : my $line = <>;
The line variable is now $VAR1 = ' print "$_ : $line";
';
7 : print "$_ : $line";
The line variable is now $VAR1 = '}
';
8 : }
The line variable is now $VAR1 = undef;
Use of uninitialized value in concatenation (.) at ./buggy.pl line 8, <> line 9.
9 :
現在很清楚,沒有定義行變量時就會出問題。而且,程序等待更多的輸入。再按 11 次回車鍵產生了以下輸出:
buggy.pl 調試器命令,第 3 部分
The line variable is now $VAR1 = '
';
10 :
The line variable is now $VAR1 = '
';
11 :
The line variable is now $VAR1 = '
';
12 :
The line variable is now $VAR1 = '
';
13 :
The line variable is now $VAR1 = '
';
14 :
The line variable is now $VAR1 = '
';
15 :
The line variable is now $VAR1 = '
';
16 :
The line variable is now $VAR1 = '
';
17 :
The line variable is now $VAR1 = '
';
18 :
The line variable is now $VAR1 = '
';
19 :
The line variable is now $VAR1 = '
';
20 :
Debugged program terminated. Use q to quit or R to restart,
use O inhibit_exit to avoid stopping after program termination,
h q, h R or h O to get additional info.
DB<3>
到現在為止已經很清楚了,由於即使在不存在行的情況下,程序仍無條件地等待 20 行的輸入,所以程序會出錯。修復就是要在從 <> filehandle 讀取 $line 之後測試它:
buggy.pl fixed
#!/usr/bin/perl -w
use strict;
foreach (0..20)
{
my $line = <>;
last unless defined $line;# exit loop if $line is not defined
print "$_ : $line";
}
如您所見,修復過的程序在所有情況下都可以正確工作!
關於 Perl 調試器的結論 Emacs 編輯器支持 Perl 調試器並使其更易於使用。您可以在 Emacs 中使用 Info(輸入 M-x info)來閱讀有關 GUD Emacs 的更詳細信息。GUD 是與 Perl 調試器一起工作的全局調試方式(當在 Emacs 中編輯 Perl 程序時輸入 M-x perldb)。
只需少量工作就可以讓 vi 系列的編輯器也能支持 Perl 調試器。有關詳細信息,請參閱 perldoc perldebug 頁面。有關其它編輯器的信息,請參考每個編輯器的文檔。
Perl 內置的調試器是一個強大的工具,可以執行比我們剛剛看到的簡單用法復雜得多的任務。但它的確要求使用者具備大量 Perl 專門
知識。正因為如此,我們現在要看一些簡單些的工具,這些工具將更適合初級和中級 Perl 程序員。
Devel::ptkdb
要使用 Devel::ptkdb 調試器,首先得從 CPAN(請參閱下面的參考資料)
下載它並將它安裝在您的系統上。(某些用戶可能還需要安裝 Tk 模塊,該模塊也可以從 CPAN 獲得。)就我個人看來,Devel::ptkdb 在 UNIX 系統(如
Linux)上最好用。(雖然在理論上 Devel::ptkdb 並不限於與 UNIX 兼容的系統,但是,我從未聽說過有人成功地在
Windows 上使用 Devel::ptkdb。正如一句老話所講:除了滑雪穿過旋轉門之外,任何事都是可能的。)
如果無法讓系統管理員為您安裝(例如,因為您自己就是系統管理員),可以嘗試在命令提示行執行以下操作(可能需要以 root 身份執行這些操作):
從 CPAN 安裝 Devel::ptkdb
perl -MCPAN -e'install Tk'
perl -MCPAN -e'install Devel::ptkdb'
如果是第一次運行 CPAN 安裝例程,那麼,在回答一些初始問題之後,將自動下載並安裝適當的模塊。
可以用 ptkdb 調試器運行程序,如下所示(使用我們以前的 buggy.pl 示例):
使用 Devel::ptkdb
perl -d:ptkdb buggy.pl buggy.pl
要閱讀 Devel::ptkdb 模塊的文檔,請使用命令 "perldoc Devel::ptkdb"。我們在本文中使用版本 1.1071。(雖然更新的版本可能隨時問世,但它們與我們正在使用的版本應該沒有很大的不同。)
將出現一個窗口,在該窗口的左側是程序源代碼,右側是觀察過的表達式列表(初始為空)。在 "Enter Expr:" 框中輸入字 "$line"。然後單擊 "Step Over" 按鈕觀察程序的執行情況。
"Run" 按鈕將運行程序,直到運行完畢或到遇到斷點為止。單擊源代碼清單窗口中的行號可以設置或刪除斷點。如果選擇右側的 "BrkPts" 選項卡,則可以編輯斷點列表,並使它們受變量或函數的制約。(用這種方法設置條件斷點非常簡單。)
Ptkdb 還有 File、Control、Data、Stack 和 Bookmarks 菜單。這些菜單全部在 perldoc 文檔中解釋。因為 Ptkdb 使用起來如此方便,所以,它絕對是初級和中級 Perl 程序員的必備工具。甚至對於 Perl 高手,它也很有用(只要他們不告訴任何人他們正在使用那些新款圖形界面)。
編寫自己的 Perl shell 有時使用調試器顯得大材小用。例如,如果要與大型程序的其余部分隔離來單獨測試某些簡單的代碼,那麼對於這種任務,調試器就顯得過於復雜。這時,Perl shell 就派上用場了。
當然還有其它一些有效的方法來實現 Perl shell,但是,我們將要看到的是一種常規的
解決方案,該方案非常適用於大多數日常工作。一旦理解了這個工具,就應該可以隨意地按照您的需要和喜好來修改它。
以下代碼需要 Term::ReadLine 模塊。使用幾乎與 Devel::ptkdb 相同的方式從 CPAN 下載並安裝它。
Perl shell #!/usr/bin/perl -w
use Term::ReadLine;
use Data::Dumper;
my $historyfile = $ENV{HOME} . '/.phistory';
my $term = new Term::ReadLine 'Perl Shell';
sub save_list
{
my $f = shift;
my $l = shift;
open F, $f;
print F "$_\n" foreach @$l
}
if (open H, $historyfile)
{
@h = ;
chomp @h;
close H;
$h{$_} = 1 foreach @h;
$term->addhistory($_) foreach keys %h;
}
while ( defined ($_ = $term->readline("My Perl Shell> ")) )
{
my $res = eval($_);
warn $@ if $@;
unless ($@)
{
open H, ">>$historyfile";
print H "$_\n";
close H;
print "\n", Data::Dumper->Dump([$res], ['Result']);
}
$term->addhistory($_) if /\S/;
}
Perl shell 可以極好地完成幾項工作,也可以較好地完成某些工作。
首先,它在您的主目錄中名為 ".phistory" 的文件內保留一個唯一的、已經輸入的命令歷史記錄。如果對同一條命令輸入兩次,將只保留一條命令(請參閱用於打開 $historyfile 並從中讀取歷史行的函數)。
每輸入一條新命令,就將命令列表保存到 .phistory 文件中。因此,如果輸入一條導致 shell 崩潰的命令,上一次會話的歷史記錄將不會丟失。
Term::ReadLine 模塊使輸入命令來執行更加容易。因為將命令限制成每次僅一行,所以可以將前面不錯的 buggy.pl 編寫為:
Perl shell 中的 buggy.pl
Da Perl Shell> use strict
$Result = undef;
Perl Shell> print "$_: " . <> foreach (0..20)
0: ...
1: ...
問題當然是 <> input 操作符最終會吃掉 shell 自己的輸入。因此,不要在 Perl shell 中使用 <> 或 STDIN,因為它們會使事情更困難。您可以試一下這個:
Perl shell 中修復了錯誤的 buggy.pl
Perl Shell> open F, "buggy.pl"
$Result = 1;
Perl Shell> foreach (0..20) { last if eof(F); print "$_: " . ; }
0: #!/usr/bin/perl -w
1:
2: use strict;
3:
4: foreach (0..20)
5: {
6: my $line = <>;
7: last unless defined $line; # exit loop if $line is not defined
8: print "$_ : $line";
9: }
$Result = undef;
如您所見,shell 可以使您輕松地將語句精簡成一行。它還是非常普遍的一種隔離錯誤的解決方案,並提供了極好的學習環境。自己實踐一下,看看是否可以自己編寫一個 Perl shell 來進行調試,並看看您學到了多少。
構建工具庫 我們已經討論了內置 Perl 調試器、Devel::ptkdb 和相關工具的最基本內容。還有很多調試 Perl 的方法。重要的是要理解調試過程:如何發現、解決和修復錯誤。當然,最重要的一點是確保深入理解程序的需求。
Perl 內置的調試器非常強大,但是它不適合於初級或中級 Perl 程序員。(Emacs 是個例外,只要理解了 Emacs 下的調試,那麼即使對於初學者,它也可以稱得上是一種有用的工具。)
目前為止,Devel::ptkdb 模塊和調試器(因為它們的能力和易用性)是初級和中級程序員最好的選擇。另一方面,Perl shell 是用於解決少量代碼中孤立問題的個性化調試解決方案。
無論是帶有 GUD 的 Emacs 編輯器,還是 Perl shell,或者是代碼中的打印語句,每一個軟件測試人員都構建自己的調試工具集。希望我們本文所討論的工具可以將您的調試過程變得更輕松。