Beginning PHPUnit

Post on 17-May-2015

2.801 views 3 download

Tags:

Transcript of Beginning PHPUnit

Beginning PHPUnit從今天起進入測試的世界

關於我

Jace Ju / jaceju / 大澤木小鐵

PHP Smarty 樣版引擎 作者

Plurk: http://www.plurk.com/jaceju 歡迎追我 >////<

接下來的內容都是基本功

不會有太深的專有名詞

今日重點

•如何用 PHPUnit 寫測試

今日重點

•如何用 PHPUnit 寫測試

•基本的 PHPUnit 用法

今日重點

•如何用 PHPUnit 寫測試

•基本的 PHPUnit 用法

•如何搭配測試來做重構

今日重點

•如何用 PHPUnit 寫測試

•基本的 PHPUnit 用法

•如何搭配測試來做重構•如何用測試找出錯誤

今日重點

進入主題

Question 1

你如何測試

你的 Web 應用程式?

瀏覽器打開來

一個步驟一個步驟測試

這叫測試?

這叫測試?

這叫自虐!

Question 2

你認為你交給客戶的程式

都是沒問題的嗎?

任何程式都不可能

完美考慮到

所有使用者的狀況

客戶一定會踩中你沒有想到的地雷民明書房 - 軟體莫非定律一百則

Question 3

你確定每次抓完一個 bug 後

不會生出其他 bugs 嗎?

Bugs 總會在

改了幾行程式碼後來找你

每次改完程式

都一定要從頭測試

這時候我們需要

用程式來寫測試

Example

來個簡單的

計算 1 + 2 + 3 + ... + N = ?

先來看看

範例專案目錄結構

project

├── application

└── library

│ └── Math.php

└── run_test.php

範例專案目錄結構

專案目錄

應用程式目錄

套件目錄

欲測試的類別

測試程式

project

├── application

└── library

│ └── Math.php

└── run_test.php

範例專案目錄結構

專案目錄

應用程式目錄

套件目錄

欲測試的類別

測試程式

project

├── application

└── library

│ └── Math.php

└── run_test.php

範例專案目錄結構

專案目錄

應用程式目錄

套件目錄

欲測試的類別

測試程式

project

├── application

└── library

│ └── Math.php

└── run_test.php

範例專案目錄結構

專案目錄

應用程式目錄

套件目錄

欲測試的類別

測試程式

project

├── application

└── library

│ └── Math.php

└── run_test.php

範例專案目錄結構

專案目錄

應用程式目錄

套件目錄

欲測試的類別

測試程式

從 Math.php 開始

Math.php

Math.php

這裡的類別扮演了命名空間的角色

<?phpclass Math{

}

Math.php

加入 Math::sum 方法

<?phpclass Math{    public static function sum($min, $max)    {        $sum = 0;        for ($i = $min; $i <= $max; $i++) {            $sum += $i;        }        return $sum;    }}

Math.php

// 接續上一頁if (defined('TEST_MODE')) {

}

利用 TEST_MODE 常數來進入測試

Math.php

測試 1 加到 10 的結果

// 接續上一頁if (defined('TEST_MODE')) {    // Test 1    $result = Math::sum(1, 10);    if (55 !== $result) {        echo "Test 1 failed!\n";    } else {        echo "Test 1 OK!\n";    }

}

Math.php

// 接續上一頁if (defined('TEST_MODE')) {    // Test 1    $result = Math::sum(1, 10);    if (55 !== $result) {        echo "Test 1 failed!\n";    } else {        echo "Test 1 OK!\n";    }

    // Test 2    $result = Math::sum(1, 100);    if (5050 !== $result) {        echo "Test 2 failed!\n";    } else {        echo "Test 2 OK!\n";    }}

測試 1 加到 100 的結果

接著看 run_test.php

run_test.php

run_test.php

<?phpdefine('TEST_MODE', true);定義 TEST_MODE

常數

run_test.php

引用欲測試的類別

<?phpdefine('TEST_MODE', true);require_once __DIR__ . '/library/Math.php';

執行測試

執行測試

在命令列執行該指令

# php run_test.php

執行測試# php run_test.phpTest 1 OK!Test 2 OK!測試成功

但是測試不是只有

比對值的相等

是否為某變數類型

但是測試不是只有

比對值的相等

陣列是否包含某值

是否為某變數類型

但是測試不是只有

比對值的相等

但是測試不是只有

比對值的相等

陣列是否包含某值

是否為某變數類型

類別是否有某屬性

陣列是否包含某值

是否為某變數類型

類別是否有某屬性是否有預期的錯誤

但是測試不是只有

比對值的相等

每一種比對

都要寫好多程式

結果用程式寫測試

反而增加了負擔

如果有工具來幫我們

做這些事就好了...

主角終於姍姍來遲

PHPUnitby Sebastian Bergmann

http://phpunit.de

從 JUnit 移植

http://www.junit.org/

屬於 xUnit 家族

http://en.wikipedia.org/wiki/List_of_unit_testing_frameworks

# pear channel-discover pear.symfony-project.com# pear install symfony/YAML# pear channel-discover pear.phpunit.de# pear channel-discover components.ez.no# pear install -o phpunit/phpunit

安裝

改用 PHPUnit 測試

準備專案的測試環境

project

├── application

└── library

└── Math.php

範例專案目錄結構

回到原先的專案目錄

project

├── application

├── library

│ └── Math.php

└── tests

範例專案目錄結構

新增一個測試目錄

project

├── application

├── library

│ └── Math.php

└── tests

├── application

└── library

範例專案目錄結構

建立與專案目錄下一模一樣的目錄結構

(除了 tests 以外)

接下來要建立測試

project

├── application

├── library

│ └── Math.php

└── tests

├── application

└── library

範例專案目錄結構

我們要測試的是這個類別

project

├── application

├── library

│ └── Math.php

└── tests

├── application

└── library

└── MathTest.php

範例專案目錄結構

在慣例上我們會在測試目錄下對應的

library 目錄中建立一個 MathTest.php

MathTest.php

MathTest.php

<?php

class MathTest{

}

定義一個 Test Case 類別

MathTest.php

<?php

class MathTest{

}

慣例上類別名稱與檔名相同

MathTest.php

引用我們要測試的類別檔案

<?phprequire_once __DIR__ . '/Math.php';class MathTest{

}

MathTest.php

繼承 PHPUnit 的 TestCase 類別

<?phprequire_once __DIR__ . '/Math.php';class MathTest extends PHPUnit_Framework_TestCase{

}

MathTest.php

加入一個測試

<?phprequire_once __DIR__ . '/Math.php';class MathTest extends PHPUnit_Framework_TestCase{    public function testSum()    {

    }}

MathTest.php

測試函式的開頭一定要為 test

<?phprequire_once __DIR__ . '/Math.php';class MathTest extends PHPUnit_Framework_TestCase{    public function testSum()    {

    }}

MathTest.php

test 後面通常是要測試的方法名稱也可以是一個

CamelCase 的句子

<?phprequire_once __DIR__ . '/Math.php';class MathTest extends PHPUnit_Framework_TestCase{    public function testSum()    {

    }}

MathTest.php

把原來測試的方式改用 PHPUnit 的

assertions

<?phprequire_once __DIR__ . '/Math.php';class MathTest extends PHPUnit_Framework_TestCase{    public function testSum()    {        $this->assertEquals(55, Math::sum(1, 10));        $this->assertEquals(5050, Math::sum(1, 100));    }}

MathTest.php

<?phprequire_once __DIR__ . '/Math.php';class MathTest extends PHPUnit_Framework_TestCase{    public function testSum()    {        $this->assertEquals(55, Math::sum(1, 10));        $this->assertEquals(5050, Math::sum(1, 100));    }}

這邊是預期的結果

MathTest.php

<?phprequire_once __DIR__ . '/Math.php';class MathTest extends PHPUnit_Framework_TestCase{    public function testSum()    {        $this->assertEquals(55, Math::sum(1, 10));        $this->assertEquals(5050, Math::sum(1, 100));    }}

這邊是實際得到的結果

執行測試# phpunit tests/library/MathTest

直接在 console 下指令

不需要指定完整 php 檔名

執行測試# phpunit tests/library/MathTestPHPUnit 3.5.15 by Sebastian Bergmann.

.

Time: 0 seconds, Memory: 5.25Mb

OK (1 test, 2 assertions)

這樣就算測試成功了

執行測試# phpunit tests/library/MathTestPHPUnit 3.5.15 by Sebastian Bergmann.

.

Time: 0 seconds, Memory: 5.25Mb

OK (1 test, 2 assertions)總共有一個測試兩個 assertions

我看到你們

心中的疑問了

phpunit 指令會自動載入

PHPUnit 相關類別

每個 Test Case 類別

都可以有多組 Tests也就是 testXxxx 方法

每組 Test

都可以有多個 assertions

不過類似的 assertions 太多

寫起來感覺很麻煩

用 Data Provider 來提供測試資料

MathTest.php

先將原來的 MathTest 內容修改成這樣

<?phprequire_once dirname(dirname(__DIR__)) . '/library/Math.php';class MathTest extends PHPUnit_Framework_TestCase{

    public function testSum()    {        $this->assertEquals(55, Math::sum(1, 10 ));    }

}

MathTest.php

<?phprequire_once dirname(dirname(__DIR__)) . '/library/Math.php';class MathTest extends PHPUnit_Framework_TestCase{

    public function testSum($expected, $min, $max)    {        $this->assertEquals(55, Math::sum(1, 10 ));    }

}

加上方法參數

MathTest.php

<?phprequire_once dirname(dirname(__DIR__)) . '/library/Math.php';class MathTest extends PHPUnit_Framework_TestCase{

    public function testSum($expected, $min, $max)    {        $this->assertEquals($expected, Math::sum($min, $max));    }

}

將預期結果與實際結果

都改為引用參數

MathTest.php

加入提供資料的 public 方法

<?phprequire_once dirname(dirname(__DIR__)) . '/library/Math.php';class MathTest extends PHPUnit_Framework_TestCase{

    public function testSum($expected, $min, $max)    {        $this->assertEquals($expected, Math::sum($min, $max));    }

    public function myDataProvider()    {        return array(            array(55, 1, 10),            array(5050, 1, 100)        );    }}

MathTest.php

每組資料都對應到上面的方法參數

<?phprequire_once dirname(dirname(__DIR__)) . '/library/Math.php';class MathTest extends PHPUnit_Framework_TestCase{

    public function testSum($expected, $min, $max)    {        $this->assertEquals($expected, Math::sum($min, $max));    }

    public function myDataProvider()    {        return array(            array(55, 1, 10),            array(5050, 1, 100)        );    }}

MathTest.php

利用 PHPUnit 的 @dataProvider 這個 annotation

來引用 data provider

<?phprequire_once dirname(dirname(__DIR__)) . '/library/Math.php';class MathTest extends PHPUnit_Framework_TestCase{    /**     * @dataProvider myDataProvider     */    public function testSum($expected, $min, $max)    {        $this->assertEquals($expected, Math::sum($min, $max));    }

    public function myDataProvider()    {        return array(            array(55, 1, 10),            array(5050, 1, 100)        );    }}

執行測試# phpunit tests/library/MathTest

再測試一次

執行測試# phpunit tests/library/MathTestPHPUnit 3.5.15 by Sebastian Bergmann.

..

Time: 0 seconds, Memory: 5.25Mb

OK (2 tests, 2 assertions)這次變成了兩個 tests

Provider 所提供的每組資料

都會被視為一個 Test

Situation

如果要給

Math::sum 的程式碼一個分數...

如果要給

Math::sum 的程式碼一個分數...

小學生都知道

1 + 2 + 3 + ... + N

梯形公式

=

重構 Math::sum 的程式碼

Math.php

回到 Math.php<?phpclass Math{ public static function sum($min, $max) { $sum = 0; for ($i = $min; $i <= $max; $i++) { $sum += $i; } return $sum; }}

Math.php

拿掉原來的運算式

<?phpclass Math{ public static function sum($min, $max) { $sum = 0;

return $sum; }}

Math.php

改用公式解

<?phpclass Math{ public static function sum($min, $max) { $sum = $min + $max * $max / 2;

return $sum; }}

執行測試# phpunit tests/library/MathTest

一樣是 MathTest.php

執行測試# phpunit tests/library/MathTestPHPUnit 3.5.15 by Sebastian Bergmann.

F

Time: 0 seconds, Memory: 6.00Mb

There was 1 failure:

1) MathTest03::testSumFailed asserting that <integer:51> matches expected <integer:55>.

/path/to/tests/library/MathTest.php:7

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

出現測試錯誤了

執行測試# phpunit tests/library/MathTestPHPUnit 3.5.15 by Sebastian Bergmann.

F

Time: 0 seconds, Memory: 6.00Mb

There was 1 failure:

1) MathTest03::testSumFailed asserting that <integer:51> matches expected <integer:55>.

/path/to/tests/library/MathTest.php:7

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

預期結果為 55但是實際卻是 51

Math.php

看來問題出在剛剛改的程式碼

<?phpclass Math{ /** * 計算總合 */ public static function sum($min, $max) { $sum = $min + $max * $max / 2;

return $sum; }}

Math.php

原來忘了先乘除後加減

<?phpclass Math{ /** * 計算總合 */ public static function sum($min, $max) { $sum = ($min + $max) * $max / 2;

return $sum; }}

執行測試# phpunit tests/library/MathTestPHPUnit 3.5.15 by Sebastian Bergmann.

.

Time: 0 seconds, Memory: 5.25Mb

OK (1 test, 2 assertions)

測試成功了

測試能確保我們的重構

是在正確的方向

再來點複雜的

用物件組出

SQL 的 Select 語法在 ORM Framework 中很常見

概念設計

概念設計

•類別: DbSelect

概念設計

•類別: DbSelect

•方法:

概念設計

•類別: DbSelect

•方法:‣ from :對應到 SELECT 語法的 FROM

概念設計

•類別: DbSelect

•方法:‣ from :對應到 SELECT 語法的 FROM

‣ cols :對應到 SELECT 語法的欄位,預設為 *

$select = new DbSelect();echo $select->from(‘table’);

SELECT * FROM table

Example 1

$select = new DbSelect();echo $select->from(‘table’)->cols(array(    ‘col_a’, ‘col_b’));

SELECT col_a, col_b FROM table

Example 2

project

├── application

├── library

│ └── DbSelect.php

└── tests

├── application

└── library

└── DbSelectTest.php

範例專案目錄結構

準備好這兩個檔案

DbSelect.php

先建立 DbSelect 類別

但我們還不知道怎麼實作

<?phpclass DbSelect{

}

DbSelectTest.php

所以我們先建立測試類別

<?phprequire_once dirname(dirname(__DIR__)) . '/library/DbSelect.php';class DbSelectTest extends PHPUnit_Framework_TestCase{

}

DbSelectTest.php

在設計中類別會有一個 from 方法

<?phprequire_once dirname(dirname(__DIR__)) . '/library/DbSelect.php';class DbSelectTest extends PHPUnit_Framework_TestCase{    public function testFrom()    {    

    }

}

DbSelectTest.php

先寫出了它的用法與預期產生的結果做為測試用

<?phprequire_once dirname(dirname(__DIR__)) . '/library/DbSelect.php';class DbSelectTest extends PHPUnit_Framework_TestCase{    public function testFrom()    {        $select = new DbSelect();        $select->from('test');        $this->assertEquals('SELECT * FROM test',  $select->__toString());    }

}

DbSelect.php

回到 DbSelect.php<?phpclass DbSelect{

}

DbSelect.php

先加入 from 方法

<?phpclass DbSelect{

    

        public function from($table)    {

    }    

    

}

DbSelect.php

加入 __toString 方法

<?phpclass DbSelect{

        public function from($table)    {

    }    

        public function __toString()    {

    }}

DbSelect.php

先寫出可以通過測試的程式

<?phpclass DbSelect{

        public function from($table)    {

    }    

        public function __toString()    {

        return 'SELECT * FROM test';    }}

執行測試# phpunit tests/library/DbSelectTestPHPUnit 3.5.15 by Sebastian Bergmann.

.

Time: 0 seconds, Memory: 5.25Mb

OK (1 test, 1 assertions)

我知道這看起來很蠢

DbSelect.php

把 table 改成可以置換

<?phpclass DbSelect{

    

        public function from($table)    {

    }    

        public function __toString()    {

        return 'SELECT * FROM ' . $this->_table;    }}

DbSelect.php

加入 $_table 屬性

<?phpclass DbSelect{    protected $_table = 'table';    

        public function from($table)    {

    }    

        public function __toString()    {

        return 'SELECT * FROM ' . $this->_table;    }}

DbSelect.php

from 方法是一個有驗證的 setter

<?phpclass DbSelect{    protected $_table = 'table';    

        public function from($table)    { if (!preg_match('/[0-9a-z]+/i', $table)) {     throw new IllegalNameException('Illegal Table Name'); }        $this->_table = $table;        return $this;    }    

        public function __toString()    {

        return 'SELECT * FROM ' . $this->_table;    }}

執行測試# phpunit tests/library/DbSelectTestPHPUnit 3.5.15 by Sebastian Bergmann.

.

Time: 0 seconds, Memory: 5.25Mb

OK (1 test, 1 assertions)

Design Write Test Coding→ →Direction Find Target Fire→ →

完成一個功能並測試成功後

就可以繼續下一個功能

DbSelectTest.php

回到 DbSelectTest.php

<?phprequire_once dirname(dirname(__DIR__)) . '/library/DbSelect.php';class DbSelectTest extends PHPUnit_Framework_TestCase{    public function testFrom()    {        $select = new DbSelect();        $select->from('test');        $this->assertEquals('SELECT * FROM test', $select->__toString());    }

}

DbSelectTest.php

補上 cols 方法的測試

<?phprequire_once dirname(dirname(__DIR__)) . '/library/DbSelect.php';class DbSelectTest extends PHPUnit_Framework_TestCase{    public function testFrom()    {        $select = new DbSelect();        $select->from('test');        $this->assertEquals('SELECT * FROM test',  $select->__toString());    }        public function testCols()    {        $select = new DbSelect();        $select->from('test')->cols(array(            'col_a',            'col_b',        ));        $this->assertEquals('SELECT col_a, col_b FROM test',  $select->__toString());    }}

DbSelect.php

回到 DbSelect.php<?phpclass DbSelect{    protected $_table = 'table';    

        public function from($table)    { if (!preg_match('/[0-9a-z]+/i', $table)) {     throw new IllegalNameException('Illegal Table Name'); }        $this->_table = $table;        return $this;    }    

        public function __toString()    {

        return 'SELECT * FROM ' . $this->_table;    }}

DbSelect.php

加入 cols 方法

<?phpclass DbSelect{    protected $_table = 'table';    

        public function from($table)    { if (!preg_match('/[0-9a-z]+/i', $table)) {     throw new IllegalNameException('Illegal Table Name'); }        $this->_table = $table;        return $this;    }        public function cols($cols)    {        $this->_cols = (array) $cols;        return $this;    }      public function __toString()    {

        return 'SELECT * FROM ' . $this->_table;    }}

DbSelect.php

加上 $_cols 屬性

<?phpclass DbSelect{    protected $_table = 'table';        protected $_cols = '*';        public function from($table)    { if (!preg_match('/[0-9a-z]+/i', $table)) {     throw new IllegalNameException('Illegal Table Name'); }        $this->_table = $table;        return $this;    }        public function cols($cols)    {        $this->_cols = (array) $cols;        return $this;    }      public function __toString()    {

        return 'SELECT * FROM ' . $this->_table;    }}

DbSelect.php

用 $_cols 屬性替換掉原來的 *

<?phpclass DbSelect{    protected $_table = 'table';        protected $_cols = '*';        public function from($table)    { if (!preg_match('/[0-9a-z]+/i', $table)) {     throw new IllegalNameException('Illegal Table Name'); }        $this->_table = $table;        return $this;    }        public function cols($cols)    {        $this->_cols = (array) $cols;        return $this;    }      public function __toString()    {        $cols = implode(', ', (array) $this->_cols);        return 'SELECT ' . $cols . ' FROM ' . $this->_table;    }}

執行測試# phpunit tests/library/DbSelectTestPHPUnit 3.5.15 by Sebastian Bergmann.

.

Time: 0 seconds, Memory: 5.25Mb

OK (2 test, 2 assertions)兩個測試都通過了

通常新增的功能

不會影響舊的功能

如果舊測試發生錯誤

就表示新的功能帶來了 bug

每個測試所會用到的資源

都要隔離並重新初始化

DbSelectTest.php

回到 DbSelectTest.php

<?phprequire_once dirname(dirname(__DIR__)) . '/library/DbSelect.php';class DbSelectTest extends PHPUnit_Framework_TestCase{    public function testFrom()    {        $select = new DbSelect();        $select->from('test');        $this->assertEquals('SELECT * FROM test',  $select->__toString());    }        public function testCols()    {        $select = new DbSelect();        $select->from('test')->cols(array(            'col_a',            'col_b',        ));        $this->assertEquals('SELECT col_a, col_b FROM test',  $select->__toString());    }}

DbSelectTest.php

每個測試都需要 new DbSelect()

<?phprequire_once dirname(dirname(__DIR__)) . '/library/DbSelect.php';class DbSelectTest extends PHPUnit_Framework_TestCase{    public function testFrom()    {        $select = new DbSelect();        $select->from('test');        $this->assertEquals('SELECT * FROM test', $select->__toString());    }        public function testCols()    {        $select = new DbSelect();        $select->from('test')->cols(array(            'col_a',            'col_b',        ));        $this->assertEquals('SELECT col_a, col_b FROM test', $select->__toString());    }}

DRYDon't Repeat Yourself

Fixture每個測試必定會用的資源

DbSelectTest.php

先拿掉原來的 new DbSelect()

<?phprequire_once dirname(dirname(__DIR__)) . '/library/DbSelect.php';class DbSelectTest extends PHPUnit_Framework_TestCase{

    public function testFrom()    {        $select->from('test');        $this->assertEquals('SELECT * FROM test',  $select->__toString());    }        public function testCols()    {        $select->from('test')->cols(array('col_a', 'col_b'));        $this->assertEquals('SELECT col_a, col_b FROM test',   $select->__toString());    }    

}

DbSelectTest.php

加入一個 fixture

<?phprequire_once dirname(dirname(__DIR__)) . '/library/DbSelect.php';class DbSelectTest extends PHPUnit_Framework_TestCase{    protected $_select;    

        public function testFrom()    {        $select->from('test');        $this->assertEquals('SELECT * FROM test', $select->__toString());    }        public function testCols()    {        $select->from('test')->cols(array('col_a', 'col_b'));        $this->assertEquals('SELECT col_a, col_b FROM test',   $select->__toString());    }    

}

DbSelectTest.php

利用 setUp 方法來初始化 fixture

<?phprequire_once dirname(dirname(__DIR__)) . '/library/DbSelect.php';class DbSelectTest extends PHPUnit_Framework_TestCase{    protected $_select;        protected function setUp()    {        $this->_select = new DbSelect();    }        public function testFrom()    {        $select->from('test');        $this->assertEquals('SELECT * FROM test', $select->__toString());    }        public function testCols()    {        $select->from('test')->cols(array('col_a', 'col_b'));        $this->assertEquals('SELECT col_a, col_b FROM test',   $select->__toString());    }

}

DbSelectTest.php

每一次測試完成後用 tearDown 消滅 fixture

<?phprequire_once dirname(dirname(__DIR__)) . '/library/DbSelect.php';class DbSelectTest extends PHPUnit_Framework_TestCase{    protected $_select;        protected function setUp()    {        $this->_select = new DbSelect();    }        public function testFrom()    {        $select->from('test');        $this->assertEquals('SELECT * FROM test',  $select->__toString());    }        public function testCols()    {        $select->from('test')->cols(array('col_a', 'col_b'));        $this->assertEquals('SELECT col_a, col_b FROM test',   $select->__toString());    }        protected function tearDown()    {        $this->_select = null;    }}

DbSelectTest.php

$select 改用 $this->_select

<?phprequire_once dirname(dirname(__DIR__)) . '/library/DbSelect.php';class DbSelectTest extends PHPUnit_Framework_TestCase{    protected $_select;        protected function setUp()    {        $this->_select = new DbSelect();    }        public function testFrom()    {        $this->_select->from('test');        $this->assertEquals('SELECT * FROM test',  $this->_select->__toString());    }        public function testCols()    {        $this->_select->from('test')->cols(array('col_a', 'col_b'));        $this->assertEquals('SELECT col_a, col_b FROM test',   $this->_select->__toString());    }        protected function tearDown()    {        $this->_select = null;    }}

setUp() → testFrom() → tearDown()

setUp() → testCols() → tearDown()

執行測試# phpunit tests/library/DbSelectTestPHPUnit 3.5.15 by Sebastian Bergmann.

.

Time: 0 seconds, Memory: 5.25Mb

OK (2 test, 2 assertions)

這時 DbSelectTest 變成被測試的對象

測試也需要重構

測試與被測試的角色對調

“Houston, we have a problem.”

DbSelect

被用戶發現有 bug

我們預期的用法

假設我們也完成了 where 方法

可以產生條件式

$select = new DbSelect();$select->from('table')->where('id = 1');

我們預期的用法$select = new DbSelect();$select->from('table')->where('id = 1');

// 輸出:// SELECT * FROM table WHERE id = 1

用戶的寫法$select = new DbSelect();$select->from('table WHERE id = 1');

// 輸出:// SELECT * FROM table WHERE id = 1

但用戶卻發現這樣寫也可以

寫程式的人是我

寫出 Bug 的是別的什麼東西

把用戶遇到的問題加入測試

DbSelectTest.php

回到 DbSelectTest.php

<?phprequire_once dirname(dirname(__DIR__)) . '/library/DbSelect.php';class DbSelectTest extends PHPUnit_Framework_TestCase{    // ... 略 ...

}

DbSelectTest.php

加入不合法資料表名稱的測試

<?phprequire_once dirname(dirname(__DIR__)) . '/library/DbSelect.php';class DbSelectTest extends PHPUnit_Framework_TestCase{    // ... 略 ...

    public function testIllegalTableName()    {        try {            $this->_select->from('test WHERE id = 1');        }        catch (IllegalNameException $e) {            throw $e;        }    }}

DbSelectTest.php

透過 PHPUnit 的 @expectedException

annotation 來驗證

<?phprequire_once dirname(dirname(__DIR__)) . '/library/DbSelect.php';class DbSelectTest extends PHPUnit_Framework_TestCase{    // ... 略 ...

    /**     * @expectedException IllegalNameException     */    public function testIllegalTableName()    {        try {            $this->_select->from('test WHERE id = 1');        }        catch (IllegalNameException $e) {            throw $e;        }    }}

執行測試# phpunit tests/library/DbSelectTestPHPUnit 3.5.15 by Sebastian Bergmann.

.F.

Time: 0 seconds, Memory: 6.00Mb

There was 1 failure:

1) DbSelectTest::testIllegalTableNameExpected exception IllegalNameException

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

應該丟出 IllegalNameException

卻沒有

有時候你需要測試出

預期會錯誤的狀況

DbSelectTest.php

切換到 DbSelect.php<?phpclass DbSelect{    // ... 略 ...

    public function from($table)    { if (!preg_match('/[0-9a-z]+/i', $table)) {     throw new Exception('Illegal Table Name: ' . $table); }        $this->_table = $table;        return $this;    }

    // ... 略 ...}

DbSelectTest.php

問題出在這裡

<?phpclass DbSelect{    // ... 略 ...

    public function from($table)    { if (!preg_match('/[0-9a-z]+/i', $table)) {     throw new Exception('Illegal Table Name: ' . $table); }        $this->_table = $table;        return $this;    }

    // ... 略 ...}

DbSelectTest.php

前後分別少了 ^ 及 $

<?phpclass DbSelect{    // ... 略 ...

    public function from($table)    { if (!preg_match('/^[0-9a-z]+$/i', $table)) {     throw new Exception('Illegal Table Name: ' . $table); }        $this->_table = $table;        return $this;    }

    // ... 略 ...}

執行測試# phpunit tests/library/DbSelectTestPHPUnit 3.5.15 by Sebastian Bergmann.

...

Time: 1 second, Memory: 5.50Mb

OK (3 tests, 3 assertions)

成功!

花一整天找 bug

不如花一小時寫測試

其他常用技巧

如果有多個類別要測試

以前我會用 Test Suite

現在直接測試

tests 資料夾就可以phpunit tests

我們希望控制

執行測試時輸出的結果

交給 phpunit.xml 吧

project

├── application

├── library

└── tests

└── phpunit.xml

範例專案目錄結構

把 phpunit.xml 放在 tests 目錄下

phpunit.xml

root tag 為 phpunit<phpunit>

</phpunit>

phpunit.xml

可以在 root tag 加上測試的設定

<phpunit colors=”true”>

</phpunit>

phpunit.xml

加上多個 test suites<phpunit colors=”true”> <testsuite name="Application Test Suite"> <directory>./application</directory> </testsuite> <testsuite name="Library Test Suite"> <directory>./library</directory> </testsuite></phpunit>

以 phpunit.xml 做測試# phpunit -c tests/phpunit.xml PHPUnit 3.5.15 by Sebastian Bergmann.

....

Time: 0 seconds, Memory: 6.00Mb

OK (4 tests, 4 assertions) 因為 colors=”true” 所以會有顏色

phpunit.xml

也可以在執行測試前預先執行 PHP 程式

例如定義類別自動載入程式

<phpunit colors=”true” bootstrap=”./bootstrap.php”> <testsuite name="Application Test Suite"> <directory>./application</directory> </testsuite> <testsuite name="Library Test Suite"> <directory>./library</directory> </testsuite></phpunit>

bootstrap.php 範例<?phpdefine('PROJECT_PATH', realpath(dirname(__DIR__)));

set_include_path(implode(PATH_SEPARATOR, array(    realpath(PROJECT_PATH . '/library'),    realpath(PROJECT_PATH . '/application'),    get_include_path())));

function autoload($className){    $className = str_replace('_', '/', $className);    require_once "$className.php";}

spl_autoload_register('autoload');

請上官方網站查看

更多 phpunit.xml 的介紹

http://goo.gl/tvmq4

最後分享一些小心得

切割你的程式

讓它易於測試

要寫測試不難

難在測試什麼

不要去盲目的信任

要做到反覆的驗證

學會基礎其實不難

變成習慣比較困難

這份 Slides

還有很多東西沒提

只能期待下次再相逢

謝謝大家