iT邦幫忙

DAY 28
11

逐步提昇PHP技術能力系列 第 28

逐步提昇PHP技術能力 - 逐步改善軟體架構 - 單元測試

從無法進行單元測試的「古典」程式開始做架構改善,現在已經可以做單元測試了。所以還是先把單元測試做一下。這樣未來有程式的變動,或是進一步調整,都可以透過測試做一些品質的保證,也可以提早發現問題,不必等到程式開始在伺服器執行。
雖然最近看到phptestr有漂亮的介面,寫測試好像也蠻簡單,不過還是先用比較熟悉的phpunit來做。

首先,當然還是要先架構測試環境。先透過composer把phpunit裝起來,所以在composer.json中加上require-dev:

{
    "require": {
        "twig/twig":"1.*"
    },
    "require-dev": {
        "phpunit/phpunit":"3.7.*"
    }
}

然後執行composer update來安裝。安裝完以後,繼續準備測試環境。首先是準備測試資料庫,只要把原本的資料庫、設定檔、bootstrap等複製一份做出測試資料庫及設定就可以。然後新建一個目錄來放單元測試的程式,就叫tests吧。

然後再考慮一下要測試哪些部分...如果是使用現成的framework,並不需要針對framework做測試。不過因為這次是自己土砲的架構,所以包含架構最好也做一下測試。這樣需要做的大概包括幾個部分:

  1. View架構
  2. Model架構
  3. DAO類別

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.

疑,怎麼錯了XD...也看一下畫面:

發現最新文章沒出來...

檢查一下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)

好,成功。

======

之前只用眼睛看一下瀏覽器畫面,果然還是會漏掉。這種情況,使用單元測試是不會被放過的。因為時間不夠,所以單元測試只是簡單的示範,實際上最好測試正確、錯誤以及一些邊界條件,也最好參考一下測試覆蓋率,看看是否有程式中的一些條件分支漏掉了。

單元測試也是檢驗設計的一個方法,簡單地說,如果寫出來的東西很難測試,通常也有架構的問題,最好想一下怎樣調整讓他可以測試,通常經過調整以後,架構上的彈性會更好。


上一篇
逐步提昇PHP技術能力 - 逐步改善軟體架構 - 轉換到PDO
下一篇
逐步提昇PHP技術能力 - 逐步改善軟體架構 - 寫一個簡單的Controller
系列文
逐步提昇PHP技術能力30

1 則留言

我要留言

立即登入留言