用phpUnit入門TDD


用phpunit實戰TDD系列

從一個銀行賬戶開始

假設你已經 安裝了phpunit.

我們從一個簡單的銀行賬戶的例子開始了解TDD(Test-Driven-Development)的思想。

在工程目錄下建立兩個目錄, srctest,在src下建立文件 BankAccount.php,在test目錄下建立文件BankAccountTest.php

按照TDD的思想,我們先寫測試,再寫生產代碼,因此BankAccount.php留空,我們先寫BankAccountTest.php

<?php
class BankAccountTest extends PHPUnit_Framework_TestCase
{
}
?>

現在我們運行一下,看看結果。運行phpunit的命令行如下:

phpunit --bootstrap src/BankAccount.php test/BankAccountTest.php

--bootstrap src/BankAccount.php是說在運行測試代碼之前先加載 src/BankAccount.php,要運行的測試代碼是test/BankAccountTest.php

如果不指定具體的測試文件,只給出目錄,phpunit則會運行目錄下所有文件名匹配 *Test.php 的文件。因為test目錄下只有BankAccountTest.php一個文件,所以執行

phpunit --bootstrap src/BankAccount.php test

會得到一樣的結果。

There was 1 failure:

1) Warning
No tests found in class "BankAccountTest".

FAILURES!
Tests: 1, Assertions: 0, Failures: 1.

一個警告錯誤,因為沒有任何測試。

賬戶實例化

下面我們添加一個測試。注意,TDD是一種設計方法,可以幫助你自底向上地設計一個模塊的功能。我們寫測試的時候,要從用戶的角度出發。如果用戶使用我們的BankAccount類,他首先做什么事呢?一定是新建一個BankAccount的實例。那么我們第一個測試就是對於 實例化 的測試。

public function testNewAccount(){
    $account1 = new BankAccount();
}

運行phpunit,意料之中地失敗。

PHP Fatal error:  Class 'BankAccount' not found in /home/wuchen/projects/jolly-code-snippets/php/phpunit/test/BankAccountTest.php on line 5

沒有發現BankAccount類的定義,下面我們就要寫生產代碼。使測試通過。在src/BankAccount.php(后面稱之為源文件)中輸入以下內容:

<?php
class BankAccount {
}
?>

運行phpunit,測試通過。

OK (1 test, 0 assertions)

接下來,我們要增加測試,使得測試失敗。如果新建一個賬戶,賬戶的余額應該是0。於是我們添加了一個assert語句:

public function testNewAccount(){
    $account1 = new BankAccount();
    $this->assertEquals(0, $account1->value());
}

注意value()BankAccount的一個成員函數,當然這個函數還沒有定義,作為使用者我們希望BankAccount提供這個函數。

運行phpunit,結果如下:

PHP Fatal error:  Call to undefined method BankAccount::value() in /home/wuchen/projects/jolly-code-snippets/php/phpunit/test/BankAccountTest.php on line 6

結果告訴我們BankAccount並沒有value()這個成員函數。添加生產代碼:

class BankAccount {
    public function value(){
        return 0;
    }
}

為什么要讓value()直接返回0,因為測試代碼中希望value()返回0。TDD的原則就是不寫多余的生產代碼,剛好讓測試通過即可。

賬戶的存取

運行phpunit通過后,我們先假設BankAccount的實例化已經滿足要求了,接下來,用戶希望怎么使用BankAccount呢?一定希望往里面存錢,嗯,希望BankAccount有一個deposit函數,通過調用該函數,可以增加賬戶余額。於是我們增加下一個測試。

public function testDeposit(){
    $account = new BankAccount();
    $account->deposit(10);
    $this->assertEquals(10, $account->value());
}

賬戶初始余額是0,我們往里面存10元,其賬戶余額當然應該為10。運行phpunit,測試失敗,因為deposit函數還沒有定義:

.PHP Fatal error:  Call to undefined method BankAccount::deposit() in /home/wuchen/projects/jolly-code-snippets/php/phpunit/test/BankAccountTest.php on line 11

接下來在源文件中增加deposit函數:

public function deposit($ammount) {
}

再運行phpunit,得如下結果:

1) BankAccountTest::testDeposit
Failed asserting that 0 matches expected 10.

這時因為我們在deposit函數中並沒有操作賬戶余額,余額初始值為0,deposit函數執行之后依然是0,不是用戶期望的行為。我們應該往余額上增加用戶存入的數值。

為了操作余額,余額應該是BankAccount的一個成員變量。這個變量不允許外界隨便更改,因此定義為私有變量。下面我們在生產代碼中加入私有變量$value,那么value函數應該返回$value的值。

class BankAccount {
    private $value;
    
    public function value(){
        return $this->value;
    }

    public function deposit($ammount) {
        $this->value = 10;
    }
}

運行 phpunit,測試通過。接下來,我們想,用戶還需要什么?對,取錢。當取錢時,賬戶余額要扣除這個值。如果給 deposit函數傳遞負數,就相當於取錢了。
於是我們在測試代碼的testDeposit函數中增加兩行代碼。

$account->deposit(-5);
$this->assertEquals(5, $account->value());

再運行 phpunit,測試失敗了。

1) BankAccountTest::testDeposit
Failed asserting that 10 matches expected 5.

這時因為在生產代碼中我們簡單地把$value設成10的結果。改進生產代碼。

public function deposit($ammount) {
    $this->value += $ammount;
}

再運行phpunit,測試通過。

新的構造函數

接下來,我想到,用戶可能需要一個不同的構造函數,當創建BankAccount對象時,可以傳入一個值作為賬戶余額。於是我們在testNewAccount增加這種實例化的測試。

public function testNewAccount(){
    $account1 = new BankAccount();
    $this->assertEquals(0, $account1->value());
    $account2 = new BankAccount(10);
    $this->assertEquals(10, $account2->value());
}

運行phpunit,結果為:

1) BankAccountTest::testNewAccount
Failed asserting that null matches expected 10.

這時因為BankAccount沒有帶參數的構造函數,因此new BankAccount(10)會返回一個空對象,空對象的value()函數自然返回的也是null。為了通過測試,我們在生產代碼中增加帶參數的構造函數。

public function __construct($n){
    $this->value = $n;
}

再運行測試:

1) BankAccountTest::testNewAccount
Missing argument 1 for BankAccount::__construct(), called in /home/wuchen/projects/jolly-code-snippets/php/phpunit/test/BankAccountTest.php on line 5 and defined

/home/wuchen/projects/jolly-code-snippets/php/phpunit/src/BankAccount.php:5
/home/wuchen/projects/jolly-code-snippets/php/phpunit/test/BankAccountTest.php:5

2) BankAccountTest::testDeposit
Missing argument 1 for BankAccount::__construct(), called in /home/wuchen/projects/jolly-code-snippets/php/phpunit/test/BankAccountTest.php on line 12 and defined

/home/wuchen/projects/jolly-code-snippets/php/phpunit/src/BankAccount.php:5
/home/wuchen/projects/jolly-code-snippets/php/phpunit/test/BankAccountTest.php:12

兩個調用new BankAccount()的地方都報告了錯誤,增加了帶參數的構造函數,不帶參數的構造函數又不行了。從c++/java過渡來的同學馬上想到增加一個默認的構造函數:

public function __construct() {
    $this->value = 0;
}

但這樣是不行的,因為php不支持函數重載,所以不能有多個構造函數。

怎么辦?對了,我們可以為參數增加默認值。修改構造函數為:

public function __construct($n = 0){
    $this->value = $n;
}

這樣調用 new BankAccount()時,相當於傳遞了0給構造函數,滿足了需求。
phpunit運行以下,測試通過。

這時,我們的生產代碼為:

<?php
class BankAccount {
    private $value;             // default to 0

    public function __construct($n = 0){
        $this->value = $n;
    }
    
    public function value(){
        return $this->value;
    }

    public function deposit($ammount) {
        $this->value += $ammount;
    }
}
?>

總結

雖然我們的代碼並不多,但是每一步都寫得很有信心,這就是TDD的好處。即使你對php的語法不是很有把握(比如我),也可以對自己的代碼很有信心。

用TDD的方式寫程序的另一個好處,就是編碼之前不需要對單個模塊進行仔細的設計,可以在寫測試的時候進行設計。這樣開發出來的模塊既可以滿足用戶需要,也不會冗余。

后面將會介紹 phpunit 的更多用法。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM