読者です 読者をやめる 読者になる 読者になる

tanaka's Programming Memo

プログラミングについてのメモ。

PHPUnitを使ったテスト駆動開発(TDD)

TDDとは、プログラムを開発する際に、入出力の動作を決めて、先にテストプログラムを用意してから実際のプログラムを開発する手法。

データベースから取得したレコードをシリアライズして、指定のファイル名に保存するという簡単な機能を実装するのをTDD的な手法で開発してみる。

手順的には自己流がかなり混ざっているので正しいTDDになっているかは分からないが、一応開発できた。

手順

  1. テストのプログラムを書く
  2. 実際のクラスを作成し、関数が未実装を返すように実装
  3. テストが失敗することを確認する
  4. スタブクラスに実際のプログラムを実装して、テストが成功するようにする
  5. 全てのテストにパスしたら、スタブクラスが実装目的だったクラスとして完成

前提

ターミナルでPHPUnitが動作するように設定しておく。差し当たり、ターミナルでコマンド入力で開発を進める。

クラスの機能

  • クラス名はexportRecとする。
  • bool start(ファイル名)で処理開始。
    • フォルダは全て存在していること。指定のフォルダがない場合はfalseを返す。
  • bool export(フェッチした1件分のレコード)で保存実行。
    • startで指定したファイル名の最後にナンバリングをする。
    • 何かエラーが発生したらfalseを返す。

テストクラスを作成する

  • 関数startとexportのテストを実施
  • 事前にテスト用のフォルダを作成する
  • 失敗用のフォルダの確認はしなくてよい。テスト時に間違って成功したらチェックする
  • フォルダがない時の失敗用のチェックを行う

最初のバージョン

exportRecTest.phpの最初のコード

動作する時に成功用のフォルダを作成することと、startのテストを記載。まずはこれだけで動作させるのを目指す。

<?php
/**
 * exportRecのテスト
 */

require_once 'exportRec.php';

class exportRecTest extends PHPUnit_Framework_TestCase
{
	/** 出力先フォルダ*/
	protected static $okdir = "./okdir";
	/** 出力失敗用パス*/
	protected static $ngdir = "./ngdir";
	
	/**
     * 記録開始
	 */
	public function testStart()
	{
		// 失敗テスト
		$test = new exportRec;
		$this->assertFalse($test->start(self::$ngdir));
		
		// 開始成功
		$this->assertTrue($test->start(self::$okdir));
	}
}

?>

exportRec.php

最低限、必要な情報のみを揃えた実装クラスexportRecを用意。start関数のみを宣言し、内容は実装せずにfalseを返すのみにしておく。

<?php
/**
 * データレコードをバイナリに出力するクラス
 */

class exportRec
{
	/**
	 * 指定のファイル名でファイルの作成を開始する
	 * @param string $file ファイルパス。拡張子の前に数字を挿入したファイルを連蔵で出力する準備
	 * @return bool true=成功 / false=失敗
	 */
	public function start($file)
	{
		return false;
	}
}

?>

テスト

phpunitを動かして、テストの結果をチェックする。

phpunit exportRecTest

PHPUnit 4.2.6 by Sebastian Bergmann.

F

Time: 45 ms, Memory: 3.00Mb

There was 1 failure:

1) exportRecTest::testStart
Failed asserting that false is true.

/Users/yutanaka/git/trans-server/tama_ex08/test/exportRecTest.php:25

FAILURES!
Tests: 1, Assertions: 2, Failures: 1.

この時点では、1つ成功して、1つ失敗する。

開始処理を実装

1. フォルダがないことのチェック

exportRec.phpのstart関数を実装。

	public function start($file)
	{
		$this->count = 0;
		// フォルダが存在することを確認する
		if (file_exists(dirname($file))) {
			$this->filename = $file;
			return true;
		}
		
		// 失敗
		return false;
	}

以上で、失敗のテストは正しく動作する。成功のテストは、テスト用のフォルダが見つからないので失敗してしまう。テストプログラムに成功用のフォルダを作成する処理を追加する。

	/**
	 * テスト前の準備。必要なフォルダの作成と、失敗用パスが存在しないことをチェック
	 */
	public static function setUpBeforeClass()
	{
		// 出力用フォルダの作成
		if (!file_exists(dirname(self::$okdir))) {
			mkdir(dirname(self::$okdir));
		}
	}	
	

以上で、startのテストは完了。

exportの実装

最初のテストを記載

実装を開始するために、最小限のテストをテストクラスに追加。

	/**
	 * 出力テスト
	 */
	public function testExport()
	{
		// データの作成を開始
		$test = new exportRec;
		$test->start(self::$okdir);
		
		// データを作成
		$data = array('one','two','三','四');
		$test->export($data);

		// テストが未完成であることを明示する
		$this->markTestIncomplete(
				'このテストは、まだ実装されていません。'
		);
	}

exportRec.phpにexportのスタブ関数を追加

テストを開始するために、trueを返すだけのexportクラスをexportRec.phpに追加。

	/**
	 * 指定のオブジェクトをシリアライズして、ファイル名のカウンタで保存
	 * @param object $data
	 */
	public function export($data) {
		return true;
	}

以上で、テストを実行して通ることを確認する。

exportRecの実装

仕様に従って、プログラムを実装する。exportRecの完成コードは以下の通り。ファイル名と拡張子を取得するコードをstart()に追加している。

<?php
/**
 * データレコードをバイナリに出力するクラス
 */

class exportRec
{
	/** ファイル名のパス*/
	protected $filepath = "";
	/** ファイル名のみ*/
	protected $filename = "";
	/** 拡張子*/
	protected $ext = "";
	
	/** 現在のカウント*/
	protected $count = 0;
	
	/**
	 * 指定のファイル名でファイルの作成を開始する
	 * @param string $file ファイルパス。拡張子の前に数字を挿入したファイルを連蔵で出力する準備
	 * @return bool true=成功 / false=失敗
	 */
	public function start($file)
	{
		$this->count = 0;
		// フォルダが存在することを確認する
		if (file_exists(dirname($file))) {
			$this->filepath = $file;
			$fname = basename($file);
			$pos = strrpos($fname,'.');
			if ($pos === false) {
				$this->filename = $fname;
				$this->ext = "";
			}
			else {
				$this->filename = substr($fname,0,$pos);
				$this->ext = substr($fname,$pos);
			}
			
			return true;
		}
		
		// 失敗
		return false;
	}
	
	/**
	 * 指定のオブジェクトをシリアライズして、ファイル名のカウンタで保存
	 * @param object $data 書き込む配列やオブジェクト
	 * @return true=書き出し成功 / false=失敗
	 */
	public function export($data) {
		$fname = dirname($this->filepath)."/".$this->filename.$this->count.$this->ext;
		$handle = fopen($fname,"wb");
		if ($handle === false) {
			return false;
		}
		fwrite($handle,serialize($data));
		fclose($handle);

		$this->count++;
		
		return true;
	}
}

?>

テストは、1件の未実装の報告のみで成功。

実行結果をテストする

テストプログラムを記載する。未実装の報告コードを外して、assertStringEqualsFile()を使って、作成に使ったオブジェクトと保存したファイルを比較する。

	/**
	 * 出力テスト
	 */
	public function testExport()
	{
		// データの作成を開始
		$test = new exportRec;
		$test->start(self::$okdir);
		
		// データを作成
		$data = array('one','two','三','四');
		$test->export($data);
		
		// ファイルと文字列の比較
		$this->assertStringEqualsFile('./okdir/data0.bin', serialize($data));


		// テスト2。nullや改行、2番目のファイルとしてナンバリングのチェック
		$data = array(null,1,'2','三\n四');
		$test->export($data);
		
		// ファイルと文字列の比較
		$this->assertStringEqualsFile('./okdir/data1.bin', serialize($data));
	}


以上で開発完了。