PHP與撰寫測試入門

許多人對於撰寫測試躍躍欲試,卻因為PHPUnit等工具太複雜、

相關名詞艱澀難懂而不知該如何開始。

其實PHPUnit這類測試工具並非必須品,相關專有名詞也可以先擺一邊,

只要發揮創意想辦法測試就是第一步了。

我們從最簡單的形式開始談起吧!

假設我們有一個簡單的加法程式要測試


最簡單的測試其實只要這樣寫


要執行測試非常簡單,只要打開terminal然後輸入

php simple_add_test.php

就可以看到測試結果了!

PHP Parse error:  syntax error, unexpected '{' in /home/howtomakeaturn/projects/testing-tutorial/simple_add_test.php on line 10

突然發現if打成了$if,修正之後再次執行的結果是

PHP Parse error:  syntax error, unexpected '}', expecting ',' or ';' in /home/howtomakeaturn/projects/testing-tutorial/simple_add_test.php on line 12

發現少打一個分號,修正之後再次執行的結果是

PHP Parse error:  syntax error, unexpected '';' (T_ENCAPSED_AND_WHITESPACE), expecting ',' or ';' in /home/howtomakeaturn/projects/testing-tutorial/simple_add_test.php on line 13

發現多打一個單引號,修正之後再次執行的結果是

simple_add_test passed

終於通過測試了!

注意到了嗎?光是替這樣簡單的程式撰寫簡單的測試,就可以省下不少開發時間:原本要打開瀏覽器重新整理三次才能修完bug(人工測試3次),變成只要在terminal跑三次就修完bug了!

Q1: 打開瀏覽器重新整理三次其實沒花多少時間...寫測試真麻煩,而且不划算。

有人說寫測試一定划算,也有人說寫測試一定不划算,其實都是不對的說法!
某些情況下確實不用寫測試,光靠瀏覽器就人工測試完成了。
但很多時候人工測試的時間會大於撰寫測試的時間。譬如說一個需要先登入、輸入一串假資料、東點西點按鈕才出現結果的頁面。那種時候,寫測試就會划算了。

Q2: 二加三等於五是什麼爛測試...,也太不嚴謹了吧。

這類稱之為happy path的測試確實非常隨便。但是以入門來說,光寫這類測試其實已經有很出色的測試效果:

1. syntax error全部會被找到
2. 至少確認happy path通過了。很多時候程式出錯就是連這種happy path都過不了

不過,為了謹慎起見,我們來寫更完整的測試吧!


接著在terminal輸入

php simple_add_better_test.php

會看到結果顯示

simple_add_test passed
simple_add_test passed
simple_add_test passed
simple_add_test passed

恭喜,通過更嚴酷的測試了!

Q3: 老實說你的測試還是很爛...只是拿1跟2跟0跟-1隨便測一下而已,實在不能保證什麼。

沒有錯,其實軟體測試從來就無法保證軟體不會出錯。
有人說軟體測試是為了讓程式正確執行,其實不太正確!
軟體測試只是為了讓你對程式有信心而已。
要測到什麼程度則取決於:

1. 你心臟有多大顆
2. 這個程式值得你花多少時間去測到何種程度

以你的高標準來說,可以再拿999,999,999跟-999,999,999,999,999,999等數字去測,甚至拿null、空字串、亂數字串丟進去測(如果你覺得有意義的話)。

很不幸的,大部份的程式不會像上面那樣單純顯示結果,而是會包含一堆html。
像是一個新增文章的程式,使用者會從表單填寫標題與內容,接著以HTTP POST送出。


Title:

像這樣結果複雜的程式,該怎麼測試呢?

嘿嘿,還記得一開始說的「只要發揮創意想辦法測試」嗎?

我們來檢查最後結果有沒有包含特定字串吧!


接著在terminal輸入

php blog_submit_test.php

會看到結果顯示

blog_submit_test for title passed
blog_submit_test for content passed

奇妙吧!根本還沒打開瀏覽器,就幾乎可以確定這個新增文章的程式沒問題了!
不但syntax確認沒問題,也很有信心會正確新增文章!

Q4: 這個測試未免也太爛了。一看就知道bug一堆。要是測試標題輸入「Title」不就會抓到h1裡面那個「Title」?那就是根本沒測到!另一個測試「I am happy」更扯,如果頁面其他地方本來就有字串包含「I am happy」,不就毫無意義嗎?

說得真好!不過Q3已經說了,軟體測試只是為了對程式有信心而已。
雖然只寫了兩個破爛測試,但我已經對程式很有信心囉!我認為OK的!
而且不是說了要發揮創意嗎?不然我還能怎麼測呢?

好吧,其實會這麼鳥是因為呈現邏輯(presentation logic)和商業邏輯(business logic)混在一起的關係。
把business logic獨立出來測吧!

先把business logic獨立出來放進另一個檔案,包進function內


再改一下原本程式


Title:

測試則改寫成


接著在terminal輸入

php blog_submit_v2_test.php

會看到結果顯示

blog_submit_test for title passed
blog_submit_test for content passed

酷吧!你提到的「頁面其他地方本來就有字串包含『I am happy』」的bug解決了!
這下是不是對程式更有信心了?
除此之外,現在測試裡面不再硬塞value給$_POST變數,也不再需要output buffering的技巧了。
事情更單純了吧。你常聽到有人說測試要一小塊一小塊測,就是這麼一回事。

Q5: 不對喔...我發現事情不對勁。這下你的測試根本沒include到blog_submit_v2.php這支檔案!檔名根本不配blog_submit_v2_test.php。變成只測到business logic而沒測到presentation logic了。

我測完business logic其實已經很安心了,至於presentation logic的話我會直接打開瀏覽器,頁面正確顯示我就安心了。你真要那麼教條化嗎?好吧那你再寫一個測試去include blog_submit_v2.php這檔案好了。(寫那種測試的時間真的值得嗎?直接開瀏覽器檢查不好嗎?)
但是我同意blog_submit_v2_test.php這命名有問題。就改成add_blog_to_db_test.php吧!

Q6: 等一下!我發現你的範例跟測試都是個笑話!你把add_blog_to_db函式最重要的任務「資料庫存取」省略了!回傳那array根本搞笑,真的存取資料庫是要怎麼測試?你倒是說說看。

老話一句,發揮創意吧。把mysql函式寫得不成功便成仁就是一種方式

if (mysqli_query($conn, $sql)) {
    return [$title, $content];
} else {
    die("Error: " . $sql . "\n" . mysqli_error($conn));
}

或是最後回傳true之類的,讓測試程式能夠檢查就好。發揮創意吧,發揮創意吧。

讀到這邊,相信你已經發現一件事了:最傳統的一個php檔案寫到底的寫法,非常難以測試。就算測了心裡也很不踏實。聽過有人說「寫測試不難,難的是寫出能被測試的程式」嗎?

以後不要再寫像那樣老舊的php code了,建議把重要的business logic全部包成獨立的function,然後再分別測試function比較好!

除此之外,前面只用到==運算子和strpos函式而已,如果想寫出更多種的測試,光是內建函式就有這些可以用喔
is_​array
is_​bool
is_​callable
is_​double
is_​float
is_​int
is_​integer
is_​long
is_​null
is_​numeric
is_​object
is_​real
is_​resource
is_​scalar
is_​string
很酷吧!記得,重點是發揮創意,寫出能被測試的code、從最基本的測試開始做起!

有測試總比沒有好!

Q7: 我發現盲點了。寫測試說是要節省開發時間,但是光跑測試本身我就要輸入

php simple_add_test.php
php simple_add_better_test.php
php blog_submit_test.php
php blog_submit_v2_test.php

這東西四次!想想看,每次增加新功能或是修改舊功能,為了確保全部程式都正確,我要重新輸入這東西四次!光是打字我就飽了,哪有更省時間?

這個問題很好解決,寫一個測試主程式就好啦


以後只要在terminal輸入

php test_suite.php

會看到結果顯示

simple_add_test passed
simple_add_test passed
simple_add_test passed
simple_add_test passed
simple_add_test passed
blog_submit_test for title passed
blog_submit_test for content passed
blog_submit_test for title passed
blog_submit_test for content passed

光是這麼無聊的範例,看到9個測試都通過就很有成就感了,正式上線的application那種幾百幾千個測試通過的感覺更充實、更對系統穩定性有把握。

每次增加新功能或是修改舊功能,為了確保全部程式都正確,記得都跑一次php test_suite.php喔!再也不需要打開瀏覽器點來點去半天了!

結語

這篇文章是對測試基本觀念的說明,實際開發時使用PHPUnit或是其他測試框架,在做的事情其實跟本文差不多。

如果認為PHPUnit太複雜的話,可以先從比較簡單的測試工具開始,像是SimpleTest就不錯。

至於所謂嚴謹的測試技巧,以及物件導向的開發測試手法,我們下次再談吧!


社群feedback(last updated: 2015-10-14)

本文在PHP台灣FB社群引起一些討論與檢討,

推薦大家一併延伸閱讀

討論串一

討論串二

這樣寫測試錯了嗎?


如果您喜歡我的文章,可以在這裡訂閱。我有新想法的時候,很樂意跟你分享。

本文的程式碼可以在這裡取得。

(Photo via Masahito Oku, CC Licensed.)