2004年11月18日

PHPUnitの使い方

PHPUnitは、オブジェクトのテストにだけ使えるのかと思っていましたが、単純な関数の単体テストでも有効なんですね。(ドキュメントのサンプルは、関数のテストが載ってますが)

とういわけで、PHPUnitの単純な使い方を紹介します。

PHPUnitの使い方を参考にしました。(というかそのまんまです。すみません。というか、ありがとうございます。)


■1. 何をテストするか?
スクリプトにアクセスしたユーザのログをファイルに記録する関数をテストすることにします。

bool loginLogWriter(string user)

userを引数にとり、ファイルに、user、アクセス日時を書き込みます。
user、アクセス日時は、1回のアクセスに付き、1行をファイルの最後にタブ区切りで追記します。
書き込みが正常に終了した場合に、trueを返します。失敗した場合は、falseを返します。


何をテストするかを考えていくときりがありませんが、
タブ区切り、ファイル書込ということで、次の項目をテストすることにします。


  1. 引数で渡されたuserが正しく記録されていること。

  2. userにタブが含まれていても正しく記録されること。

  3. 書き込むファイルが存在しない場合にfalseを返すこと。

■2. テストのスケルトンの作成

まず、始めに(テストする関数を作る前に)、スケルトンを作ります。

test.php

<?php
require_once 'PHPUnit/GUI/HTML.php';
// Test Suites
require_once 'test/loginLogWriterTest.php'; //←テストが書かれたスクリプト

// run Test Suites
$suite = new PHPUnit_TestSuite("loginLogWriterTest"); //←テストするクラス名(1)
$result = new PHPUnit_GUI_HTML($suite);
$result->show();
?>


test/loginLogWriterTest.php

<?php
require_once 'PHPUnit.php';
require 'loginLogWriter.php'; //←テストするスクリプトをインクルード

class loginLogWriterTest extends PHPUnit_TestCase { //←(1)のテストするクラス名
    function loginLogWriterTest($name) //←コンストラクタ
    {
        $this->PHPUnit_TestCase($name);
    }

    function setUp()
    {
    }

    function tearDown()
    {
    }

    function testSetup() //←testで始まるメソッドがテストとして実行されます。
    {
        $this->fail("no implementation"); //←assertのメソッドについては、PHPUnit_Assertを参照
    }
}
?>

loginLogWriter.php

<?php
?>

とりあえずファイルだけ作ります。

ここまでで、とりあえず、test.phpを実行してみます。
(un)check all にチェックをつけて、run testsをクリックします。

failメソッドで、強制的に失敗させていますので、失敗していれば(橙色になっていれば)成功です。


■3. テストの作成
関数を作成する前に、まず、テストを作成します。
「1. 引数で渡されたuserが正しく記録されていること。」から作ります。


test/loginLogWriterTest.php

<?php
require_once 'PHPUnit.php';
require 'loginLogWriter.php';

class loginLogWriterTest extends PHPUnit_TestCase {
    var $logFile;
    function loginLogWriterTest($name)
    {
        $this->PHPUnit_TestCase($name);
    }

    function setUp() //←テストの前に呼び出されます
    {
        $this->logFile = "/tmp/test_log.log";
        if ( !file_exists($this->logFile) ) {
            touch($this->logFile); //←ログを書き出すファイルを生成しています
        }
    }

    function tearDown() //←テストの終了後に呼び出されます
    {
        unlink($this->logFile); //←生成したファイルを削除しています
    }

    function testWriteLog() //←testで始まるメソッドがテストとなります
    {
        $user = "hoge";
        $regexp = "|^".date("Y/m/d")."\t".date("H:i:").".*".$user."\t\n$|";
        loginLogWriter($user);
        $out = file($this->logFile); //←ファイルの内容を配列に取り出します
        $this->assertRegExp($regexp, $out[count($out)-1]); //←正規表現を使って出力結果をチェックします
    }
}
?>

ここまでで、テストを実行します。

まだ、loginLogWriter()を作成していないので、エラーになります。


■4. 関数の作成

まず、関数の定義だけを記述します。

loginLogWriter.php

<?php
function loginLogWriter($user){
}
?>

テストを実行します。

まだ、処理を書いていないので、エラーになりました。
テストスクリプトのエラーも出ていますが、気にしないでおきましょう。

さて、いよいよ、実際の処理を作ります。

loginLogWriter.php

<?php
function loginLogWriter($user){
    $logFile = "/tmp/test_log.log";
    $logData = array(date("Y/m/d"), date("H:i:s"), $user, "\n");
    $logData = implode("\t", $logData); //←区切りはタブとします
    $fp = fopen($logFile,"a");
    flock($fp,2);
    fwrite($fp, $logData);
    fclose($fp);
}
?>

テストを実行します。


■5. 次のテストです。
「2. userにタブが含まれていても正しく記録されること。」

同様に、テストを作ります。

test/loginLogWriterTest.php

<?php
require_once 'PHPUnit.php';
require 'loginLogWriter.php';

class loginLogWriterTest extends PHPUnit_TestCase {
    var $logFile;
    function loginLogWriterTest($name)
    {
        $this->PHPUnit_TestCase($name);
    }

    function setUp()
    {
        $this->logFile = "/tmp/test_log.log";
        if ( !file_exists($this->logFile) ) {
            touch($this->logFile);
        }
    }

    function tearDown()
    {
        unlink($this->logFile);
    }

    function testWriteLog()
    {
        $user = "hoge";
        $regexp = "|^".date("Y/m/d")."\t".date("H:i:").".*".$user."\t\n$|";
        loginLogWriter($user);
        $out = file($this->logFile);
        $this->assertRegExp($regexp, $out[count($out)-1]);
    }

    function testReplaceTab() //←追加したテストです
    {
        $user = "hoge\thoge";
        $repUser = str_replace("\t", "  ", $user); //←タブを空白2個に置換します
        $regexp = "|^".date("Y/m/d")."\t".date("H:i:").".*".$repUser."\t\n$|";
        loginLogWriter($user);
        $out = file($this->logFile);
        $this->assertRegExp($regexp, $out[count($out)-1]);
    }
}
?>

テストを実行します。

エラーになります。


次に、関数の処理を修正します。

loginLogWriter.php

<?php
function loginLogWriter($user){
    $logFile = "/tmp/test_log.log";
    $user = str_replace("\t", "  ", $user); //←追加しました
    $logData = array(date("Y/m/d"), date("H:i:s"), $user, "\n");
    $logData = implode("\t", $logData);
    $fp = fopen($logFile,"a");
    flock($fp,2);
    fwrite($fp, $logData);
    fclose($fp);
}
?>

テストします。

緑色のバーになれば、テスト完了です。

■6. 最後のテストです
「3. 書き込むファイルが存在しない場合にfalseを返すこと。」

同じ手順です。
テストを作ります。

test/loginLogWriterTest.php

<?php
require_once 'PHPUnit.php';
require 'loginLogWriter.php';

class loginLogWriterTest extends PHPUnit_TestCase {
    var $logFile;
    function loginLogWriterTest($name)
    {
        $this->PHPUnit_TestCase($name);
    }

    function setUp()
    {
        $this->logFile = "/tmp/test_log.log";
        if ( !file_exists($this->logFile) ) {
            touch($this->logFile);
        }
    }

    function tearDown()
    {
        unlink($this->logFile);
    }

    function testWriteLog()
    {
        $user = "hoge";
        $regexp = "|^".date("Y/m/d")."\t".date("H:i:").".*".$user."\t\n$|";
        loginLogWriter($user);
        $out = file($this->logFile);
        $this->assertRegExp($regexp, $out[count($out)-1]);
    }

    function testReplaceTab()
    {
        $user = "hoge\thoge";
        $repUser = str_replace("\t", " ", $user);
        $regexp = "|^".date("Y/m/d")."\t".date("H:i:").".*".$repUser."\t\n$|";
        loginLogWriter($user);
        $out = file($this->logFile);
        $this->assertRegExp($regexp, $out[count($out)-1]);
    }

    function testFileExists() //←追加しました
    {
        unlink($this->logFile);
        $user = "hoge";
        $this->assertFalse(loginLogWriter($user));
    }
}
?>

テストを実行します。

テストが失敗することを期待したのですが、緑色のバーになり成功しました。
確認のために、関数に戻り値trueを入れてみます。

loginLogWriter.php

<?php
function loginLogWriter($user){
    $logFile = "/tmp/test_log.log";
    $user = str_replace("\t", "  ", $user);
    $logData = array(date("Y/m/d"), date("H:i:s"), $user, "\n");
    $logData = implode("\t", $logData);
    $fp = fopen($logFile,"a");
    flock($fp,2);
    fwrite($fp, $logData);
    fclose($fp);
    return true; //←追加しました
}

テストします。

橙色になり、失敗しました。関数は、正常に実行されているようです。
ログを書き込むファイルを削除したはずなのに、と考えていると思い出しました。
fopenは、オープンするファイルがない場合には、ファイルを作成します。

テストが失敗の状態ですので、関数の処理を修正して、テストが成功することを確認します。

loginLogWriter.php

<?php
function loginLogWriter($user){
    $logFile = "/tmp/test_log.log";
    if ( file_exists($logFile) ) {
        $user = str_replace("\t", "  ", $user);
        $logData = array(date("Y/m/d"), date("H:i:s"), $user, "\n");
        $logData = implode("\t", $logData);
        $fp = fopen($logFile,"a");
        flock($fp,2);
        fwrite($fp, $logData);
        fclose($fp);
        return true;
    }
    else {
        return false;
    }
?>

テストを実行します。今度は成功しました。


このように、テストの作成→テストの失敗の確認→処理の作成→テストの成功の確認 を何度も繰り返して、プログラムを作成していけばいいのではないでしょうか。

Posted by hosco at 2004年11月18日 22:44