リスティング広告代行の専門会社|Yahoo!プロモーション広告・Google AdWords完全対応|東京・大阪・名古屋

PHP ファイルシステムを操作するクラスのテスト


ディレクトリ・ファイルのコピーや消去などをするメソッドのテストってどうしていますか?

先日そんなクラスを作ってみて、ユニットテストはどう作ったらいいのか迷いました。
今回は、いくつか実践してみた方法を紹介したいと思います。

<お題>任意のディレクトリにファイルをコピーするメソッドのテストを行う

環境はMac OS X Mavericks、PHP5.4、PHP Unit3.7です。

その1.ユニットテスト終了後にフォルダを消す

最初に書いたユニットテストはこのような感じでした。
テスト出力用のディレクトリを作って実際にコピーする方法です。
テスト出力用ディレクトリはtearDownAfterClass()でユニットテスト終了後に削除します。

class FileCopierClassTest extends \PHPUnitFrameworkTestCase
{
    /**
     * @var string
     */
    private $outputDirectory;

    /**
     * 最初のテストメソッドを実行する前に、ディレクトリを初期化する
     */
    public function setUpBeforeClass()
    {
        $this->outputDirectory = __DIR__ . '/outputTest';

        if (is_dir($this->outputDirectory)) {
            // サブディレクトリとファイルを削除する
            // ...

            rmdir($this->outputDirectory);
        }

        mkdir($this->outputDirectory);
    }

    /**
     * 最後のテストメソッドを実行した後、ディレクトリを削除する
     */
    public function tearDownAfterClass()
    {
        if (is_dir($this->outputDirectory)) {
            // サブディレクトリとファイルを削除する
            // ...

            rmdir($this->outputDirectory);
        }
    }

    /**
     * @test
     */
    public function コピーのテスト()
    {
        $fileCopier = new FileCopierClass();

        $fileCopier->copy($this->outputDirectory);

        $this->assertTrue(is_file($this->outputDirectory . '/test.txt'));
    }
}

このユニットテストには以下のような問題がありました。

  • テストが異常終了するとディレクトリが残ったままになる
  • パーミッションなど実際のファイルシステムの影響を受ける

その2.vfsStreamを使ってファイルシステムのモックでテストする

phpUnitのマニュアルを見ていたらファイルシステムのモックという方法が載っていました。 次はこの方法で試してみることにしました。

まずcomposerを使ってインストールします。最新バージョンはpackagistで調べてください。

composer.json

"require-dev": {
    "mikey179/vfsStream": "1.*"
}

次に、setUpBeforeClass()でファイルシステムのモックを作成します。

class FileCopierClassTest extends \PHPUnitFrameworkTestCase
{
    public function setUpBeforeClass()
    {
        vfsStreamWrapper::setup('rootdir');
    }

テストを実行します。
ディレクトリ・ファイルへのアクセスはvfsStreamの提供するメソッドを使用します。

/**
 *@test
 */
public function コピーのテスト()
{
    $fileCopier = new FileCopierClass();
    $fileCopier->copy(vfsStream::url('rootdir'));

    $this->assertTrue(vfsStreamWrapper::getRoot()->hasChild('test.txt'));
}

実際のファイルシステムを操作しないので、テストの異常終了やパーミッションを気にせずテストが行えるようになりました。
上記例では単純なファイルコピーのテストしかしていませんが、vfsStreamには他にもいろいろな機能が用意されています。

  • ディレクトリ・ファイルの作成
  • 実際のファイルシステムからのコピー
  • 配列を渡して複雑な構造のディレクトリ(およびファイル)を一括で作成
  • 期待通りのディレクトリ(およびファイル)構造になっているか、配列を渡して一括で検査
  • ディレクトリのクォータ制限
  • テストに使う大容量ファイルを一発作成

詳しくはvfsStream公式マニュアルをご覧ください。

vfsStreamのサンプルコードを紹介


use org\bovigo\vfs\vfsStream;
use org\bovigo\vfs\vfsStreamWrapper;
use org\bovigo\vfs\visitor\vfsStreamStructureVisitor;

class ExcelStyleCopierTest extends \PHPUnitFrameworkTestCase
{
    public function setUp()
    {
        vfsStream::setup('rootdir');
    }

    /**
     * @test
     */
    public function モックしたファイルシステム上にディレクトリを作成する()
    {
        vfsStreamWrapper::getRoot()->addChild(vfsStream::newDirectory('testdir'));
        $this->assertTrue(vfsStreamWrapper::getRoot()->hasChild('testdir'));
    }

    /**
     * @test
     */
    public function モックしたファイルシステム上にファイルを作成する()
    {
        vfsStreamWrapper::getRoot()->addChild(vfsStream::newFile('dir/test.txt'));
        $this->assertTrue(vfsStreamWrapper::getRoot()->hasChild('dir/test.txt'));
    }

    /**
     * @test
     */
    public function パーミッションを指定してディレクトリを作成する()
    {
        vfsStreamWrapper::getRoot()->addChild(vfsStream::newDirectory('testdir', 755));

        $this->assertEquals(755,
            vfsStreamWrapper::getRoot()->getChild('testdir')->getPermissions()
        );
    }

    /**
     * @test
     */
    public function パーミッションを指定してファイルを作成する()
    {
        vfsStreamWrapper::getRoot()->addChild(vfsStream::newDirectory('dir/test.txt', 666));

        $this->assertEquals(666,
            vfsStreamWrapper::getRoot()->getChild('dir/test.txt')->getPermissions()
        );
    }

    /**
     * @test
     */
    public function モックしたファイルシステムのパスを取得する()
    {
        $originalPath = 'rootdir/dir/test.txt';

        // "vfs://..."のパスを取得する
        $url = vfsStream::url($originalPath);

        // "vfs://..."を"rootdir/dir..."に変換する
        $path = vfsStream::path($url);

        $this->assertEquals($originalPath, $path);
    }

    /**
     * @test
     */
    public function 実際のファイルシステムからディレクトリをコピーする()
    {
        // test
        //  |- test.php
        //  |- test.txt
        vfsStream::copyFromFileSystem('/Users/user1/test');

        $this->assertTrue(vfsStreamWrapper::getRoot()->hasChild('test.php'));
        $this->assertTrue(vfsStreamWrapper::getRoot()->hasChild('test.txt'));
    }

    /**
     * @test
     */
    public function 配列を渡してディレクトリとファイルを一括作成()
    {
        $structure = array(
            'parent' => [
                'dir1' => [
                    'test.txt' => 'content',
                    'test.csv' => 'content'
                ],
                'dir2' => [],
                'test.php' => 'content',
            ]
        );

        vfsStream::create($structure);

        $this->assertTrue(vfsStreamWrapper::getRoot()->hasChild('parent/dir1'));
        $this->assertTrue(vfsStreamWrapper::getRoot()->hasChild('parent/dir1/test.txt'));
        $this->assertTrue(vfsStreamWrapper::getRoot()->hasChild('parent/dir1/test.csv'));
        $this->assertTrue(vfsStreamWrapper::getRoot()->hasChild('parent/dir2'));
        $this->assertTrue(vfsStreamWrapper::getRoot()->hasChild('parent/test.php'));

        // setup()の第三引数に配列を渡すとテスト開始時に一括作成してくれます
        // vfsStream::setup('rootdir', null, $structure);
    }

    /**
     * @test
     */
    public function 期待通りの構造になっているか配列と比較してチェック()
    {
        $structure = array(
            'subDirectory' => [
                'dir1' => [
                    'test.txt' => 'content',
                    'test.csv' => 'content'
                ],
                'dir2' => [],
                'test.php' => 'content',
            ]
        );

        vfsStream::create($structure);

        $visitor = new vfsStreamStructureVisitor();
        $visitor->visit(vfsStreamWrapper::getRoot()->getChild('subDirectory'));

        $this->assertEquals($structure, $visitor->getStructure());
    }
}

vfsStreamを使った感想

テストで使ったファイルは全て削除されるので便利でした。vfsStreamのファイルシステム上でphp標準の関数copy()、rmdir()なども使うことができます。

ただし弱点もありchdir(), realpath()などのいくつかの関数がvfsStreamのファイルシステム上で使用できません。
Known Issues

本来テストしたかった関数がissueで「do not work」と記載されていたため、残念ながら別の方法を探すことにしました。

その3.OSのテンポラリディレクトリを使ってテストする

OSのテンポラリディレクトリに出力して、テストが終わったファイルをOSに消してもらおう!という方法です。
OSのテンポラリディレクトリと言っても、WindowsだったらC:\temp、Linux/Unix系だったら/tmp,/var/tmpなどいろいろありますね。どの関数でパスを取得してどこに出力するべきか、さらっと調べてみました。

sys_get_temp_dir

PHP が一時ファイルを保存するデフォルトのディレクトリのパスを返す

  • macの場合/tmp、periodic(定期的なタスク機能)によりデフォルト3日で削除される。
  • OSによっては/tmpの下に自作プログラムからファイルを書き込まない、など流儀があるらしい。

tempnam([$dir], [$prefix])

一意なファイル名を生成する

  • ディレクトリを省略した場合は、システムのテンポラリディレクトリに出力
  • macの場合/private/tmp、再起動すると削除される
  • プレフィクス(Windowsでは3文字)が付けられるため出力元のプログラムが分かりやすい

tmpfile

テンポラリファイルを作成する

  • 書き込み可(w+)のファイルハンドルが返るため続けて書き込みができる

どれを使ったらいいのか?

/var/tmpを返す関数はざっと見たところありませんでした。
/tmpより保存期間が長いようですが、先述したOSの流儀うんぬん...に従うとこちらに出力したほうがいいのかもしれません。

結果、tempnam()を使用することにしました。
個人の領域に書き込むので流儀うんぬん...もなく再起動すると消されるからです。
/private/tmp/[ランダムなファイル名]が作成されるので、テストファイル出力用ディレクトリは自分で作る必要がありそうです。

このようなユニットテストになりました


class FileCopierClassTest extends \PHPUnitFrameworkTestCase
{
    /**
     * @var string
     */
    private $outputDirectory;

    /**
     * 最初のテストメソッドを実行する前に、ディレクトリを初期化する
     */
    public function setUpBeforeClass()
    {
        if ($this->outputDirecotry = tempnam('', 'myProgram_')) {
            unlink($this->outputDirecotry);
            mkdir($this->outputDirecotry);
        }
    }

    /**
     * @test
     */
    public function コピーのテスト()
    {
        $fileCopier = new FileCopierClass();

        $fileCopier->copy($this->outputDirectory);

        $this->assertTrue(is_file($this->outputDirectory . '/test.txt'));
    }
}

まとめ

もっと調べたら、さらにいいユニットテストの方法があるかもしれません。
今回調べたvfsStreamは結局使わずに終わってしまいましたが、とても勉強になりました。
いろいろな手法を知って、分かりやすくスマートなユニットテストをさくっと書けるようになれたらいいなと思います。



業界初のリスティング広告運用総合支援ツール Lisket(リスケット)

無料メルマガ

登録はたったの5秒!一週間分のコラムを毎週月曜にメールでお届けします。

Facebookもチェック