Download - Advanced php testing in action
2011.12@果子
AdvancedPHP Testing
in action
關於我
Jace Ju / jaceju / 大澤木小鐵
Plurk: http://www.plurk.com/jaceju
傳統的 PHPUnit 用法
視窗切換法
指令監看法
watch -n 15 -d \find tests/ -mmin -1 -iname '"*.php"' -exec \'phpunit -c tests/phpunit.xml {} \;'
註: Mac 的 watch 指令有問題
每 15 秒就找出一分鐘之內有異動的 php 檔案來測試
找個好用的 IDE
NetBeans
http://netbeans.org/
在 NetBeans 設定PHPUnit
建立 NetBeans 專案
NetBeans 測試快捷鍵
動作 WindowsLinux Mac
Test Project Alt + F6 Ctrl + F6
Test Target File Ctrl + F6 Cmd + F6
Run Test Case F6 F6
註: Mac 上的 Ctrl + F6 這個鍵有時似乎無法正常動作
傳統 PHP 程式的問題所在
➡ 無法達到分工的目的➡ 出現錯誤時難以確認問題點➡ 無法單獨測試每個環節➡ 看起來就是醜
Why Framework?
Why MVC ?
一致性
分工
邏輯可以重複使用
Which Framework?
A simple mvc framework
https://github.com/jaceju/simple-mvc-framework
Why not ZF?
library!"" Controller.php!"" Response.php!"" Request.php!"" View.php#!"" Request# $"" Http.php!"" Response# $"" Http.php!"" Test# $"" ControllerTestCase.php$"" View $"" Html.php
Core Library
範例
Todo
project!"" application# !"" controllers# # $"" IndexController.php# !"" models# # $"" Todo.php# $"" views# $"" index.phtml$"" tests !"" application # !"" controllers # # $"" IndexControllerTest.php # !"" models # # $"" TodoTest.php # $"" views # $"" InterfaceTest.php !"" bootstrap.php $"" phpunit.xml
Application & Tests
Model應用邏輯
Model 怎麼測?
➡ 準備一個測試用的乾淨資料庫➡ 儘可能測試 Model 的應用邏輯
準備 Schema
CREATE DATABASE `testing` CHARSET=utf8;USE `testing`;
CREATE TABLE `todo` ( `id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT '自動編號', `task` varchar(100) NOT NULL COMMENT '工作', `done` enum('y','n') NOT NULL DEFAULT 'n' COMMENT '是否完成', PRIMARY KEY (`id`)) ENGINE=MyISAM DEFAULT CHARSET=utf8 COMMENT='Todo';
doc/schema.sql
資料庫環境相關參數以 PDO 為例
http://www.php.net/manual/en/pdo.construct.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> <php> <var name="DB_DSN" value="mysql:dbname=testing;host=127.0.0.1" /> <var name="DB_USER" value="username" /> <var name="DB_PASSWD" value="password" /> </php></phpunit>
tests/phpunit.xml
➡ fetchAll()
➡ add($task)
➡ done($id)
Model: Todo
先從測試開始
<?php
class TodoTest extends PHPUnit_Framework_TestCase{ private $_pdo = null;
private $_todo = null;
public function setUp() { $this->_pdo = new PDO( $GLOBALS['DB_DSN'], $GLOBALS['DB_USER'], $GLOBALS['DB_PASSWD']); $this->_pdo ->query('TRUNCATE TABLE todo');
Todo::setDb($this->_pdo); $this->_todo = new Todo(); }
public function tearDown() { $this->_pdo ->query('TRUNCATE TABLE todo'); }
tests/application/models/TodoTest.php
<?php
class Todo{ protected static $_pdo = null; public static function setDb(PDO $pdo) { self::$_pdo = $pdo; }
application/models/Todo.php
public function testAdd() { $this->assertEquals( 1, $this->_todo->add('Task 1') );
$this->assertEquals( 2, $this->_todo->add('Task 2') ); }
public function add($task) { $query = 'INSERT INTO todo (task) ' . 'VALUES (?)';
self::$_pdo->prepare($query) ->execute(array($task));
return self::$_pdo->lastInsertId(); }
public function testFetchAll() { $this->_todo->add('Task 1', 'm'); $this->_todo->add('Task 2', 'f');
$result = $this->_todo->fetchAll();
$this->assertEquals( 2, count($result) );
$this->assertContains( 'Task 1', $result[0] );
$this->assertContains( 'Task 2', $result[1] ); }
public function fetchAll() { $query = 'SELECT * FROM todo'; $stmt = self::$_pdo ->query($query);
return $stmt ->fetchAll(PDO::FETCH_ASSOC); }
public function testDone() { $this->_todo->add('Task 1'); $this->_todo->add('Task 2');
$this->assertEquals( 1, $this->_todo->done(1) );
$this->assertEquals( 1, $this->_todo->done(2) );
$this->assertEquals( 0, $this->_todo->done(3) ); }}
public function done($id) { $query = 'UPDATE todo ' . 'SET done = \'y\'' . 'WHERE id = ?'; $stmt = self::$_pdo->prepare($query);
$stmt->execute(array($id));
return $stmt->rowCount(); }}
寫完一個測試後就寫相對應的程式碼
寫好就用快速鍵測試
就是這麼簡單
資料...真的存進去了?
View資料呈現
View 究竟是什麼?
➡ 樣版引擎➡ 輸出 HTML / XML / JSON
無法知道瀏覽器的行為
如何測試介面?
Selenium IDE
http://seleniumhq.org/projects/ide/
➡ 錄製使用者的操作行為➡ 針對瀏覽器來修正腳本➡ 重新測試腳本➡ 轉存為 PHPUnit 測試腳本
<?php
class InterfaceTest extends PHPUnit_Extensions_SeleniumTestCase{
protected function setUp() { $this->setBrowser("*chrome"); $this->setBrowserUrl("http://test.dev/"); }
public function testMyTestCase() { $this->open("/advanced_php_testing/mvc/"); $this->type("id=new-todo", "Task 1"); $this->keyPress("id=new-todo", "13"); $this->waitForPageToLoad("30000"); $this->assertEquals("Task 1", $this->getText("//ul[@id='todo-list']/div[1]/div/div")); $this->type("id=new-todo", "Task 2"); $this->keyPress("id=new-todo", "13"); $this->waitForPageToLoad("30000"); $this->assertEquals("Task 2", $this->getText("//ul[@id='todo-list']/div[2]/div/div")); }}
tests/application/views/InterfaceTest.php
資料存取的問題
<?php
class InterfaceTest extends PHPUnit_Extensions_SeleniumTestCase{
protected function setUp() { $this->_pdo = new PDO( $GLOBALS['DB_DSN'], $GLOBALS['DB_USER'], $GLOBALS['DB_PASSWD']); $this->_pdo->query('TRUNCATE TABLE todo');
Todo::setDb($this->_pdo); $this->_todo = new Todo();
$this->setBrowser("*chrome"); $this->setBrowserUrl("http://test.dev/"); }
tests/application/views/InterfaceTest.php
PHPUnit Selenium如何執行?
Selenium Server
http://seleniumhq.org/projects/remote-control/
➡ Run Selenium Server
➡ Run Testing in NetBeans
介面測試的時機
Controller流程控制
單純測試流程的難題
DatabaseAccess
Web Service
Session
HTTP Header CookieBrowser
Request
Web ServerResponse
XmlHTTPRequest
HTML JSON
FileUpload
PHPUnit 沒有提供流程測試的機制
自己來比較快
以 HTTP Header 為例
Request
<?php
class Request{ protected $_headers = array( 'REQUEST_METHOD' => 'GET', );
public function setHeader($name, $value) { $this->_headers[$name] = $value; }
public function isPost() { return ('POST' === $this->_headers['REQUEST_METHOD']); }
public function isAjax() { return ('XMLHttpRequest' === $this->_headers['X_REQUESTED_WITH']); }
library/Request.php
<?php
class Request_Http extends Request{ public function isPost() { return ('POST' === $_SERVER['REQUEST_METHOD']); }
public function isAjax() { return ('XMLHttpRequest' === $_SERVER['X_REQUESTED_WITH']); }}
library/Request/Http.php
Response
<?php
class Response{ protected $_headers = array( 'Content-Type' => 'text/html; charset=utf-8', );
public function setHeader($name, $content) { $this->_headers[$name] = $content; }
public function getHeader($name) { return isset($this->_headers[$name]) ? $this->_headers[$name] : null; }
protected function sendHeaders() { // do nothing }
library/Response.php
Dependency Injection
$controller = new IndexController(new Todo());$controller->setRequest(new Request_Http()) ->setResponse(new Response_Http()) ->sendResponse(true) ->dispatch();
實際執行
$controller = new IndexController(new Todo());$controller->setRequest(new Request()) ->setResponse(new Response()) ->dispatch();
測試
將流程當做 Test Case
<?php
class Test_ControllerTestCase extends PHPUnit_Framework_TestCase{ protected $_controller = null;
protected $_request = null;
protected $_response = null;
public function setUp() { $this->_controller->setRequest($this->_request) ->setResponse($this->_response); }
public function dispatch($url) { $this->_parseUrl($url); $this->_controller->dispatch(); return $this; }
protected function _parseUrl($url) { $urlInfo = parse_url($url); if (isset($urlInfo['query'])) { parse_str($urlInfo['query'], $_GET); } }
library/Test/ControllerTestCase.php
➡ assertAction($action)
➡ assertResponseCode($code)
➡ assertRedirectTo($url)
<?php
class IndexControllerTest extends Test_ControllerTestCase{ public function setUp() { $todo = new Todo(); $this->_request = new Request(); $this->_response = new Response(); $this->_controller = new IndexController($todo); parent::setUp(); }
public function tearDown() { $this->_request->reset(); $this->_response->reset(); }
public function testHome() { $this->dispatch('/'); $this->assertAction('index') ->assertResponseCode(200); }
tests/application/tests/IndexControllerTest.php
隔離資料來源
Mock & Stub讓同事沒有藉口
Phake
https://github.com/mlively/Phake
<?php
class IndexControllerTest extends Test_ControllerTestCase{ public function setUp() { $todo = $this->_setUpTodo(); $this->_controller = new IndexController($todo); // ... }
protected function _setUpTodo() { $todo = Phake::mock('Todo'); Phake::when($todo)->fetchAll()->thenReturn(array( array( 'id' => 1, 'task' => 'Task 1', 'done' => 'n', ), )); return $todo; }
tests/application/tests/IndexControllerTest.php
驗證輸出結果
➡ assertQuery($selector)
➡ assertQueryContain($selector, $text)
phpQuery
http://code.google.com/p/phpquery/
<?php
class IndexControllerTest extends Test_ControllerTestCase{ // ...
public function testHome() { $this->dispatch('/'); $this->assertAction('index') ->assertResponseCode(200) ->assertQuery('#todo-list'); }
public function testAdd() { $this->_request->setMethod('POST'); $_POST['task'] = 'Task 1'; $this->dispatch('/?act=add') ->assertAction('add') ->assertRedirectTo('./') ->assertResponseCode(200) ->assertQueryContain( '#todo-list>.todo>.display>.todo-text', 'Task 1' ); }
tests/application/tests/IndexControllerTest.php
總結
➡ Model測試資料應用邏輯
➡ View測試介面在瀏覽器上的運作
➡ Controller將干擾隔離以便測試流程
其他參考資源
➡ Zend_Test http://framework.zend.com/manual/en/zend.test.html
➡ CakePHP Testinghttp://book.cakephp.org/2.0/en/development/testing.html
➡ Planet PHPUnit http://planet.phpunit.de/
➡ PHPUnit Manualhttp://www.phpunit.de/manual/3.6/en/
謝謝大家
Q & A