PHPでテスト (PHPUnit / DBUnit) を書く前にする事とか依存性の注入とか

投稿日:

最近凄く寒くなってきて、こういった本格的な冬までの移行期間に何を着たらいいのかわからないというか適切な洋服を持っていなくていつも困ります。
軽く羽織れてなおかついい感じの洋服が欲しいです。

今回は自分がPHPでテスト (PHPUnit / DBUnit) を書く前に、
まずテストが書ける様に行っていること
を書き連ねてみます。

例えば既存ソースの修正があって、それにはテストが無かったとします。
テストを書かないのが許されるのなんて小学生までだよね~、そうだよね~、よしテスト書くぞとなります。
で、ソースを見ます。

テスト対象

あくまで例なのです。

<?php

namespace App;

use App\Database;

class Something
{
    /**
     * コンストラクタ
     */
    public function __construct()
    {
    }

    /**
     * 何かしらの登録処理
     * @param array $params
     * @return boolean
     * @throws \InvalidArgumentException
     * @throws \Exception
     */
    public function register(array $params)
    {
        $validation = new Validation();

        if ($validation->run($params) === false) {
            throw new \InvalidArgumentException('引数エラー');
        }

        try {
            $pdo = Database\Pdo::forge();
            $pdo->beginTransaction();

            $register = new Register();
            foreach ($params as $param) {
                $register->execute($param);
            }

            $pdo->commit();

        } catch (\Exception $e) {
            if ($pdo instanceof \PDO) {
                $pdo->rollback();
            }

            throw $e;
        }

        return true;
    }
}

このSomethingクラスは、

  • 登録処理の取りまとめ役
  • register() メソッドにパラメータを渡すと
    • Validationクラスを使ってバリデーションを行う
      • バリデーションにひっかかったら \InvalidArgumentException を投げる
    • Registerクラスにパラメータを渡してDBに登録
      • トランザクションを張っている
      • 登録処理(insert)に成功すればcommitしてtrueを返す
      • 登録処理(insert)に失敗すればrollbackをして\Exceptionを投げる

を行うようです。
そして、上記のSomething::register()に改修をいれたいとします。

テストケース

冒頭にあったように、まず改修を入れる前にテストを書きたい (現状の動きを把握/保障したい) のでテストケースを挙げてみると、
※ 仕様化テストっていうのかな

  • register() メソッドに変なパラメータを渡すと 例外(\InvalidArgumentException) が発生してほしい
    • DBにも変化が無いこと
  • 登録処理で失敗したらDBはrollbackされて 例外(\Exception) が発生してほしい
  • 登録処理に成功したらきちんとDBにcommitされていてtrueが返ってきてほしい

大体こんなケースを思いつくかと思います。

だがこれだと辛い

で、分かる人は分かると思うのですがこれだと辛い。
なにが辛いかというと、

  • Validation
  • Register

上記2つの依存するクラスをこちらでコントロールできないという部分です。
もちろん直接修正すればテストできますが、テストするたびに書き換えないといけません。
それはさすがにめんどいです。
Database\PdoPDOをそのまま使っているだけなので、今回は割愛します(本当はこいつも外部から注入すべき)

ではどうするか

こんなとき自分はどうするかというと、

<?php

namespace App;

use App\Database;

/**
 * Something
 */
class Something
{
    /**
     * コンストラクタ
     */
    public function __construct()
    {
    }

    /**
     * 何かしらの登録処理
     * @param array $params
     * @return boolean
     * @throws \InvalidArgumentException
     * @throws \Exception
     */
    public function register(array $params)
    {
        // オブジェクトの生成を別メソッドで
        $validation = $this->createValidation();

        if ($validation->run($params) === false) {
            throw new \InvalidArgumentException('引数エラー');
        }

        try {
            $pdo = Database\Pdo::forge();
            $pdo->beginTransaction();

            // オブジェクトの生成を別メソッドで
            $register = $this->createRegister();
            foreach ($params as $param) {
                $register->execute($param);
            }

            $pdo->commit();

        } catch (\Exception $e) {

            if ($pdo instanceof \PDO) {
                $pdo->rollback();
            }

            throw $e;
        }

        return true;
    }

    /**
     * \App\Validation生成
     * @return \App\Validation
     */
    protected function createValidation()
    {
        return new Validation();
    }

    /**
     * \App\Register生成
     * @return \App\Register
     */
    protected function createRegister()
    {
        return new Register();
    }
}

こんな感じでオブジェクトの生成を別メソッドに出してしまいます。

こうすると何がうれしいのか

こうすると何がうれしいかというと、mockが使えます。
例えば、上記にあったこちらのテストケースは

  • register() メソッドに変なパラメータを渡すと 例外(\InvalidArgumentException) が発生してほしい
    • DBにも変化が無いこと

こんな感じで書けます。

/**
 * validationでエラーになったらInvalidArgumentException
 * @test
 */
public function validationでエラーになったらInvalidArgumentException()
{
    // Validationクラスのモック
    $validationMock = $this->getMockBuilder('App\Validation')
                            ->setMethods(['run'])
                            ->getMock();
    // runメソッドでfalseを返すようにする
    $validationMock
        ->expects($this->once())
        ->method('run')
        ->willReturn(false)
        ;

    // テスト対象クラスを上記で作ったモックを返却するモックにする
    $targetMock = $this->getMockBuilder('App\Something')
                        ->setMethods(['createValidation', 'createRegister'])
                        ->getMock();
    $targetMock
        ->expects($this->once())
        ->method('createValidation')
        ->willReturn($validationMock)
        ;

    // createRegisterは呼ばれていないこと
    $targetMock
        ->expects($this->never())
        ->method('createRegister')
        ->willReturn('')
        ;

    // 期待する例外
    $expected_error_message = '引数エラー'; // 例外時のメッセージ
    $this->setExpectedException(
        '\InvalidArgumentException', $expected_error_message
    );

    // 実行
    $targetMock->register([]); // 引数はarrayであればなんでもいい
}

ポイントとしては、

  • App\Validationクラスをmockにする
    • runメソッドでfalseを返すようにする
  • App\Somethingクラスもmockにする
  • App\Something::createValidation()で返却されるApp\Validationクラスを上記App\Validationのmockが返却されるようにする

というところです。
※ createRegisterが呼ばれていないこともテストしています
(このメソッドが呼ばれない限り更新処理は行われないはず)

こうすれば、App\Something::register()内でApp\Validation::run()が呼ばれたらfalseが返されて、正しく\InvalidArgumentExceptionが発生していることが確認できます。

他のテストケースも上手くmockを使えばテストが書けると思います。
既存のソースコードに対してテストが書ければ、後は安心して新しい実装を組み込むことが出来ます。

あと、mockにしておけばValidationクラスやRegisterクラスの中身が未完成でもインターフェイスだけ決まっていればテストを書くことが出来ます。
※ 実際、Validationクラスのrunメソッドは未実装のままテストを書きました

上記サンプルのクラスとテストはこちらです。
shimabox/php-before-dependency-injection

参考 : PHPUnit マニュアル – 第8章 データベースのテスト

依存性の注入

上記のような、あるクラスを動かす上で他のクラスが必要なことをAはBに依存するとか言ったりします。
で、依存するクラスは渡せるようにしちゃおうよっていうのが依存性の注入だと自分では認識しています。

ただ上記の例だと、依存性の注入をしているわけではなくて依存している部分を別メソッドに抜き出した?形です。
こういうの名前があるのかよくわかりませんが、

に近いのかな。。(これの Creation Method に該当するのかなぁ)

本当だったらコンストラクタで渡せるように(コンストラクタインジェクション)したり、

/**
 * @var \App\Validation
 */
protected $validation;

/**
 * @var \App\Register
 */
protected $register;

/**
 * コンストラクタ
 * @param \App\Validation $validation
 * @param \App\Register $register
 */
public function __construct(Validation $validation, Register $register)
{
    $this->validation = $validation;
    $this->register = $register;
}

セッターを用意(セッターインジェクション)するのが、

/**
 * @var \App\Validation
 */
protected $validation;

/**
 * @var \App\Register
 */
protected $register;

/**
 * @param \App\Validation $validation
 */
public function setValidation(Validation $validation)
{
    $this->validation = $validation;
}

/**
 * @param \App\Register $register
 */
public function setRegister(Register $register)
{
    $this->register = $register;
}

ベストなんでしょうけど、このクラスを使っている場所が多いと呼び出し側の修正が大変です。
※ 具象クラスではなくて抽象クラスをタイプヒンティングに使うという手もありますね
※ 抽象クラスを渡す場合、テストでは抽象クラスを実装したテスト用クラスを渡す. みたいに出来ます

そのため、一旦こんな形で呼び出し側が変更しないで済むような修正を自分はします。
(でも、ここまで書けたら呼び出し側の修正もチマチマと出来ますよね)

それと、まず実装をガガガとしちゃう場合も上記の様に依存部分を別出しにしておくとテストが書きやすくなります。
※ あとでテスト書けるという安心感も大事

あといくら依存性の注入といっても、10個も20個も依存するようなクラスがあるとそれはそれで設計に問題があると思いますので、なるべく依存するクラスの数が少なくなるように心がけています。
そしてそれらを組み合わせる。みたいな。
(気づいたらクラスがめちゃくちゃ増えるし、かえってわかりづらくなるという問題もあるので、、、なんですけども)

結合度は低めで凝集度は高く、うーーーーーーーん。難しいなぁ。。。。

その他

コンストラクタでオブジェクト生成メソッドを使いたいケースも、もしかしたらあるかもしれません。※さぼりたい
こんな感じ。

/**
 * @var \App\Validation
 */
protected $validation;

/**
 * @var \App\Register
 */
protected $register;

/**
 * コンストラクタ
 */
public function __construct()
{
    $this->validation = $this->createValidation();
    $this->register = $this->createRegister();
}

でも、これだと上手くいきません。
自分が軽く調べた限り、getMock()した瞬間にコンストラクタが呼ばれて、そのタイミングだとメソッドをモック化(ここではcreateValidationcreateRegister)していてもnullが返るからです。
※ 認識違いでしたらすいません

こういうケースの場合はおとなしく、依存性の注入を行ったほうがいいと思いますし、mockしたいクラスは、コンストラクタで余計なことをしていないクラスがよい です。
mockしたいクラスに限らず、基本的にコンストラクタで余計なことはしないほうがいいと個人的には思います。
(依存しているクラスを直接newしていたりとか)

それと、直接DBを触るテストは書かないほうがいいとかDBを弄る部分はmockを使えみたいに言われる場合が多いですが、どうしても書きたい時もあるというか、自分はテスト用のスキーマを用意して書いちゃうことのほうが多いです。

まとめ

ポエムになった。

作成者: shimabox

Web系のプログラマをやっています。 なるべく楽しく生きていきたい。

コメントする

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください