從無法進行單元測試的「古典」程式開始做架構改善,現在已經可以做單元測試了。所以還是先把單元測試做一下。這樣未來有程式的變動,或是進一步調整,都可以透過測試做一些品質的保證,也可以提早發現問題,不必等到程式開始在伺服器執行。
雖然最近看到phptestr有漂亮的介面,寫測試好像也蠻簡單,不過還是先用比較熟悉的phpunit來做。
首先,當然還是要先架構測試環境。先透過composer把phpunit裝起來,所以在composer.json中加上require-dev:
{
"require": {
"twig/twig":"1.*"
},
"require-dev": {
"phpunit/phpunit":"3.7.*"
}
}
然後執行composer update
來安裝。安裝完以後,繼續準備測試環境。首先是準備測試資料庫,只要把原本的資料庫、設定檔、bootstrap等複製一份做出測試資料庫及設定就可以。然後新建一個目錄來放單元測試的程式,就叫tests吧。
然後再考慮一下要測試哪些部分...如果是使用現成的framework,並不需要針對framework做測試。不過因為這次是自己土砲的架構,所以包含架構最好也做一下測試。這樣需要做的大概包括幾個部分:
View架構的測試比較簡單,只要做一個簡單的template,然後檢查他是否有被正確顯示:
<?php
include 'bootstraptest.php';
class TestView extends PHPUnit_Framework_TestCase
{
public $fixtrue = null;
public function setup()
{
$this->fixture = Fillano\Core\View::getInstance('views');
}
public function testViewAssign()
{
$this->fixture->assign(array('test'=>'testTwig'));
$this->assertEquals('testTwig', $this->fixture->fetch('testTwig','html'));
$this->fixture->assign(array('test'=>'testTwig123'));
$this->assertEquals('testTwig123', $this->fixture->fetch('testTwig','html'));
}
public function testViewFetch()
{
$this->fixture->assign(array('test'=>'testTwig123'));
$this->assertEquals('testTwig123', $this->fixture->fetch('testTwig','html'));
}
public function testViewRender()
{
$this->expectOutputString('testTwig123');
$this->fixture->assign(array('test'=>'testTwig123'));
$this->fixture->render('testTwig','html');
}
public function teardown()
{
$this->fixture = null;
}
}
不過仔細考慮一下,如果沒辦法抽換bootstraptest.php,我就沒辦法測試不同的樣板引擎,這樣的設計似乎有點問題...要改善這個問題讓測試容易撰寫,也許還是調整一下架構比較好。
回頭看一下View的程式碼:
<?php
namespace Fillano\Core;
abstract class View
{
public abstract function asign($datas = array());
public abstract function render($template="", $ext);
public abstract function fetch($template="", $ext);
public static function getInstance($path) {
if (!defined('TEMPLATE_ENGINE')) {
die('Please specify a template engine in config.php');
}
$class = "Fillano\\Core\\".TEMPLATE_ENGINE;
return new $class($path);
}
}
其實在getInstance加一個參數指定樣板引擎,而不是在方法中去讀取設定,這樣的作法可能比較好。所以稍微調整一下:
<?php
namespace Fillano\Core;
abstract class View
{
public abstract function asign($datas = array());
public abstract function render($template="", $ext);
public abstract function fetch($template="", $ext);
public static function getInstance($engine, $path) {
$class = "Fillano\\Core\\".$engine;
if(!class_exists($class)) {
throw new \Exception('Specified Template Engine class not found.');
}
return new $class($path);
}
}
不過在使用時就要改成:
<?php
require 'bootstrap.php';
if(isset($_SESSION['user']['account'])) {
$member = true;
$name = $_SESSION['user']['name'];
} else $member = false;
if(isset($_SESSION['msg'])) {
$message = mysql_real_escape_string($_SESSION['msg']);
unset($_SESSION['msg']);
}
//$model = new Fillano\Models\MysqlIndex($conn);
$model = Fillano\Core\ModelFactory::getInstance('Index');
$data = $model->getForumList();
$view = Fillano\Core\View::getInstance(TEMPLATE_ENGINE, 'views');//getInstance時,要傳使用的template engine設定給他
$view->assign(array(
'member'=>$member,
'data'=>$data,
'name'=>$name,
'message'=>$message
));
$view->render('index', 'html');
調整好了以後,就來寫簡單的unittest。首先,為了可以驗證資料,寫一個簡單的template(views/testTwig.html):
{{test}}
這個template只做一件事,就是接收傳給他的test變數,然後顯示出來。接下來寫unittest:
<?php
include 'bootstrap.php';
class TestTwigView extends PHPUnit_Framework_TestCase
{
public $fixtrue = null;
public function setup()
{
$this->fixture = Fillano\Core\View::getInstance('TwigView', 'views');
}
public function testAssign()
{
$this->fixture->assign(array('test'=>'testTwig'));
$this->assertEquals('testTwig', $this->fixture->fetch('testTwig','html'));
$this->fixture->assign(array('test'=>'testTwig123'));
$this->assertEquals('testTwig123', $this->fixture->fetch('testTwig','html'));
}
public function testFetch1()
{
$this->fixture->assign(array('test'=>'testTwig123'));
$this->assertEquals('testTwig123', $this->fixture->fetch('testTwig','html'));
}
public function testFetch2()
{
$this->fixture->assign(array('xo'=>'testTwig123'));
$this->assertEquals('', $this->fixture->fetch('testTwig','html'));
}
public function testRender1()
{
$this->expectOutputString('testTwig123');
$this->fixture->assign(array('test'=>'testTwig123'));
$this->fixture->render('testTwig','html');
}
public function testRender2()
{
$this->expectOutputString('');
$this->fixture->assign(array('xo'=>'testTwig123'));
$this->fixture->render('testTwig','html');
}
/**
* @expectedException Twig_Error_Loader
*/
public function testWrongTemplate()
{
$this->fixture->assign(array('test'=>'testTwig'));
$this->fixture->fetch('none', 'xwot');
}
public function teardown()
{
$this->fixture = null;
}
}
除了測試正常操作,也簡單測試一下如果丟給他不存在的template,是否會拋出該有的例外。
接下來檢查一下Model。Model的架構問題其實跟View差不多,所以還是跟View一樣做一下調整。不過細節就不貼出來,反正差不多。
重點還是做一下Model的單元測試:
先寫簡單的Mysql實作的單元測試看看:
<?php
include 'bootstrap.php';
class TestMysqlIndex extends PHPUnit_Framework_TestCase
{
public function testGetForumList()
{
$a = \Fillano\Core\ModelFactory::getInstance('Mysql', 'Index');
$list = $a->getForumList();
$this->assertEquals(array('id'=>1,'name'=>'論壇一', 'count'=>2, 'title'=>'論壇一,文章二'), $list[0]);
}
}
然後執行測試:
Feng-Hsu-Pingteki-MacBook-Air:4-1a fillano$ ./phpunit tests/TestMysqlIndex.php
PHPUnit 3.7.28 by Sebastian Bergmann.
.
Time: 19 ms, Memory: 9.50Mb
OK (1 test, 1 assertion)
看起來沒問題。然後再來做PDO實作的部份:
<?php
include 'bootstrap.php';
class TestPDOIndex extends PHPUnit_Framework_TestCase
{
public function testGetForumList()
{
$a = \Fillano\Core\ModelFactory::getInstance('PDO', 'Index');
//echo print_r($a->getForumList(), true);
$list = $a->getForumList();
$this->assertEquals(array('id'=>1,'name'=>'論壇一', 'count'=>2, 'title'=>'論壇一,文章二'), $list[0]);
}
}
然後跑一下測試,這是最簡單的了...
Feng-Hsu-Pingteki-MacBook-Air:4-1a fillano$ ./phpunit tests/TestPDOIndex.php
PHPUnit 3.7.28 by Sebastian Bergmann.
F
Time: 23 ms, Memory: 9.50Mb
There was 1 failure:
1) TestPDOIndex::testGetForumList
Failed asserting that two arrays are equal.
--- Expected
+++ Actual
@@ @@
Array (
- 'id' => 1
+ 'id' => '1'
'name' => '論壇一'
- 'count' => 2
- 'title' => '論壇一,文章二'
+ 'count' => '2'
+ 'title' => null
)
/Users/fillano/builds/ironman6/4-1a/tests/TestPDOIndex.php:11
FAILURES!
Tests: 1, Assertions: 1, Failures: 1.
疑,怎麼錯了...也看一下畫面:
發現最新文章沒出來...
檢查一下PDOIndex.php,發現有地方寫錯了...應該這樣寫:
<?php
namespace Fillano\Models;
class PDOIndex implements IIndex
{
private $pdo;
public function __construct($pdo)
{
$this->pdo = $pdo;
}
public function getForumList()
{
$sql = "SELECT f.*,count(a.forums_id) AS count "
."FROM forums f "
."LEFT JOIN articles a ON a.forums_id = f.id "
."GROUP BY a.forums_id "
."ORDER BY f.id"
;
$forums = $this->pdo->query($sql);
$data = array();
$sql = "SELECT * "
."FROM articles "
."WHERE forums_id=:forum_id "
."ORDER BY create_time DESC LIMIT 1";
$stmt = $this->pdo->prepare($sql);
foreach($forums as $forum) {
if($stmt->execute(array(":forum_id"=>$forum['id']))) {
$article = $stmt->fetch();
}
$data[] = array('id'=>$forum['id'], 'name'=>$forum['name'], 'count'=>$forum['count'], 'title'=>$article['title']);
}
return $data;
}
}
之前直接把execute的結果assign給$article了,execute只是執行查詢,結果要用fetch來取回才對。改完再測試一下:
Feng-Hsu-Pingteki-MacBook-Air:4-1a fillano$ ./phpunit tests/TestPDOIndex.php
PHPUnit 3.7.28 by Sebastian Bergmann.
.
Time: 18 ms, Memory: 9.50Mb
OK (1 test, 1 assertion)
好,成功。
======
之前只用眼睛看一下瀏覽器畫面,果然還是會漏掉。這種情況,使用單元測試是不會被放過的。因為時間不夠,所以單元測試只是簡單的示範,實際上最好測試正確、錯誤以及一些邊界條件,也最好參考一下測試覆蓋率,看看是否有程式中的一些條件分支漏掉了。
單元測試也是檢驗設計的一個方法,簡單地說,如果寫出來的東西很難測試,通常也有架構的問題,最好想一下怎樣調整讓他可以測試,通常經過調整以後,架構上的彈性會更好。