beginning phpunit
TRANSCRIPT
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!測試成功
但是測試不是只有
比對值的相等
是否為某變數類型
但是測試不是只有
比對值的相等
陣列是否包含某值
是否為某變數類型
但是測試不是只有
比對值的相等
但是測試不是只有
比對值的相等
陣列是否包含某值
是否為某變數類型
類別是否有某屬性
陣列是否包含某值
是否為某變數類型
類別是否有某屬性是否有預期的錯誤
但是測試不是只有
比對值的相等
每一種比對
都要寫好多程式
結果用程式寫測試
反而增加了負擔
如果有工具來幫我們
做這些事就好了...
主角終於姍姍來遲
屬於 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');
最後分享一些小心得
切割你的程式
讓它易於測試
要寫測試不難
難在測試什麼
不要去盲目的信任
要做到反覆的驗證
學會基礎其實不難
變成習慣比較困難
這份 Slides
還有很多東西沒提
只能期待下次再相逢
謝謝大家