PHPUnitでPHPスクリプトのテストをして見る。

実際に作ったプログラムで紹介しようと思います。
テスト対象のプログラムはユーザー情報をCSVファイルのアップロードから
一括で登録すると言う管理機能です。
いくつも異常系データがあるのでいちいちCSVファイルを作ってアップロードするのは
面倒です。
なのでPHPUnitを使って


・データのチェックロジックの信頼性
・データのインサートロジックの信頼性


この部分をテストして見ようと思います。
以下、テスト対象となる処理

<?
/**
 * user/user_entry.php
 *
 * ユーザー一括登録機能
 *
 * @access  public
 * @author  歳虫
 * @create  2005/01/15
 **/

// 初期化
include_once('../init.php');

// 認証
if(!isset($_SESSION['_admin_login_id'])){
    print "管理者としてログインしてからアクセスして下さい。";
    exit;
}

class UserEntryCsv {

	var $_tmpl;// テンプレートエンジンクラス格納用
	var $stat;// 実行結果格納用
	var $checkError;// 入力チェック結果のエラー内容を格納(1レコード毎)
	var $insertError;// DBインサート結果のエラー内容を格納(1レコード毎)

	// コンストラクタ
	function UserEntryCsv(){}

	// テンプレートの準備
	function setTemplate(){
		$ac = getDiscreditVariable($_REQUEST["ac"]);

		$this->_tmpl = newTemplate("user_entry_csv.html", true);
		$this->_tmpl->assign('SCRIPT_NAME', $_SERVER['SCRIPT_NAME']);
	}

	// メイン処理
	function execute(){
		if (isset($_REQUEST['mode']) && $_REQUEST['mode'] == 'upload') {

			if (is_uploaded_file($_FILES['csvfile']['tmp_name'])) {

				// ファイル読み込み
				$csv_data = $this->getUploadFile();

				$suc_cnt = 0;
				$result = array();
				for($count=0;$count<count($csv_data);$count++){

					$data = $csv_data[$count];

					// エラーの初期化
					$this->initInputDataError();
					$this->initInsertError();

					// 入力値のチェック
					if(!$this->checkInputData($data)){
						// チェックに引っかかったらエラーを結果に積み上げる。
						$result[$count] = "<font color=\"#FF0000\">".($count+1)."行目 ".$this->getInputDataError()."</font>\n";
					}else{
						// DB Insert
						if (!$this->execInsert($data)) {
							//エラー
							$result[$count] = "<font color=\"#FF0000\">".($count+1)."行目 データの登録に失敗しました。 ".$this->getInsertError()."</font>\n";
						}else{
							$result[$count] = ($count+1)."行目 データを登録しました。\n";
							$suc_cnt++;
						}
					}
				}
				$stat = "処理件数:".$count ."件\n";
				if($suc_cnt > 0){
					$stat .= "登録件数:".$suc_cnt."件\n";
				}
				if(($count-$suc_cnt)>0){
					$stat .= "<font color=\"#FF0000\">エラー件数:" . ($count-$suc_cnt) . "件</font>\n";
				}
				$stat .=  "\n" . implode("",$result);
			} else {
				$stat = "<font color=\"#FF0000\">ファイルのアップロードに失敗しました。</font>\n";
			}

			// 処理結果を格納
			$this->stat = $stat;
		}

		return true;
	}

	function flush(){
		// 処理結果を出力
		$this->_tmpl->assign('stat', nl2br($this->stat));

		// フォームを出力
		$this->_tmpl->assign_def('form');

		// 出力
		$this->_tmpl->flush();
	}

	function getUploadFile(){
		// CSVファイルからデータを取り出す。※取り出しと同じタイミングでデータチェックを行うと機能のファンクション化が出来ないので、まず取り出すだけ。
		$fp = fopen($_FILES['csvfile']['tmp_name'], 'r');
		$count = 0;
		$csv_data = array();
		while ($data = fgetcsv($fp,10240)) {
			if($count > 0){//タイトル行は無視
				foreach($data as $key => $val){
					$data[$key] = (mb_detect_encoding($val) == "SJIS")? mb_convert_encoding($val,"EUC-JP","SJIS") : $val;
				}
				$csv_data[$count-1] = $data;
			}
			$count ++;
		}
		fclose($fp);

		return $csv_data;
	}

	function checkInputData($data){

		$account      = $data[ 0];
		$handle       = $data[ 1];
		$email        = $data[ 2];
		$pass         = $data[ 3];
		$name_family  = $data[ 4];
		$name_given   = $data[ 5];
		$ruby_family  = $data[ 6];
		$ruby_given   = $data[ 7];
		$birth_year   = $data[ 8];
		$birth_month  = $data[ 9];
		$birth_day    = $data[10];
		$zip1         = $data[11];
		$zip2         = $data[12];
		$prefecture   = $data[13];
		$municipality = $data[14];
		$address      = $data[15];
		$gender       = $data[16];
		$mobile_email = $data[17];
		$blog_title   = $data[18];
		$blog_note    = $data[19];
		$template_id  = $data[20];

		/* -------------------- 入力値チェック ここから -------------------- */
		// アカウント判定
		if ($account == '') {
			$err .= "アカウントを入力して下さい。 ";
		} else {
			$accountErr = false;
			if (8 < strlen($account)) {
				$err .= "アカウントが長すぎます。 ";
				$accountErr = true;
			}
			if (preg_match("/^[0-9]+$/", $account)) {
				$err .= "数字のみのアカウントは使えません。 ";
				$accountErr = true;
			} elseif (!preg_match('/^[0-9a-zA-Z\-\_$]+/', $account)) {
				$err .= "アカウントは半角英数字のみです。 ";
				$accountErr = true;
			}
			if (!$accountErr) {
				$sql_res = selectAccount($account);
				if (count($sql_res) != 0) {
					$err .= "既に登録されているアカウントです。 ";
				}
			}
		}

		// ユーザー名判定
		if ($handle == '') {
			$err .= "ユーザー名を入力して下さい。 ";
		} elseif (255 < strlen($handle)) {
			$err .= "ユーザー名が長すぎます。 ";
		}

		// メールアドレス判定
		if ($email == '') {
			$err .= "メールアドレスを入力して下さい。 ";
		} else {
			$emailErr = false;
			if (255 < strlen($email)) {
				$err .= "メールアドレスが長すぎます。 ";
				$emailErr = true;
			} elseif (!check_email($email)) {
				$err .= "メールアドレスが不正です。 ";
				$emailErr = true;
			}
			if (!$emailErr) {
				$sql_res = selectEmail($email);
				if (count($sql_res) != 0) {
					$err .= "既に登録されているメールアドレスです。 ";
				}
			}
		}

		// 名前判定
		$name_family = mb_convert_kana($name_family, 'KV');
		if($name_family == '') {
			$err .= "姓(漢字)を入力して下さい。 ";
		} elseif (255 < strlen($name_family)) {
			$err .= "姓(漢字)が長すぎます。 ";
		}
		$name_given = mb_convert_kana($name_given, 'KV');
		if ($name_given == '') {
			$err .= "名(漢字)を入力して下さい。 ";
		} elseif (255 < strlen($name_given)) {
			$err .= "名(漢字)が長すぎます。 ";
		}
		$ruby_family = mb_convert_kana($ruby_family, 'KVC');
		if( $ruby_family == '') {
			$err .= "姓(カナ)を入力して下さい。 ";
		} elseif (255 < strlen($ruby_family)) {
			$err .= "姓(カナ)が長すぎます。 ";
		} elseif (!mb_ereg("^[ァ-ヶー]+$", $ruby_family)) {
			$err .= "姓(カナ)はカタカナで入力して下さい。 ";
		}
		$ruby_given = mb_convert_kana($ruby_given, 'KVC');
		if( $ruby_given == '') {
			$err .= "名(カナ)を入力して下さい ";
		} elseif (255 < strlen($ruby_given)) {
			$err .= "名(カナ)が長すぎます ";
		} elseif (!mb_ereg("^[ァ-ヶー]+$", $ruby_given)) {
			$err .= "名(カナ)はカタカナで入力して下さい。 ";
		}

		// 生年月日判定(一応)
		$birthErr = false;
		if ($birth_year == '') {
			$err .= "誕生年を入力して下さい。 ";
			$birthErr = true;
		} elseif (!preg_match("/^[0-9]+$/", $birth_year)) {
			$err .= "誕生年は半角数字のみです。 ";
			$birthErr = true;
		}
		if ($birth_month == '') {
			$err .= "誕生月を入力して下さい。 ";
			$birthErr = true;
		} elseif (!preg_match("/^[0-9]+$/", $birth_month)) {
			$err .= "誕生月は半角数字のみです。 ";
			$birthErr = true;
		}
		if ($birth_day == '') {
			$err .= "誕生日を入力して下さい。 ";
			$birthErr = true;
		} elseif (!preg_match("/^[0-9]+$/", $birth_day)) {
			$err .= "誕生日は半角数字のみです。 ";
			$birthErr = true;
		}
		if (!$birthErr) {
			if (!checkdate($birth_month, $birth_day, $birth_year)) {
				$err .= "誕生日の日付が異常です。 ";
			}
		}

		// 郵便番号判定
		if ($zip1 == '') {
			$err.= "郵便番号1を入力して下さい。 ";
		} elseif (!preg_match("/^[0-9]+$/", $zip1)) {
			$err .= "郵便番号1は半角数字のみです。 ";
		} elseif (3 < strlen($zip1)) {
			$err.= "郵便番号1が長すぎます。 ";
		}
		if ($zip2 == '') {
			$err.= "郵便番号2を入力して下さい。 ";
		} elseif (!preg_match("/^[0-9]+$/", $zip2)) {
			$err .= "郵便番号2は半角数字のみです。 ";
		} elseif (4 < strlen($zip2)) {
			$err.= "郵便番号2が長すぎます。 ";
		}

		// 住所判定
		$prefecture   = mb_convert_kana($prefecture, 'KV');
		$municipality = mb_convert_kana($municipality, 'KV');
		$address      = mb_convert_kana($address, 'KV');
		if ($prefecture == '' || $municipality == '' || $address == '') {
			if ($prefecture == ''){
				$err .= "都道府県を入力して下さい。 ";
			}
			if ($municipality == ''){
				$err .= "市区町村を入力して下さい。 ";
			}
			if ($address == ''){
				$err .= "住所を入力して下さい。 ";
			}
		} else {
			if (255 < strlen($municipality)) {
				$err .= "市町村名が長すぎます。 ";
			}
			if (255 < strlen($address)) {
				$err .= "住所が長すぎます。 ";
			}
		}

		// 性別判定
		if ($gender == '') {
			$err .= "性別を入力して下さい ";
		} elseif (!($gender == '0')||($gender == '1')){
			$err .= "性別は「0(男性)」または「1(女性)」のみ入力して下さい。 ";
		}
		/* -------------------- 入力値チェック ここまで -------------------- */

		if($err){
			$this->checkError = $err;
			return false;
		}
		return true;
	}

	function getInputDataError(){
		return $this->checkError;
	}

	function initInputDataError(){
		$this->checkError = "";
	}

	function execInsert($data){

		$account      = $data[ 0];
		$handle       = $data[ 1];
		$email        = $data[ 2];
		$pass         = $data[ 3];
		$name_family  = $data[ 4];
		$name_given   = $data[ 5];
		$ruby_family  = $data[ 6];
		$ruby_given   = $data[ 7];
		$birth_year   = $data[ 8];
		$birth_month  = $data[ 9];
		$birth_day    = $data[10];
		$zip1         = $data[11];
		$zip2         = $data[12];
		$prefecture   = $data[13];
		$municipality = $data[14];
		$address      = $data[15];
		$gender       = $data[16];

		$password_crypt = md5($pass);

		/* -------------------- DB登録処理 ここから -------------------- */
		// 一応インサートする前にもう一度重複チェックして置く。
		$sql_res = selectAccount($account);
		if (count($sql_res) != 0) {
			$stat .= "既に登録されているアカウントです。 ";
		}else{
			// アカウントのチェックに成功したらメールアドレスもチェック
			$sql_res = selectEmail($email);
			if (count($sql_res) != 0) {
				$stat .= "既に登録されているメールアドレスです。 ";
			}
		}

		if(!$stat){// この時点でstatがあるのは問題外
			// ユーザー情報の登録
			$sql =  "INSERT INTO user_data (STATUS,PASS,HANDLE,NAME_FAMILY,NAME_GIVEN,RUBY_FAMILY,RUBY_GIVEN,ACCOUNT,EMAIL,BIRTH_YEAR,BIRTH_MONTH,BIRTH_DAY,GENDER,ZIP,PREFECTURE,MUNICIPALITY,ADDRESS) ".
							"VALUES('0','$password_crypt','$handle','$name_family','$name_given','$ruby_family','$ruby_given','$account','$email','$birth_year','$birth_month','$birth_day','$gender','$zip','$prefecture','$municipality','$address')";
			if (!mysql_query($sql)) {
				$stat .= "user_data へのデータ登録に失敗しました。 ";
			}
		}
		/* -------------------- DB登録処理 ここまで -------------------- */

		if($stat){
			$this->insertError = $stat;
			return false;
		}
		return true;
	}

	function getInsertError(){
		return $this->insertError;
	}

	function initInsertError(){
		$this->insertError = "";
	}
}

$main = new UserEntryCSV;
$main->setTemplate();
$main->execute();
$main->flush();

?>


まー中でなにやってんのかは良く見てもらえればなんとなくわかると思います。
ちょっと急いで作ったのでずいぶん汚いソースです(笑
申し訳ないです・・・


で、コレをテストする処理

<?php

//require_once("phpunit.php");
require_once("PHPUnit.php");        // PEAR版の場合

require_once("./user_entry_csv.php"); // テストする大本のソースファイル

/**
 * user_entry_csvクラスのテストケース
 *
 * テストデータを定義しておく。
 * CSVのアップロードの自動化はまーUnitレベルでは不可能なので
 * CSVのアップロードは成功した前提のテストデータを作って置く。
 * CSVアップロードのテストは切り離してテストする。
 *
 * と、言う事で以上を踏まえるとexecuteメソッドはテスト対象外って事ですね。
 *
 */
//class CartTest extends TestCase
class user_entry_csv_test extends PHPUnit_TestCase // PEAR版の場合
{
	var $testModule_;

	/**
	 * 入力データは正しくないと言う前提
	 */
	var $test_data1 = array(
														"kk611"        ,
														"kk611"        ,
														"hoge611@hoge" ,
														"kk611"        ,
														"姓ほげ"       ,
														"名ほげ"       ,
														"ホゲ"         ,
														"ホゲ"         ,
														"1980"         ,
														"11"           ,
														"20"           ,
														"111"          ,
														"1234"         ,
														"ほげ県"       ,
														"ほげ市"       ,
														"ほげ町"       ,
														"0"            
													);

	/**
	 * これは正しいデータ
	 */
	var $test_data2 = array(
														"kk610"        ,
														"kk610"        ,
														"hoge610@hoge" ,
														"kk610"        ,
														"姓ほげ"       ,
														"名ほげ"       ,
														"ホゲ"         ,
														"ホゲ"         ,
														"1980"         ,
														"11"           ,
														"20"           ,
														"111"          ,
														"1234"         ,
														"ほげ県"       ,
														"ほげ市"       ,
														"ほげ町"       ,
														"0"            
													);

	/**
	 * 入力データは正しくないと言う前提
	 */
	var $test_data3 = array(
														"kk613"        ,
														"kk613"        ,
														"hoge613@hoge" ,
														"kk613"        ,
														"姓ほげ"       ,
														"名ほげ"       ,
														"ホゲ"         ,
														"ホゲ"         ,
														"1980"         ,
														"11"           ,
														"20"           ,
														"111"          ,
														"1234"         ,
														"ほげ県"       ,
														"ほげ市"       ,
														"ほげ町"       ,
														"0"            
													);

	/**
	 * コンストラクタ
	 */
	function user_entry_csv_test($name)
	{
		// 必ず指定するおまじない
		//$this->TestCase($name);
		 $this->PHPUnit_TestCase($name); // PEAR版の場合
	}

	/**
	 * テストの初期化(DB接続などの前処理が必要であれば)
	 * テストメソッド毎にsetUp、tearDownが実行される
	 */
	function setUp()
	{
		$this->testModule_ = new UserEntryCsv();
	}

	/**
	 * テストの終了処理(DB切断などの後処理が必要であれば)
	 * テストメソッド毎にsetUp、tearDownが実行される。
	 * 今回は使用しない
	 */
	function tearDown() {}

	/**
	 * 以下実際のテスト
	 * 「test」で始まるfunction名がテスト対象となる
	 */
	// 入力値の整合性チェックプロセスが正しく実行されているかをテストする。
	function testDataCheckProcess (){

		$test_data = array();
		$test_data[0] = $this->test_data1;
		$test_data[1] = $this->test_data2;
		$test_data[2] = $this->test_data3;

		for($i=0;$i<count($test_data);$i++){
			// 2件目はNG
			if($i == 1){
				$this->assertEquals(false,$this->testModule_->checkInputData($test_data[$i]));
				echo "checkInputData error = ".$this->testModule_->getInputDataError()."<br />";
			}else{
				$this->assertEquals(true,$this->testModule_->checkInputData($test_data[$i]));
			}
		}
	}
	// 入力値のDBインサートが正しく実行されているかをテストする。
	function testDataInsertProcess (){

		$test_data = array();
		$test_data[0] = $this->test_data1;
		$test_data[1] = $this->test_data2;
		$test_data[2] = $this->test_data3;

		for($i=0;$i<count($test_data);$i++){
			// 2件目はNG
			if($i == 1){
				$this->assertEquals(false,$this->testModule_->execInsert($test_data[$i]));
				echo "checkInputData error = ".$this->testModule_->getInsertError()."<br />";
			}else{
				$this->assertEquals(true,$this->testModule_->execInsert($test_data[$i]));
			}
		}
	}
}

/**
 * 全てのテストを実行
 * TestSuiteクラスの引数には、TestCaseクラスのサブクラス名を指定する
$ts = new TestSuite("user_entry_csv_test");
$tr = new TestRunner();
$tr->run($ts);
 */

/**
 * 以下、PEAR版の場合
 */
$ts = new PHPUnit_TestSuite("user_entry_csv_test");
$tr = PHPUnit::run($ts);
echo '<pre>';
echo preg_replace("/( failed:.*)/", "<font color=\"red\"><b>\${1}</b></font>", $tr->toString());
echo '</pre>';
?>


このPHPUnitを使ったPHPテストスクリプトをブラウザから実行すると

testDataCheckProcess ok
testDataInsertProcess ok


と、表示されました。
要するに僕の期待した通りにちゃんと動いてるよって事ですね。


さて、使ってみた感想としては


かなり便利!


って感じですね。


使い方としては

・setUpメソッドでテスト対象のClassをNewして
・test〜と言うメソッドを用意しておく。
・test〜に「$this->assertEquals(期待値,テスト対象のクラスのメソッド);」と書く


以上の3点を含んでいれば言い訳です。
お約束はたったコレだけ。


テストケースとして考えられるのは


・正常パターンの確認
・異常パターンの確認
・限界値チェック
・アタック


こんなもんだと思うんですが
少なくとも、この内の3点はPHPUnitで実現出来ると思います。


是非ゼヒお試しあれ!!


参考URL:http://www.doyouphp.jp/tips/tips_phpunit.shtml