標籤彙整: tesing

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.)

你替controllers寫測試嗎?

前陣子看到替controllers寫測試的文章。摸了一陣子還是沒掌握。

跑去Reddit發問,得到好多回應。

因為獲益良多,所以在此翻譯討論串內容,和各位分享。

你替controllers寫測試嗎?

嗨!
我最近在學Domain Driven Design跟幾個測試技巧。
我替repositories、services、entities、factories寫各種測試來確保程式品質。
因為網站越來越依賴前端技術跟AJAX,我覺得也替controllers寫測試比較好。
但我又覺得測試controllers根本沒從controllers類別裡面initiate或是call任何東西。
只是寫一個測試去模擬對應controller action的內容而已。
這也能叫測試?這能帶來多少好處?
你們替controllers寫測試嗎?
誰能指點迷津?
感激不盡!

dohpaz42

軟體測試是一個超複雜大怪獸。每個人測試的方法都差很多。電腦理論都這樣的。
先簡單回答你的問題:測試所有東西。
完整回答比較複雜。我試著整理出來,請見諒。
測試有分很多種:unit tests,integration test,regression tests,feature tests,等等。根據測試的對象不同,你會使用不同的測試。
舉例來說,當你寫一個model,基本上你會用到unit test。你會替每個method可能走過的每條道路都寫測試。
如果你對這觀念不熟,就這樣想:若你的method會製造X種副作用,那你就得替這X種副作用寫測試。正確執行、失敗執行的情況都算。
unit test的定義就是測試application中的一個特定單位。如果測試碰到其他models或程式碼,你就mock那些物件(利用dependency injection)來忽略它們。若有個database model會寫入資料到database,我需要去管database layer是否產出正確的output嗎?大概不需要。你只需要注意正在測試的部份系統(System Under Test, SUT)會針對database的output正確反應。這代表你應該要對所有會影響系統運作的database output寫測試。只需要pass-through、proxy(或是任何不會造成副作用的方式)這樣的簡單方法,就能有效忽略那些code的可能走法。譬如說:

function fooProxy() {
    return $this->getDatabase()->query('SELECT * FROM foo');
}

這個method只是把application中另一層layer的結果丟回去,而那層layer你應該早就測過了。所以你不需要替fooProxy寫測試。

很好,感謝你保持耐心。正式回答前,我想先釐清這些。

controllers通常會與application中的多個部份一起合作。它們透過front controller、routing機制來運作,並用上models、views。很少會有專屬於controller的logic(這不代表controllers不需要測試)。要是有的話,那就測吧。測試在不同狀況下,他們與其他code的互動是否一如預期。這需要integration tests才行。

一個integration test不怎麼在乎系統運作的細節。它只在乎:如果XYZ放進去,那麼結果應該要是ZYX。拿REST API舉例:如果我丟一個GET request到/resource/a,我應該得到一個200 response,也許再加上body content。這樣算通過測試。如果我把幾個變數跟request一起丟出去,結果得到400 response,那就沒通過測試。不在乎系統是否碰到資料庫還是檔案什麼的,只在乎response是不是200。

正式回答你的問題:對,所有東西都應該測試。你會寫很多不同的test。這可能很瑣碎,甚至很無聊。但是當所有的tests都通過的時候,你終究會理解價值何在。

whtevn

我們會替controllers寫測試。但是integration tests,不是unit tests。我們會測所有api endpoints,然後記下他們在特定input下的各種response。我們也會測資料庫是否正確更新。這樣算是驗證了整個application。
這跟unit test要求徹底獨立的概念不同。從TDD的觀點,先寫unit tests是很合理。但是先寫integration tests更棒。它能協助你弄清楚功能,又不需要像unit test那樣有個逐漸遞增的feedback loop。

我們公司強調integration tests而非unit test。我們規定每個endpoint都必須要有integration test,而unit tests由工程師自行決定。確認特定的method運作正常當然重要。但我個人認為,確認所有API endpoint都符合規格,更重要。

我寫了一個integration test套件。我放在github上,但目前只有幾個範例有寫說明。

https://github.com/whtevn/routest/blob/master/sample/tests/succeed/routest-test.js

也放在npm上了。它不是外掛,只是一個免費工具。好像也算外掛。隨便啦。

https://www.npmjs.com/package/routest

mrargh

我們通常對models寫unit test,然後用web driver integration tests 測controllers – 這樣就都測到了。

philsturgeon

沒事幹才寫吧…但我總是有事幹。
我對libraries,models之類的寫unit test,然後用integration test測其餘部份。
對controllers寫unit test不是壞事,但不需要優先考慮。

WishCow

我不寫。我盡量在controllers內少放logic,把事情都交給services跟models。替controllers寫測試只是再模擬一次他們的互動而已。那樣是可以驗證參數與結果的關係,但對我來說太花功夫了,不划算。

chadcf

如果是API application我才替controllers寫測試。那些算functional tests。web介面的話,我會讓controllers內容少到不需要寫tests。大原則是讓controller action內容少到10行以內,只讓它建立物件、呼叫物件方法,然後回傳response。