我任職的翻譯公司需要線上報價功能。
其中,上傳文件到專案的功能,會同時計算字數、備份檔案到遠端。
程式碼大概是這樣:
upload($file); $reader = new Reader(); $words = $reader->count($file); $document = new Document(); $document->remote_file_id = $id; $document->words = $words; $document->save(); } }
由於這是個核心功能,我希望多寫幾個測試確保它的運作正常。
這樣的function該怎麼測試呢?
我是用Laravel + PHPUnit開發。首先,寫個基本測試,確保function沒有打錯變數名字、語法、不會有莫名exception噴出來:
addDocument($file); $this->assertGreaterThan(0, $project->words); $this->assertNotNull($project->remote_file_id); } }
問題來了,這個測試跑得非常慢。因為:
- 它真的把檔案上傳到資料備份主機
- 它真的用Reader數字數
我只想確認基本邏輯正常,並不在乎備份、計算字數功能是否正常。(那兩個應該另外測試。)
該怎麼辦呢?
是時候叫替身出來幫忙了!
Test Double
Test Double,我翻譯為「測試替身」,是Gerard Meszaros在2007年提出來的名詞。
它是軟體測試的常見手法之一,會做出類別的替身供測試使用。
又可細分為Fake, Mock, Stub, Dummy… 等等。
我們先不管差別何在,想辦法解決眼前的狗屁就好!
首先,我們得將原本的function改寫,把dependency改用參數傳入:
class Project { public function addDocument($file, $remote, $reader){ //.... //.... // $remote = new RemoteBackup(); $id = $remote->upload($file); // $reader = new Reader(); $words = $reader->count($file); $document = new Document(); $document->remote_file_id = $id; $document->words = $words; $document->save(); } }
呼叫這個function的相關code,記得改寫成:
$project->addDocument($file, new RemoteBackup(), new Reader());
重點來了,測試怎麼寫?怎麼做出測試替身?
我想要一個不管收到什麼,一律回傳5555當作假id的$remote替身;
還要一個不管收到什麼,都說它計算出6666個字數的$reader替身。
PHPUnit本身就支援Test Double,只要這樣即可:
class ProjectTest extends TestCase { public function testAddDocument() { // 先設法做出測試用的$project與$file // ... $remote = $this->getMockBuilder('RemoteBackup') ->getMock(); $remote->method('upload') ->willReturn(5555); $reader = $this->getMockBuilder('Reader') ->getMock(); $reader->method('count') ->willReturn(6666); $project->addDocument($file, $remote, $reader); $this->assertGreaterThan(0, $project->words); $this->assertNotNull($project->remote_file_id); } }
搞定!
成功避開遠端備份與計算字數的dependency!
測試完基本邏輯,接著可以寫各種不同的測試,去檢驗你想確認的部份,
全都不會真的上傳檔案到備份主機與計算字數!
利用test double的手法,你可以避開一大堆的dependency,測試到你原本以為不可能測試的地方。
很神奇吧!
Q&A
Q1: 把物件當dependency傳入真的好帥喔,這招叫什麼?
這招只是dependency injection的一種。
為了讓code能夠被測試,這招很常用到。
Q2: function傳入檔案$file又傳入$remote跟$reader。我怎麼覺得醜到爆啊?
這樣寫確實很醜。
通常物件dependency是先在constructor傳入,再從function內拿來用:
// $id = $remote->upload($file); $id = $this->remote->upload($file); // $words = $reader->count($file); $words = $this->reader->count($file);
你常常看到的這種pattern就是在幹這件事:
$stuff = new GoodClass(new ClassOne(), new ClassTwo());
public function __constructor($dependencyOne, $dependencyTwo) { $this->dependencyOne = $dependencyOne; $this->dependencyTwo = $dependencyTwo; }
但是我懶得改寫所有相關code,而且Laravel的Eloquent建構式又很難搞定物件dependency。
學到精神就好,寫法隨機應變。
Q3: 我還滿好奇,你提到的翻譯公司網站長怎樣?
http://wordcorp.net/
(Photo via AlmaArte Photography, CC licensed. )