測試 是軟件開發中一個必不可少的環節。不管我們是否有意識到,其實我們經常都在做測試工作。比如最原始的在代碼中插入 echo $variable 查看變量值,簡單粗暴而有效。當然,PHP 也會有 XDebug 這樣的工具,可以做到單步調試,不過要在服務器上安裝再配合好本地一個 IDE 和瀏覽器一起運作,始終不是那麼方便。

我們實現了一些複雜邏輯後,總會要輸入一些數據,來確認程序是否能給出預期的結果,頁面是否正常展示。更高級的開發者會寫一些代碼來自動完成這個測試過程,這樣一來每當我們需要測試一些東西的時候,只需要調用測試腳本,就能直接獲知結果了。這就是所謂的 自動測試

軟件測試已經發展為一個獨立的工種,黑盒、白盒、單元測試、功能測試……流程都比較規範化了。隨着軟件開發過程的不斷演變,各種方法論層出不窮,類似的名詞包括:敏捷開發、持續集成、XP極限編程、結對編程、TDD……一般談這些概念的時候都會被大牛黑出翔來,比如酷殼博主 @左耳朵耗子 發表過這樣一條微博:

不會寫程序的人來搞什麼軟件開發諮詢,SQA,流程設計,軟件項目管理,全是扯蛋。所以,程序員應該要像 Linus 一樣自信的對這些人說:“Talk is cheap, show me the code.”

習總書記教育我們:

空談誤國。

我想聊一下 TDD。我必須聲明我並非想借技術名詞炒作概念。自從上次一篇 關於MVC的討論 被新浪 SAE 的微博大號轉發後,引來口水無數,各種被噴。其實我也只是初學者,談談自己對編程的理解,希望能嘗試一下不同的實踐方式而已,最終想法和所有開發者一樣,都是要提高代碼質量和生產效率。

TDD 簡介

TDD 測試驅動開發,這個概念來自於敏捷中的極限編程(XP),其中的主要觀點即:測試對項目成敗起決定作用。測試應該自動化,經常測,並儘可能在功能代碼還沒實現之前就寫好測試代碼。在 XP 理論中,架構設計應該是恆定的不可隨意改動的,而代碼實現卻會不斷迭代和重構。

XP-feedback

所謂的 測試驅動開發 的開發周期:

  1. 創建一個涵蓋要實現的特性的新的測試。測試預計將在第一次執行的時候失敗,因為特性尚未實現。
  2. 執行所有測試,確保這個新的測試是失敗的。
  3. 編寫代碼來使得測試通過。
  4. 執行所有測試,確保所有測試通過。
  5. 重構新編寫的代碼並確保這些測試仍然能夠通過。

重複步驟1至5推進整體功能的實現。

PHP 測試工具與實踐

我所使用的 Yii Framework 支持單元測試和功能測試,這裡會用到 PHPUnitSelenium Remote Control 這兩個測試工具。

PHPUnit 很多人都在用,通過很多斷言語句(比如 assertTrue, assertEquals)來檢查驗證目標代碼的行為。例如下面這個例子,用於測試博客評論功能。

// CDbTestCase 由 Yii 框架繼承自 PHPUnit 中的 PHPUnit_Framework_TestCase
class CommentTest extends CDbTestCase 
{
    public function testApprove()
    {
        // 插入一條新的評論
        $comment=new Comment;
        $comment->setAttributes(array(
            'content'=>'comment 1',
            'status'=>Comment::STATUS_PENDING,
            'createTime'=>time(),
            'author'=>'me',
            'email'=>'[email protected]',
            'postId'=>$this->posts['sample1']['id'],
        ),false);
        $this->assertTrue($comment->save(false));
 
        // 驗證評論處於 pending 狀態
        $comment=Comment::model()->findByPk($comment->id);
        $this->assertTrue($comment instanceof Comment);
        $this->assertEquals(Comment::STATUS_PENDING,$comment->status);
 
        // 調用 approve() 方法後,再驗證評論狀態為“已通過”
        $comment->approve();
        $this->assertEquals(Comment::STATUS_APPROVED,$comment->status);
        $comment=Comment::model()->findByPk($comment->id);
        $this->assertEquals(Comment::STATUS_APPROVED,$comment->status);
    }
 
    ......
}

Selenium 這個工具的介紹看上去感覺很強大,可以通過模擬瀏覽器訪問 Web 頁面的方式,來進行 UI 測試。用法和上面類似,

// WebTestCase 由 Yii 框架繼承自 PHPUnit_Extensions_SeleniumTestCase
class PostTest extends WebTestCase
{ 
    public function testShow()
    {
        $this->open('post/1');
        // 驗證測試頁面文章標題
        $this->assertTextPresent($this->posts['sample1']['title']);
        // 驗證評論表單
        $this->assertTextPresent('Leave a Comment');
    }
 
    ......
}

我的觀點

TDD 並非教條。敏捷專家或是各種諮詢師在宣揚敏捷,很多團隊卻被整得很苦逼,就是因為他們像宗教一樣地信奉教條而不知變通。比如,TDD 就等同於 Unit Test 嗎?我們真的一定要在寫代碼前就寫好測試嗎?這些問題都是值得商榷的,也是爭論很多的。陳皓曾經談到 TDD 的幾個弊端——

  • 1)TDD 可能會讓程序員敷衍了事,以為 test case 沒有錯就正確了。
  • 2)TDD 可能會讓你忽略了軟件設計和架構以及程序的擴展性和重用性。

令人沮喪的是,現在說這些有點紙上談兵,因為我看到很多 PHP 程序員壓根就沒有寫測試,自己手動查看頁面沒問題後就上線了,然後陷入無盡的 bug report 循環。這屬於 各種流行的編程風格 中的撞大運編程。我覺得程序員編寫單元測試還是很有必要的,雖然會增加工作量,但以後的代碼維護會輕鬆很多。在 StackOverflow 上有人問 單元測試要測到什麼程度,“老闆為我的代碼付報酬,而不是測試”。最值得借鑒的答案則強調並非面面俱到,只是特別注意邊界情況,那些會讓團隊容易出錯的代碼。

眾所周知,Facebook 沒有專職的測試工程師,這是他們在質量控制中引以為豪並備受矚目的一點。所有的測試都是程序員自己完成,對自己的代碼質量負責,也就是 Eat your own dog food。每個程序員都是多面手,從最開始的一個 idea,到自己分析需求、架構,開發原型,自我管理版本控制,自己去測自己寫的代碼,自己去運維去部署。這樣才會對整個開發的過程有深刻的認識,親身體會某個環節的痛苦才會有想要改進的動力。

總結

每一個 PHP 程序員上輩子都是折翼的前端、美工、系統管理員和測試工程師……