beginning phpunit

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

Upload: jace-ju

Post on 17-May-2015

2.800 views

Category:

Technology


3 download

TRANSCRIPT

Page 1: Beginning PHPUnit

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

Page 2: Beginning PHPUnit

關於我

Jace Ju / jaceju / 大澤木小鐵

PHP Smarty 樣版引擎 作者

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

Page 3: Beginning PHPUnit

接下來的內容都是基本功

不會有太深的專有名詞

Page 4: Beginning PHPUnit

今日重點

Page 5: Beginning PHPUnit

•如何用 PHPUnit 寫測試

今日重點

Page 6: Beginning PHPUnit

•如何用 PHPUnit 寫測試

•基本的 PHPUnit 用法

今日重點

Page 7: Beginning PHPUnit

•如何用 PHPUnit 寫測試

•基本的 PHPUnit 用法

•如何搭配測試來做重構

今日重點

Page 8: Beginning PHPUnit

•如何用 PHPUnit 寫測試

•基本的 PHPUnit 用法

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

今日重點

Page 9: Beginning PHPUnit

進入主題

Page 10: Beginning PHPUnit

Question 1

Page 11: Beginning PHPUnit

你如何測試

你的 Web 應用程式?

Page 12: Beginning PHPUnit

瀏覽器打開來

一個步驟一個步驟測試

Page 13: Beginning PHPUnit

這叫測試?

Page 14: Beginning PHPUnit

這叫測試?

這叫自虐!

Page 15: Beginning PHPUnit

Question 2

Page 16: Beginning PHPUnit

你認為你交給客戶的程式

都是沒問題的嗎?

Page 17: Beginning PHPUnit

任何程式都不可能

完美考慮到

所有使用者的狀況

Page 18: Beginning PHPUnit

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

Page 19: Beginning PHPUnit

Question 3

Page 20: Beginning PHPUnit

你確定每次抓完一個 bug 後

不會生出其他 bugs 嗎?

Page 21: Beginning PHPUnit

Bugs 總會在

改了幾行程式碼後來找你

Page 22: Beginning PHPUnit

每次改完程式

都一定要從頭測試

Page 23: Beginning PHPUnit

這時候我們需要

用程式來寫測試

Page 24: Beginning PHPUnit

Example

Page 25: Beginning PHPUnit

來個簡單的

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

Page 26: Beginning PHPUnit

先來看看

範例專案目錄結構

Page 27: Beginning PHPUnit

project

├── application

└── library

│ └── Math.php

└── run_test.php

範例專案目錄結構

專案目錄

應用程式目錄

套件目錄

欲測試的類別

測試程式

Page 28: Beginning PHPUnit

project

├── application

└── library

│ └── Math.php

└── run_test.php

範例專案目錄結構

專案目錄

應用程式目錄

套件目錄

欲測試的類別

測試程式

Page 29: Beginning PHPUnit

project

├── application

└── library

│ └── Math.php

└── run_test.php

範例專案目錄結構

專案目錄

應用程式目錄

套件目錄

欲測試的類別

測試程式

Page 30: Beginning PHPUnit

project

├── application

└── library

│ └── Math.php

└── run_test.php

範例專案目錄結構

專案目錄

應用程式目錄

套件目錄

欲測試的類別

測試程式

Page 31: Beginning PHPUnit

project

├── application

└── library

│ └── Math.php

└── run_test.php

範例專案目錄結構

專案目錄

應用程式目錄

套件目錄

欲測試的類別

測試程式

Page 32: Beginning PHPUnit

從 Math.php 開始

Page 33: Beginning PHPUnit

Math.php

Page 34: Beginning PHPUnit

Math.php

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

<?phpclass Math{

}

Page 35: Beginning PHPUnit

Math.php

加入 Math::sum 方法

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

Page 36: Beginning PHPUnit

Math.php

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

}

利用 TEST_MODE 常數來進入測試

Page 37: Beginning PHPUnit

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";    }

}

Page 38: Beginning PHPUnit

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 的結果

Page 39: Beginning PHPUnit

接著看 run_test.php

Page 40: Beginning PHPUnit

run_test.php

Page 41: Beginning PHPUnit

run_test.php

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

常數

Page 42: Beginning PHPUnit

run_test.php

引用欲測試的類別

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

Page 43: Beginning PHPUnit

執行測試

Page 44: Beginning PHPUnit

執行測試

在命令列執行該指令

# php run_test.php

Page 45: Beginning PHPUnit

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

Page 46: Beginning PHPUnit

但是測試不是只有

比對值的相等

Page 47: Beginning PHPUnit

是否為某變數類型

但是測試不是只有

比對值的相等

Page 48: Beginning PHPUnit

陣列是否包含某值

是否為某變數類型

但是測試不是只有

比對值的相等

Page 49: Beginning PHPUnit

但是測試不是只有

比對值的相等

陣列是否包含某值

是否為某變數類型

類別是否有某屬性

Page 50: Beginning PHPUnit

陣列是否包含某值

是否為某變數類型

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

但是測試不是只有

比對值的相等

Page 51: Beginning PHPUnit

每一種比對

都要寫好多程式

Page 52: Beginning PHPUnit

結果用程式寫測試

反而增加了負擔

Page 53: Beginning PHPUnit

如果有工具來幫我們

做這些事就好了...

Page 54: Beginning PHPUnit

主角終於姍姍來遲

Page 55: Beginning PHPUnit

PHPUnitby Sebastian Bergmann

http://phpunit.de

Page 56: Beginning PHPUnit

從 JUnit 移植

http://www.junit.org/

Page 57: Beginning PHPUnit

屬於 xUnit 家族

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

Page 58: Beginning PHPUnit

# 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

安裝

Page 59: Beginning PHPUnit

改用 PHPUnit 測試

Page 60: Beginning PHPUnit

準備專案的測試環境

Page 61: Beginning PHPUnit

project

├── application

└── library

└── Math.php

範例專案目錄結構

回到原先的專案目錄

Page 62: Beginning PHPUnit

project

├── application

├── library

│ └── Math.php

└── tests

範例專案目錄結構

新增一個測試目錄

Page 63: Beginning PHPUnit

project

├── application

├── library

│ └── Math.php

└── tests

├── application

└── library

範例專案目錄結構

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

(除了 tests 以外)

Page 64: Beginning PHPUnit

接下來要建立測試

Page 65: Beginning PHPUnit

project

├── application

├── library

│ └── Math.php

└── tests

├── application

└── library

範例專案目錄結構

我們要測試的是這個類別

Page 66: Beginning PHPUnit

project

├── application

├── library

│ └── Math.php

└── tests

├── application

└── library

└── MathTest.php

範例專案目錄結構

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

library 目錄中建立一個 MathTest.php

Page 67: Beginning PHPUnit

MathTest.php

Page 68: Beginning PHPUnit

MathTest.php

<?php

class MathTest{

}

定義一個 Test Case 類別

Page 69: Beginning PHPUnit

MathTest.php

<?php

class MathTest{

}

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

Page 70: Beginning PHPUnit

MathTest.php

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

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

}

Page 71: Beginning PHPUnit

MathTest.php

繼承 PHPUnit 的 TestCase 類別

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

}

Page 72: Beginning PHPUnit

MathTest.php

加入一個測試

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

    }}

Page 73: Beginning PHPUnit

MathTest.php

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

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

    }}

Page 74: Beginning PHPUnit

MathTest.php

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

CamelCase 的句子

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

    }}

Page 75: Beginning PHPUnit

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));    }}

Page 76: Beginning PHPUnit

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));    }}

這邊是預期的結果

Page 77: Beginning PHPUnit

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));    }}

這邊是實際得到的結果

Page 78: Beginning PHPUnit

執行測試# phpunit tests/library/MathTest

直接在 console 下指令

不需要指定完整 php 檔名

Page 79: Beginning PHPUnit

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

.

Time: 0 seconds, Memory: 5.25Mb

OK (1 test, 2 assertions)

這樣就算測試成功了

Page 80: Beginning PHPUnit

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

.

Time: 0 seconds, Memory: 5.25Mb

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

Page 81: Beginning PHPUnit

我看到你們

心中的疑問了

Page 82: Beginning PHPUnit

phpunit 指令會自動載入

PHPUnit 相關類別

Page 83: Beginning PHPUnit

每個 Test Case 類別

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

Page 84: Beginning PHPUnit

每組 Test

都可以有多個 assertions

Page 85: Beginning PHPUnit

不過類似的 assertions 太多

寫起來感覺很麻煩

Page 86: Beginning PHPUnit

用 Data Provider 來提供測試資料

Page 87: Beginning PHPUnit

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 ));    }

}

Page 88: Beginning PHPUnit

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 ));    }

}

加上方法參數

Page 89: Beginning PHPUnit

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));    }

}

將預期結果與實際結果

都改為引用參數

Page 90: Beginning PHPUnit

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)        );    }}

Page 91: Beginning PHPUnit

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)        );    }}

Page 92: Beginning PHPUnit

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)        );    }}

Page 93: Beginning PHPUnit

執行測試# phpunit tests/library/MathTest

再測試一次

Page 94: Beginning PHPUnit

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

..

Time: 0 seconds, Memory: 5.25Mb

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

Page 95: Beginning PHPUnit

Provider 所提供的每組資料

都會被視為一個 Test

Page 96: Beginning PHPUnit

Situation

Page 97: Beginning PHPUnit

如果要給

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

Page 98: Beginning PHPUnit

如果要給

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

Page 99: Beginning PHPUnit

小學生都知道

1 + 2 + 3 + ... + N

梯形公式

=

Page 100: Beginning PHPUnit

重構 Math::sum 的程式碼

Page 101: Beginning PHPUnit

Math.php

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

Page 102: Beginning PHPUnit

Math.php

拿掉原來的運算式

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

return $sum; }}

Page 103: Beginning PHPUnit

Math.php

改用公式解

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

return $sum; }}

Page 104: Beginning PHPUnit

執行測試# phpunit tests/library/MathTest

一樣是 MathTest.php

Page 105: Beginning PHPUnit

執行測試# 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.

出現測試錯誤了

Page 106: Beginning PHPUnit

執行測試# 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

Page 107: Beginning PHPUnit

Math.php

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

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

return $sum; }}

Page 108: Beginning PHPUnit

Math.php

原來忘了先乘除後加減

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

return $sum; }}

Page 109: Beginning PHPUnit

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

.

Time: 0 seconds, Memory: 5.25Mb

OK (1 test, 2 assertions)

測試成功了

Page 110: Beginning PHPUnit

測試能確保我們的重構

是在正確的方向

Page 111: Beginning PHPUnit

再來點複雜的

Page 112: Beginning PHPUnit

用物件組出

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

Page 113: Beginning PHPUnit

概念設計

Page 114: Beginning PHPUnit

概念設計

•類別: DbSelect

Page 115: Beginning PHPUnit

概念設計

•類別: DbSelect

•方法:

Page 116: Beginning PHPUnit

概念設計

•類別: DbSelect

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

Page 117: Beginning PHPUnit

概念設計

•類別: DbSelect

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

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

Page 118: Beginning PHPUnit

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

SELECT * FROM table

Example 1

Page 119: Beginning PHPUnit

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

SELECT col_a, col_b FROM table

Example 2

Page 120: Beginning PHPUnit

project

├── application

├── library

│ └── DbSelect.php

└── tests

├── application

└── library

└── DbSelectTest.php

範例專案目錄結構

準備好這兩個檔案

Page 121: Beginning PHPUnit

DbSelect.php

先建立 DbSelect 類別

但我們還不知道怎麼實作

<?phpclass DbSelect{

}

Page 122: Beginning PHPUnit

DbSelectTest.php

所以我們先建立測試類別

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

}

Page 123: Beginning PHPUnit

DbSelectTest.php

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

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

    }

}

Page 124: Beginning PHPUnit

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());    }

}

Page 125: Beginning PHPUnit

DbSelect.php

回到 DbSelect.php<?phpclass DbSelect{

}

Page 126: Beginning PHPUnit

DbSelect.php

先加入 from 方法

<?phpclass DbSelect{

    

        public function from($table)    {

    }    

    

}

Page 127: Beginning PHPUnit

DbSelect.php

加入 __toString 方法

<?phpclass DbSelect{

        public function from($table)    {

    }    

        public function __toString()    {

    }}

Page 128: Beginning PHPUnit

DbSelect.php

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

<?phpclass DbSelect{

        public function from($table)    {

    }    

        public function __toString()    {

        return 'SELECT * FROM test';    }}

Page 129: Beginning PHPUnit

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

.

Time: 0 seconds, Memory: 5.25Mb

OK (1 test, 1 assertions)

Page 130: Beginning PHPUnit

我知道這看起來很蠢

Page 131: Beginning PHPUnit

DbSelect.php

把 table 改成可以置換

<?phpclass DbSelect{

    

        public function from($table)    {

    }    

        public function __toString()    {

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

Page 132: Beginning PHPUnit

DbSelect.php

加入 $_table 屬性

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

        public function from($table)    {

    }    

        public function __toString()    {

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

Page 133: Beginning PHPUnit

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;    }}

Page 134: Beginning PHPUnit

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

.

Time: 0 seconds, Memory: 5.25Mb

OK (1 test, 1 assertions)

Page 135: Beginning PHPUnit

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

Page 136: Beginning PHPUnit

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

就可以繼續下一個功能

Page 137: Beginning PHPUnit

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());    }

}

Page 138: Beginning PHPUnit

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());    }}

Page 139: Beginning PHPUnit

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;    }}

Page 140: Beginning PHPUnit

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;    }}

Page 141: Beginning PHPUnit

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;    }}

Page 142: Beginning PHPUnit

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;    }}

Page 143: Beginning PHPUnit

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

.

Time: 0 seconds, Memory: 5.25Mb

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

Page 144: Beginning PHPUnit

通常新增的功能

不會影響舊的功能

Page 145: Beginning PHPUnit

如果舊測試發生錯誤

就表示新的功能帶來了 bug

Page 146: Beginning PHPUnit

每個測試所會用到的資源

都要隔離並重新初始化

Page 147: Beginning PHPUnit

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());    }}

Page 148: Beginning PHPUnit

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());    }}

Page 149: Beginning PHPUnit

DRYDon't Repeat Yourself

Page 150: Beginning PHPUnit

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

Page 151: Beginning PHPUnit

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());    }    

}

Page 152: Beginning PHPUnit

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());    }    

}

Page 153: Beginning PHPUnit

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());    }

}

Page 154: Beginning PHPUnit

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;    }}

Page 155: Beginning PHPUnit

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;    }}

Page 156: Beginning PHPUnit

setUp() → testFrom() → tearDown()

setUp() → testCols() → tearDown()

Page 157: Beginning PHPUnit

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

.

Time: 0 seconds, Memory: 5.25Mb

OK (2 test, 2 assertions)

這時 DbSelectTest 變成被測試的對象

Page 158: Beginning PHPUnit

測試也需要重構

測試與被測試的角色對調

Page 159: Beginning PHPUnit

“Houston, we have a problem.”

Page 160: Beginning PHPUnit

DbSelect

被用戶發現有 bug

Page 161: Beginning PHPUnit

我們預期的用法

假設我們也完成了 where 方法

可以產生條件式

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

Page 162: Beginning PHPUnit

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

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

Page 163: Beginning PHPUnit

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

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

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

Page 164: Beginning PHPUnit

寫程式的人是我

寫出 Bug 的是別的什麼東西

Page 165: Beginning PHPUnit

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

Page 166: Beginning PHPUnit

DbSelectTest.php

回到 DbSelectTest.php

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

}

Page 167: Beginning PHPUnit

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;        }    }}

Page 168: Beginning PHPUnit

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;        }    }}

Page 169: Beginning PHPUnit

執行測試# 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

卻沒有

Page 170: Beginning PHPUnit

有時候你需要測試出

預期會錯誤的狀況

Page 171: Beginning PHPUnit

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;    }

    // ... 略 ...}

Page 172: Beginning PHPUnit

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;    }

    // ... 略 ...}

Page 173: Beginning PHPUnit

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;    }

    // ... 略 ...}

Page 174: Beginning PHPUnit

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

...

Time: 1 second, Memory: 5.50Mb

OK (3 tests, 3 assertions)

成功!

Page 175: Beginning PHPUnit

花一整天找 bug

不如花一小時寫測試

Page 176: Beginning PHPUnit

其他常用技巧

Page 177: Beginning PHPUnit

如果有多個類別要測試

以前我會用 Test Suite

Page 178: Beginning PHPUnit

現在直接測試

tests 資料夾就可以phpunit tests

Page 179: Beginning PHPUnit

我們希望控制

執行測試時輸出的結果

Page 180: Beginning PHPUnit

交給 phpunit.xml 吧

Page 181: Beginning PHPUnit

project

├── application

├── library

└── tests

└── phpunit.xml

範例專案目錄結構

把 phpunit.xml 放在 tests 目錄下

Page 182: Beginning PHPUnit

phpunit.xml

root tag 為 phpunit<phpunit>

</phpunit>

Page 183: Beginning PHPUnit

phpunit.xml

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

<phpunit colors=”true”>

</phpunit>

Page 184: Beginning 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>

Page 185: Beginning 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” 所以會有顏色

Page 186: Beginning PHPUnit

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>

Page 187: Beginning 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');

Page 188: Beginning PHPUnit

請上官方網站查看

更多 phpunit.xml 的介紹

http://goo.gl/tvmq4

Page 189: Beginning PHPUnit

最後分享一些小心得

Page 190: Beginning PHPUnit

切割你的程式

讓它易於測試

Page 191: Beginning PHPUnit

要寫測試不難

難在測試什麼

Page 192: Beginning PHPUnit

不要去盲目的信任

要做到反覆的驗證

Page 193: Beginning PHPUnit

學會基礎其實不難

變成習慣比較困難

Page 194: Beginning PHPUnit

這份 Slides

還有很多東西沒提

Page 195: Beginning PHPUnit

只能期待下次再相逢

Page 196: Beginning PHPUnit

謝謝大家