PHPUnitで学ぶユニットテスト超入門

ユニットテストとは

ここでいうユニットテストとは、自動で実行できる、メソッドのテストのことです。

バッチのテスト、Web APIのテスト、ブラウザでのテスト(selenium, Laravel Dusk)とは異なる。また、受け入れテストとも違う(これらは定義は組織によりまちまちだと思いますが)

なぜユニットテストが必要なのか  

簡単に何回もテストが行える

数秒でテストが可能であり、一度作ってしまえば、何回も自動でテストができる。今後そのメソッドに修正が行われる際、想定外のバグ(デグレ)があれば、すぐに判明するので、恐れずに修正が可能となる。

コンストラクやメソッドのインターフェースが綺麗になる

詳細の実装の前にテストを書こうとした場合、そのクラスやメソッドの使う側(クライアント)の視点に立てるため、コンストラクタやメソッドのインターフェースに気を使うようになり、呼び出し易い形となる。

独立性の向上

テスト可能な形になるよう意識するので、ソフトウェアが分離する。分離すればテストしやすくなるのはもちろんのこと、保守性も増す。

独立性のないコード

<?php

function file_loop_with_validator($filename)
{
    $f = fopen($filename, 'r');
    $line = fgets($f);
    while ($line !== false) {

        if (preg_match('/^A.*/', $line)) {
            echo $line;
        } else {
            echo "NG" . PHP_EOL;
        }

        $line = fgets($f);
    }
    fclose($f);
}

$filename = './TestCase.php';
file_loop_with_validator($filename);

独立性のあるコード

<?php

interface Validator {
    public function validate(string $line) : bool;
}

class SpecificValidator implements Validator {

    /**
     * @param string $line
     * @return bool
     */
    public function validate(string $line) : bool{

        if(preg_match('/^A.*/', $line)){
            return true;
        }else{
            return false;
        }
    }
}

function file_loop_with_validator($filename, Validator $validator){

    $f = fopen($filename, 'r');
    $line = fgets($f);
    while($line !== false){
        $result = $validator->validate($line);

        if($result){
            echo $line;
        }else{
            echo "NG" . PHP_EOL;
        }

        $line = fgets($f);
    }
    fclose($f);
}

$filename = './TestCase.php';
file_loop_with_validator($filename, new SpecificValidator());

コードが分離していれば、このように修正箇所がわかりやすくなります(どこからどこまでがバリデーション処理か分かりやすい)。また、今後のテストも楽になります。 もし、分離していないコードであれば、おそらく下記のようなテストになります。

この処理がファイルアップロードの処理だとして、下記2つのテストのどっちが楽でしょうか。

  • 「ファイルをアップロードし、〜テーブルのレコードが〜であることを確認」
    →ファイルをアップロードする管理画面の操作方法とどのテーブルを確認するかの知識が必要

  • ユニットテストでおしまい。
    $this->assertTrue($validator->validate('Abc'));
    $this->assertFalse($validator->validate('abc'));

テストそのものがドキュメントに

コンストラクタやメソッドの引数について、テストが教えてくれる。また、Excel等のドキュメントと異なり、間違えがない。

いやでもDB絡むときどうすんのさ

いくら分離させろと言ってもDBと分離させるのは無理。

DBからデータを取り行くときはスタブを使う

スタブ: 実際のオブジェクトを置き換えて、 設定した何らかの値を (オプションで) 返すようなテストダブルのことを スタブ といいます。

第9章 テストダブルより

DBを更新したりするときはモックを使う

モック: 実際のオブジェクトを置き換えて、 (メソッドがコールされたことなどの) 期待する内容を検証するテストダブルのことを モック といいます。

第9章 テストダブルより

コードを見てみよう!

スタブのコード

<?php

use PHPUnit\Framework\TestCase;

class PointbackTest extends TestCase
{
    /**
     * A basic functional test example.
     *
     * @return void
     */
    public function testポイントバックに成功することを確認()
    {
        // Mediaクラスのスタブを作成
        $media_stub = $this->createMock(Media::class);

        // スタブの設定
        // getPointbackUrlメソッドを実行すると'http://some-media.com/pointback'を
        // 返却するように設定
        // →つまり、わざわざMediaテーブルにアクセスせずにAPIのテストが可能に!
        $media_stub->method('getPointbackUrl')
            ->willReturn('http://some-media.com/pointback');

        $pointback = new Pointback();

        // $stub->doSomething() をコールすると
        // 'foo' を返すようになります
        $this->assertEquals(true, $pointback->pointback($media_stub, array('reward' => 100)));

    }
}

class Media {

    private $media;

    public function __construct($m_id)
    {
        $this->media = Media::where('m_id', $m_id)->first();
    }

    public function getMid(){
        return $this->media->m_id;
    }

    public function getMediaName(){
        return $this->media->name;
    }

    public function getPointbackUrl(){
        return $this->media->url;
    }
}

class Pointback{

    public function __construct()
    {
    }

    public function pointback(Media $media, $request_param){

        // url取得
        $url = $media->getPointbackUrl();


        // urlを元にポイントバック
        // ポイントバックの代わりに$urlを標準出力してみる
        var_dump($url);


        // ポイントバック成功したらtrue
        if(true){
            return true;
        }else{
            return false;
        }
    }
}

モックのコード

<?php

use PHPUnit\Framework\TestCase;

class PointbackTest extends TestCase
{
    /**
     * A basic functional test example.
     *
     * @return void
     */
    public function testポイントバック後にUserTBLの報酬更新が行われることを確認()
    {
        // Mediaクラスのスタブを作成
        $media_stub = $this->createMock(Media::class);

        // スタブの設定
        // getPointbackUrlメソッドを実行すると'http://some-media.com/pointback'を
        // 返却するように設定
        // →つまり、わざわざMediaテーブルにアクセスせずにAPIのテストが可能に!
        $media_stub->method('getPointbackUrl')
            ->willReturn('http://some-media.com/pointback');

        // Userクラスのモックを作成
        // updateReward() メソッドをモックする
        // →updateするような処理があっても、実際はDBアクセスしない!
        $user_mock = $this->getMockBuilder(User::class)
            ->setMethods(['updateReward'])
            ->getMock();

        // updateReward() メソッドが一度だけコールされ、その際の
        // パラメータは文字列 '100' となることを期待
        $user_mock->expects($this->once())
            ->method('updateReward');


        $pointback = new Pointback();

        // $stub->doSomething() をコールすると
        // 'foo' を返すようになります
        $this->assertEquals(true, $pointback->pointback($media_stub, 100, $user_mock));

    }
}

class Media {

    private $media;

    public function __construct($m_id)
    {
        $this->media = Media::where('m_id', $m_id)->first();
    }

    public function getMid(){
        return $this->media->m_id;
    }

    public function getMediaName(){
        return $this->media->name;
    }

    public function getPointbackUrl(){
        return $this->media->url;
    }
}

class Pointback{

    public function __construct()
    {
    }

    public function pointback(Media $media, $reward, $user){

        // url取得
        $url = $media->getPointbackUrl();


        // urlを元にポイントバック
        // ポイントバックの代わりに$urlを標準出力してみる
        var_dump($url);


        // ポイントバック成功したらtrue
        if(true){
            $user->updateReward($user, $reward);
            return true;
        }else{
            return false;
        }
    }
}

class User{
    public function __construct()
    {
    }

    public function updateReward($u_id, $reward){
        $user = User::where('u_id', $u_id)->first();
        $user->reward = $reward;
        $user->save();
    }
}


おすすめ記事
© 2016-2017 Fridles All Rights Reserved.