今からお前んちこいよ

ベルリンにて細々とお勉強。

PHPUnitの逆引きメモ - テストコードの小技

f:id:hakopako03:20181203051352j:plain

自分の知識整理も兼ねて PHPUnitの使い方逆引きメモ を作ってみようと思う。
これまでの職場でなんとなく蓄積した知識を書き出してみる。

結局のところ公式ドキュメントを見ればほぼ全ての詳細が書いてある。 基本的な知識、setUp() / tearDown() / assert などについては既知前提とする。
想定しているバージョンは、

  • PHP 7.1.9
  • PHPUnit 7.1

便利機能

テストデータを書く - @dataProvider

準備したデータをテストメソッドに順番に流し込んでくれる。
公式ページ) データプロバイダ

<?php
use PHPUnit\Framework\TestCase;
use Path\To\Something;

class DataTest extends TestCase
{
    /**
     * @dataProvider somethingProvider    <--- ここにデータプロバイダーメソッド名を書くと読み込んでくれる
     */
    public function testSomething($a, $b, $expected)
    {
        $sth = new Something($a, $b);
        $actual = $sth->bar();
        $this->assertSame($expected, $actual);
    }

    public function somethingProvider()
    {
        return [
            'case aaa 1' => ['aaa', 1, 0],
            'case bbb 1' => ['bbb', 1, 1],
            'case aaa 0' => ['aaa', 0, 1],
            'case bbb 0' => ['bbb', 0, 3]
        ];
    }
}

例外のテストを書く - @expectedException

期待される例外をアノテーションに記述すれば例外をテストしてくれる。
公式ページ)例外のテスト

<?php
use PHPUnit\Framework\TestCase;
use Path\To\Something;

class ExceptionTest extends TestCase
{
    /**
     * @expectedException InvalidArgumentException    <--- ここに期待される例外を記述
     */
    public function testException()
    {
        $sth = new Something($a);
    }
}

特定のテストをスキップする - @require / markTestSkipped()

環境によるスキップなどは @require アノテーション で対処できる。
例えばmysqlに繋げない環境などは以下のようになる。
公式ページ)@requires によるテストのスキップ

<?php
use PHPUnit\Framework\TestCase;

/**
 * @requires extension mysqli    <--- メソッドやクラスに必須条件を記述
 */
class DatabaseTest extends TestCase
{
    public function testConnection()
    {
    }
}
?>

とりあえずスキップしたい場合は以下のようにもできる。

<?php
use PHPUnit\Framework\TestCase;

class DatabaseTest extends TestCase
{
    public function testConnection()
    {
        $this->markTestSkipped();
        // 以降のテストは実行されない
    }
}
?>

依存メソッドをダミーに置き換える - Stub / Mock

例えば以下の doSomething() メソッドをテストしたいとする。
AnotherThing オブジェクトをモックに置き換える。
公式ページ) 9. テストダブル — PHPUnit latest Manual

<?php

class Something {
    public doSomething(string $str, AnotherThing $anotherThing) : string 
    {
        if($anotherThing->isAvailableToDo($str)){
            return $str . " did something";
        }
        return "";
    }
}

class AnotherThing {
    public isAvailableToDo(string $str) : bool 
    {
        // check something here ....
        return true;
    }
}
<?php
use PHPUnit\Framework\TestCase;
use Path\To\Something;
use Path\To\AnotherThing;

class SomethingTest extends TestCase
{
    public function testDoSomething()
    {
        // create mock object.
        $mock = $this->createMock(AnotherThing::class);
        $mock->method('isAvailableToDo')->willReturn(true);

        $something = new Something();
        $actual = $something->doSomething('taro-san', $mock);
        $this->assertEquals('taro-san did something', $actual);
    }
}
?>

小技リスト

private/protected メソッドをテストする - Reflect / Extend

例えば以下のメソッドをテストしたいとする。

<?php

class Something {
    private doSomething(string $str) : string 
    {
        return $str . " did something";
    }
}

方法1: Reflect を使って一時的にテストコードからのアクセスを可能にさせる

<?php 
use PHPUnit\Framework\TestCase;
use Path\To\Something;

class ClosedMethodTest extends TestCase
    public function testDoSomething() 
    {
        $sth = new Something();
        $reflectMethod = new ReflectMethod(Something::class, 'doSomething');
        $reflectMethod->accesible(true);
        $actual = $reflectMethod->invoke($sth, 'taro-san');
        $this->assertEquals('taro-san did something', $actual);
    }
}

方法2: 対象クラスを継承したクラスを使う

<?php
use PHPUnit\Framework\TestCase;
use Path\To\Something;

class SomethingForTest extends Something {
    public doSomething(string $str) : string 
    {
        return parent::doSomething($str);
    }
}

class ClosedMethodTest extends TestCase
    public function testDoSomething() 
    {
        $sth = new SomethingForTest();
        $actual = $sth->doSomething('taro-san');
        $this->assertEquals('taro-san did something', $actual);
    }
}

環境にあったテストをする - phpunit.xml

phpunit.xml には色々な機能がある。
ここではphpに関連した項目をいくつかあげる。

<?xml version="1.0" encoding="UTF-8"?>
<phpunit stopOnFailure="false" bootstrap="autoload.php" >
    <testsuites>
        <testsuite name="Project Test Suite">
            <directory>./tests</directory>
        </testsuite>
    </testsuites>
    <php>
        <ini name="display_errors" value="1" />    ------ ① php.ini を上書きする
        <const name="TEST_ENVIRONMENT" value="ci" />    --- ② global constを追加する
        <env name="FILEPATH" value="/path/to/file"/>    --- ③ 環境変数を設定する
    </php>
</phpunit>

これらを設定することで、dev,test(CI環境),stagingなど環境にあったテストを実行させることができる。 主にCI開発などではよくあると思う。

終わりに

「PHPUnitにこういう機能があったな」「こういう書き方があったな」という引き出しを知っておくだけでもテストコードを書く敷居を下げられると思う。