advanced php testing in action

Post on 17-May-2015

2.433 Views

Category:

Technology

0 Downloads

Preview:

Click to see full reader

DESCRIPTION

WebDev Party #1

TRANSCRIPT

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 設定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

top related