標籤彙整: testing

quality

開發網站時,寫測試的3個簡單方法

許多想要學寫測試的朋友,常常不知道怎麼開始。

其實寫測試比大多數人以為的還要簡單。

就算是小型的網站、簡單的購物網站,也可以利用軟體測試加強軟體品質。

今天跟大家分享3個簡單的導入測試開發方法。

(本文使用 Laravel + PHPUnit 示範,但相關概念通用在任何語言/工具上。)

第一招、controller轉移到entity

開發網站時,大部份商業邏輯都在對entity(例:產品、訂單、使用者…etc)操作。

相關的code通常會散在controller裡面。

舉例來說,假設我們在寫一個購物網站,那麼把產品加進訂單、更新訂單金額的code會像這樣:

寫測試的目的是為了增加開發者的信心。

像上面這樣的code,有些人會覺得打開瀏覽器按個幾次,看看最後訂單金額是否正確就好。

反覆按個幾次,就算不寫測試,也對程式品質很有把握、相信它沒有bug。

但如果程式再複雜一點呢?例如增加不同運費的邏輯:

有些人依然覺得這不複雜,在瀏覽器上多操作幾次,然後看看金額、運費是否都正確即可。

但如果你像我一樣,採取比較嚴謹的立場,預設自己寫的所有code都有bug,那麼可以先把code從controller搬到entity:

然後可以這樣寫測試:

像這樣的測試寫好之後,就不用每次改動一點商業邏輯,就打開瀏覽器重複做一堆人工測試了。

如果運費的算法更複雜,根據訂單金額而有4、5種運費金額的話,

光是要測試運費邏輯,就要人工去操作下單流程好幾次。

這種時候,寫測試方便多了。

這種開發流程,我稱為「controller轉移到entity」。

一開始邏輯單純的時候,直接寫在controller即可。

複雜起來後,再把code轉移進entity,然後補寫測試即可。

第二招、轉移到service

接續上面的例子,來看看更複雜的例子。

現在產品賣掉的時候,產品本身的庫存數量要減一。

以原本的寫法來說,需要在entity加上這一行:

然後在測試內多加幾行:

看起來沒多大問題。邏輯放在Order內還算合理,該測的也都測到了。

但如果邏輯再複雜一點呢?

假設有人氣指數的功能,在賣出去的同時,商品的人氣指數要加上10分:

這行code還是放在Order類別內嗎?

為什麼一定是$order->addProduct($product),而不能是$product->addToOrder($order)呢?

隨著Product相關邏輯變多,商品加入訂單的功能不再完全像是訂單的事,也很像是Product的事。

像這種不知道要放在Order內還是Product內的時候,可以獨立出來成Service類別:

測試的時候,改寫成這樣即可:

跟第一招一樣,可以在程式變大之後、有必要的時候才獨立成service類別並補寫測試。

轉移的成本是很低的,幾乎只是把code從這邊剪下貼上到那邊而已。

第三招、轉移到專門的POPO

POPO(Plain Old PHP Object)是指單純的全手寫類別,不去繼承Eloquent之類的華麗類別。

雖然是最基本的OOP用法,卻常常被人們所忽略。

現在假設訂單進一步變複雜,運費的邏輯會根據消費者的所在地區而有不同:

再加上金額的變化,光是運費本身的計算就很複雜、需要寫很多測試才安心。

與其在entity或service內計算,不如弄一台運費計算機出來,會更分工明確:

接著就能替計算機寫許多測試,測到安心為止:

本來的service就可以改寫成,吃計算機當作參數:

POPO常常被人們忘記,因為太習慣把code全都寫進entity或是controller裡面。

其實,在程式複雜起來之後,最好把意義上獨立的部份各自獨立成類別。

然後分別替這些POPO寫各式各樣的測試,就能大幅增加軟體的品質與穩定性了。

結論

寫測試的目的純粹是為了增加安全感,讓工程師晚上安心入睡而已。

不需要撲天蓋地般地狂寫測試,適量的測試就非常足夠。

以中大型專案來說,寫測試可以幫助省下「非常大量的時間」。

實務上,在一開始商業邏輯不多的時候,寫在controller通常沒什麼問題,把幾個變數存進資料庫而已,不太需要寫測試。

等到邏輯慢慢變複雜,發現不寫測試會不敢上線的時候,再拉出來並且補寫測試即可。

希望你之後的專案開發,可以試試看寫測試帶來的好處與美妙!


歡迎訂閱轉個彎日誌的粉絲專頁,我很樂意和你分享各種心得。

(Photo via Elizabeth Hahn, CC licensed.)

sun

先寫單元測試的12個好處!

「寫測試的好處是什麼?」

這個問題非常難回答。通常只能得到「只有寫了才知道」這種含糊不清的答案。

我最近找到Tim King在2006年的文章,非常完整的回答這個問題。

看過之後覺得獲益良多,翻譯出來和大家分享。


先寫單元測試的12個好處!

為什麼工程師會討厭寫測試?為什麼他們會拒絕先寫測試?不用解釋了,那些藉口我全聽過。我知道真正的原因為何。

大部份的工程師根本沒認真試過測試先行。不然就是當下的環境不支持他們寫,導致他們不知道自己在幹嘛。前者的情況居多。最後他們就找藉口:「我們沒時間寫單元測試」、「單元測試不能完全保證程式碼品質」。這是在替自己的悲慘找理由,而不是真的覺得不寫比較好。有趣的是,當這種人直接在你面前開發,你常會看到他開發得很不順,進度常常在開倒車。目睹這種事滿有趣的。但他依舊不會收回原本的看法、依舊不願意先寫測試。這種堅持根本有害。

Kent Beck在《Test Driven Development By Example》書中提到,測試先行有3個步驟:

1. Red:寫個能表達你打算如何使用那段code的測試,還有你期待它做什麼。這個測試會失敗。很多介面會用紅色訊息來表示它。
2. Green:寫出足夠的code來讓那個測試成功,但別多寫。如果你想寫更多code,像是檢查某些錯誤的話,那就先另寫一個測試表達它。當下只要寫剛好夠的code去通過測試即可。
3. Refactor:把多餘的code清理一下,然後改善整體設計。之後再跑一次測試,確保沒弄壞什麼地方。

重複這些步驟直到功能做完。這個流程超級簡單。為什麼工程師會畏懼它?因為這會逼他們從根本上改變開發習慣。

我們認為自己在寫code前不需要先去思考,這些code到底要做什麼。

你如何解決一個軟體問題?學校是怎麼教的呢?第一步怎麼走?你大概只想著如何解決問題本身。你心想:「我要寫哪些code來實作出解決方案?」其實你不應該先想「我要寫哪些code」,你要先想「我要怎樣才能確定問題已經被解決了?」

我們直覺認為一段code的正確與否,只要執行一次就知道了,超明顯的,何必寫那種根本廢話的測試?就是這種這種根深蒂固的想法,導致大部份的人改不了開發習慣。

成功跨過那道鴻溝的人,可以感受到下列幾項好處。我全部體會過。不用完全信我,你自己試試看就知道了。

1. 單元測試保證你的code真的能動

這會讓bug減少。當然,單元測試不能取代系統測試跟驗收測試。但單元測試能補足它們的短處。

2. 你會得到一組底層的regression-test suite

這讓你隨時可以回頭去檢查有否哪邊壞掉、bug在哪。很多團隊會每天把整組測試跑一次。這讓你在把程式交給品管部門之前,可以很輕鬆的把bug抓出來。

3. 讓你改善系統設計的時候,不怕弄壞系統

其實就是測試先行3步驟的第3步。通常測試先行寫出來的code不太需要重構。我看過很多超糟糕的系統,就像精神病患一樣,根本無法搞定。如果有準備好單元測試,你就可以對系統裡面最難搞的部份做出有效的重構。

4. 寫測試會讓coding更好玩

你會先搞懂自己的code要做什麼。然後再讓它完成任務。就算系統還沒全做完,你還是能看到code真的動起來,而且真的沒出錯。你會得到一種「我完成了!」的感覺。每分鐘都會不斷感受到喔。只要試試測試先行,你就會整個人high起來、對自己的作品感到驕傲、被激勵去完成更多事情。

5. 它們可靠地展現目前進度

你不用為了等整個系統組裝起來而多等一個月。在系統完成之前你就能展示進度了。不但能說自己寫了code,還能真的跑給別人看。傳統開發有件事搞錯了。「完成」不等於你寫了code然後丟出去。「完成」應該是你的code能在系統裡跑,而且沒bug。寫測試會讓你更接近這點。

6. 單元測試是一種使用範例

我們都碰過那種不知道怎麼用的library。通常我們會先去找範例程式碼。使用範例可算是一種文件。但公司內部的code通常不會有範例可看。所以只好慢慢試、在系統內東找西找了。因為那個同事可能根本離職了,想問他都沒辦法。單元測試可以當作一種文件。當你不知道Foo類別怎麼用,去看一下單元測試怎麼寫的即可。

7. 測試先行會強迫你寫code前先做規劃

先寫測試會逼你在動手開發前把必須完成的事和整體設計想過一遍。不但讓你更專注,還能讓設計更漂亮。

8. 先寫測試能減少bug的成本

越早發現bug越容易修。之後出現的bug通常是改了好幾個地方才出現的,
導致很難抓出哪裡導致了bug。一開始先找出bug在哪,然後要重新回想這段code是怎麼寫的,因為可能是幾個月前寫的。最後才終於弄懂,搞出一套解法。只要能減少抓bug以及修好bug的時間,幾乎都算大賺。如果在成品交給品管部門或是顧客之前,我們只花幾天就找出bug,通常算是很幸運。那幾花幾分鐘就找出bug呢?測試先行就能做到這點。

9. 它比代碼檢查的效果好

有人說事前代碼檢查比事後測試系統更好,因為成本比較低。在系統完成之後才測試系統,要修好bug可說是麻煩多了。越早發現bug,就越簡單、越便宜、越好搞定。代碼檢查的好處就在這:只花幾天就能抓出bug,不需要等幾個月。但是測試先行成本更低。只要幾分鐘就抓出bug,連幾天都不用。

10. 幾乎解決了「開發者瓶頸」(coder’s block)

不知道下一行寫什麼嗎?就跟「作家瓶頸」(writer’s block)一樣,開發者瓶頸很可能是個大問題。測試先行有系統地處理開發上關於結構的部份,讓你能專心在需要創造的部份。你可能會卡在下段code不知道怎麼測、該怎麼通過測試,但你永遠不會因為下一步卡住。通常會有完全相反的結果:你很想在累倒之前休息一下,但因為清楚看到前面的錄了,所以根本不想停下來。

11. 單元測試讓設計更棒

測試一小塊code會強迫你定義清楚那段code負責什麼。如果測起來很簡單,就表示它的責任很明確,cohesion很高。如果一段code能被單元測試,那就表示它很容易就能放進系統之中,就跟它很容易放進測試之中一樣。它跟相關的code只具有loose coupling 。 High cohesion與loose coupling代表了出色、好維護的設計。容易測試的code也很容易維護。

12. 寫測試會讓開發速度更快

不寫單元測試也許會讓速度更快,但無法保證code真的能跑。開發上會花一堆時間在在事後的修bug。測試先行會消除這類的浪費,從一開始就做對、讓bug更好修。

就算好處這麼多,很多工程師還是繼續維持他們的老樣子。如果你在組織裡極度重視流程,你跟他們一部份人會起衝突。我只能祝你好運。記住一件事,人們不會因為一個東西聽起來不錯就買帳。他們只有在極度渴望、超想得到手來品嚐時才會買帳。希望以上幾點可以幫助你說服他們。

不過,如果你是前者,也就是那種頑固的工程師,不在乎好的軟體設計,只在乎堅持己見…。嗯,我覺得你還真可憐。

(Photo via 8#X, CC licensed)

codeigniter-logo

CodeIgniter不是一個現代框架

CodeIgniter在今年4月出了第三版。

我在Facebook、批踢踢至今沒看到有人撰文批評它,我覺得這樣不對。

要跟全世界的PHP工程師競爭,只學習CodeIgniter是不夠的。

CodeIgniter不是一個現代框架。必須有人指出這點。

CodeIgniter 3的釋出

這則新聞第一時間就在Reddit出現:

Codeigniter 3 is out

得分最高的評論如下:

是怎樣的被虐狂才會想在2015年用CodeIgniter來開發專案啊?沒有namespaces,沒有autoloading,沒有composer!我乾脆投河自盡好了。

這則評論看起來很偏激嗎?但它拿到最高評論可不只是因為搞笑。

CodeIgniter不是一個現代框架。它沒跟上PHP的幾個趨勢。

怎樣算是現代PHP框架?

現代框架沒有正式定義,但我認為至少要具備三點:

  • 符合PHP元件推薦規範(PHP Standard Recommendations)
  • 支援Composer套件管理
  • 提供成熟的測試環境

為什麼要有這三點才算是現代框架?

因為它們解決了PHP社群幾年來的三個大問題:

  • 一盤散沙的社群
  • 四散的套件
  • web需要軟體測試

一盤散沙的社群

不像Ruby社群,大家全力貢獻在Rails。

PHP社群四散各地、同一個功能在不同框架被重複開發、

框架間難以使用彼此的元件、不同社群難以使用其他社群開發的套件。

這樣的現象在近年終於獲得改善:各框架派出了幾位代表,大家協商出一套coding convention。

從此有了PHP框架互用性小組,制定了PHP元件推薦規範

以後若想要開發新框架,可以從現有的框架中拿幾個元件來組合著用。

PSR規範中對於namespace有清楚的規範,

而CodeIgniter 3中還是到處充滿CI_Controller、CI_Model這種過時的前綴字(prefix)寫法。

四散的套件

在開發套件的時候,PHP有元老級的PEAR、CodeIgniter的Sparks、

Laravel的Bundles、Zend Framework 2的Modules、CakePHP的Bakery。

這種現象是整個PHP社群的損失:PHP社群如此龐大,但是大家各搞各的,心力無法集中。

所幸,Composer的出現解決了這點。

現在大家都開發Composer套件了,整個PHP社群開始共享彼此的成果。

套件相依性的問題也在Composer一併解決,大幅加速了開發的速度與軟體的品質。

CodeIgniter 3還是沒直接支援Composer。

你還是要寫:

之類的囉唆東西。

web需要軟體測試

隨著web application越來越複雜、架構越來越龐大,

軟體測試開始成為web圈的討論重點。

軟體測試不但協助設計架構,確保軟體品質,還能協助重構。

web社群已經從「測試要寫很多很多」爭吵到「測試過多沒意義,足夠就好」。

DHH在2012年已經開始主張「測試覆蓋率不要追求100%」、

「code和測試的比例超過1比2就是出問題」

Testing like the TSA

他在2014年更進一步論述「測試先行已死」

TDD is dead. Long live testing.

PHP近年總算有了PHPUnit廣泛出現在各框架,大幅改善了測試環境。

而CodeIgniter呢?連像樣的基本測試都做不到。

結論

CodeIgniter的主要contributor,Phil Sturgeon,在2012年就曾經貼文闡述

「CodeIgniter不重寫就做不到的5件事」:

5 Things CodeIgniter Cannot Do (without a rewrite)

原文我就不翻譯了。他指出的五個缺失如下:

  • 自動載入
  • 命名空間
  • 資料庫結構抽象化
  • 單元測試
  • migration機制

綜合以上,我的結論就是:

要跟全世界的PHP工程師競爭,只學習CodeIgniter是不夠的。

CodeIgniter不是一個現代框架。


Q&A

Q1: 我耳聞Composer很久了,但到底怎麼入門呢?

COMPOSER設計原理與基本用法

COMPOSER進階原理:PHP命名空間與PSR-0


Q2: 我暫時不可能離開CodeIgniter,有什麼辦法嘗試軟體測試?

CI 2曾經有人把simpletest整合進去。不知道CI 3做不做得到就是了。

CodeIgniter with SimpleTest


Q3: CodeIgniter也許是有些小缺點,但真有那麼落後嗎?

我在2014年2月曾經寫信詢問Phil Sturgeon意見。他的答覆如下:

Hey Tony,

(上略)

我不會再理CodeIgniter了,它玩完了。試試看Laravel吧,然後看一下這些短片:

https://laracasts.com/

你會學到很多:)


Phil Sturgeon

原文

Hey Tony,

You definitely don’t want to rate yourself based on your own age and ability. I’m 25 and there are lot of people around who are younger and smarter, or older and smarter, and more successful. I rate myself based on my improvements over time, and as long as I’m always learning then I’m always getting better.

I’d ignore CodeIgniter completely, its done. Try getting into Laravel, and watch these screencasts:

https://laracasts.com/

You’ll learn a lot :)


Phil Sturgeon


社群看法

PHP台灣

double

一堆dependency怎麼測試?把替身叫出來。

我任職的翻譯公司需要線上報價功能。

其中,上傳文件到專案的功能,會同時計算字數、備份檔案到遠端。

程式碼大概是這樣:

由於這是個核心功能,我希望多寫幾個測試確保它的運作正常。

這樣的function該怎麼測試呢?

我是用Laravel + PHPUnit開發。首先,寫個基本測試,確保function沒有打錯變數名字、語法、不會有莫名exception噴出來:

問題來了,這個測試跑得非常慢。因為:

  • 它真的把檔案上傳到資料備份主機
  • 它真的用Reader數字數

我只想確認基本邏輯正常,並不在乎備份、計算字數功能是否正常。(那兩個應該另外測試。)

該怎麼辦呢?

是時候叫替身出來幫忙了!

Test Double

Test Double,我翻譯為「測試替身」,是Gerard Meszaros在2007年提出來的名詞。
它是軟體測試的常見手法之一,會做出類別的替身供測試使用。
又可細分為Fake, Mock, Stub, Dummy… 等等。
我們先不管差別何在,想辦法解決眼前的狗屁就好!

首先,我們得將原本的function改寫,把dependency改用參數傳入:

呼叫這個function的相關code,記得改寫成:

重點來了,測試怎麼寫?怎麼做出測試替身?

我想要一個不管收到什麼,一律回傳5555當作假id的$remote替身;
還要一個不管收到什麼,都說它計算出6666個字數的$reader替身。

PHPUnit本身就支援Test Double,只要這樣即可:

搞定!
成功避開遠端備份與計算字數的dependency!

測試完基本邏輯,接著可以寫各種不同的測試,去檢驗你想確認的部份,
全都不會真的上傳檔案到備份主機與計算字數!

利用test double的手法,你可以避開一大堆的dependency,測試到你原本以為不可能測試的地方。

很神奇吧!


Q&A

Q1: 把物件當dependency傳入真的好帥喔,這招叫什麼?

這招只是dependency injection的一種。

為了讓code能夠被測試,這招很常用到。

Q2: function傳入檔案$file又傳入$remote跟$reader。我怎麼覺得醜到爆啊?

這樣寫確實很醜。

通常物件dependency是先在constructor傳入,再從function內拿來用:

你常常看到的這種pattern就是在幹這件事:

但是我懶得改寫所有相關code,而且Laravel的Eloquent建構式又很難搞定物件dependency。

學到精神就好,寫法隨機應變。

Q3: 我還滿好奇,你提到的翻譯公司網站長怎樣?

http://wordcorp.net/

(Photo via AlmaArte Photography, CC licensed. )

軟體測試:我不在新創公司用mocks

為了確保軟體品質,多少會寫點測試。

常會看到文章提倡使用mocking的手法,說是要獨立測試程式的各個部份比較好。

我觀察這個手法很久了,遲遲不願意真正應用。

我總覺得mocking讓測試的code變得很冗長、不好讀。

最近終於發現,其實不少人跟我抱持同樣看法。

這邊舉Testing on the Toilet: Don’t Overuse Mocks一文中提到的例子。

不使用mocks、寫出有相依性的test長這樣:

使用mocking達到測試環境獨立的test長這樣:

不但可讀性下降、還得多花時間撰寫mocks。

在新創公司,我不寫mocks的原因有兩個:

一、新創公司,軟體越快上線越好。花時間寫mocks,太不划算了。
二、既然軟體架構不大,反正任何一個測試fail,整個application都會壞掉。tests無論如何都得全部pass才行。

想來想去,只有在用到第三方library或API、不方便在tests中真正呼叫時,才會想用mocks。

話雖如此,有看法認為應該用interface再加上一層薄layer來解決test第三方library的問題:

Test Smell: Everything is mocked

如果是在開發framework就另當別論了。要確實分別測試各個部份才行。

Laravel的tests

話雖如此,也許更資深、接觸的軟體架構更大之後,我會改變想法也說不定。