Posted on

PHPUnitのモックオブジェクトの使い方を仕組みから理解する

前回はPHPUnitのメイン処理を確認しました。今回はPHPUnitデフォルトのモックオブジェクトの仕組みを確認してみます。公式ドキュメントでは、第9章 テストダブルが該当箇所となります。
20161019203514

PHPUnitのモックオブジェクトについて

PHPUnitは以下のような構成ですが、その中の「phpunit-mock-objects」がPHPUnitデフォルトのモックライブラリとなります。

  • phpunit
  • php-code-coverage
  • php-file-iterator
  • php-text-template
  • php-timer
  • php-token-stream
  • phpunit
  • phpunit-mock-objects ← これ

構成

PHPUnitモックオブジェクトのファイル構成は以下の通りです。

├── Builder
│   ├── Identity.php
│   ├── InvocationMocker.php
│   ├── Match.php
│   ├── MethodNameMatch.php
│   ├── Namespace.php
│   ├── ParametersMatch.php
│   └── Stub.php
├── Exception
│   ├── BadMethodCallException.php
│   ├── Exception.php
│   └── RuntimeException.php
├── Generator
│   ├── deprecation.tpl.dist
│   ├── 省略...
├── Generator.php
├── Invocation
│   ├── Object.php
│   └── Static.php
├── Invocation.php
├── InvocationMocker.php
├── Invokable.php
├── Matcher
│   ├── AnyInvokedCount.php
│   ├── AnyParameters.php
│   ├── ConsecutiveParameters.php
│   ├── Invocation.php
│   ├── InvokedAtIndex.php
│   ├── InvokedAtLeastCount.php
│   ├── InvokedAtLeastOnce.php
│   ├── InvokedAtMostCount.php
│   ├── InvokedCount.php
│   ├── InvokedRecorder.php
│   ├── MethodName.php
│   ├── Parameters.php
│   └── StatelessInvocation.php
├── Matcher.php
├── MockBuilder.php
├── MockObject.php
├── Stub
│   ├── ConsecutiveCalls.php
│   ├── Exception.php
│   ├── MatcherCollection.php
│   ├── Return.php
│   ├── ReturnArgument.php
│   ├── ReturnCallback.php
│   ├── ReturnReference.php
│   ├── ReturnSelf.php
│   └── ReturnValueMap.php
├── Stub.php
└── Verifiable.php

今回は中でも主要処理のあるGenerator、MockBuilder、InvocationMockerをチェックしました。

スタブとモックオブジェクトについて

モックオブジェクトを確認する前に、スタブとモックオブジェクトについて簡単に整理しておきます。PHPUnitの公式ドキュメントでは以下のように記載しています。
スタブ

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

モックオブジェクト

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

違いがちょっとわかりにくいですが、スタブもモックオブジェクトもテストの為の代用品(テストダブル)であることに変わりはありません。では何が違うのか。それは使われ方によって変わってきます。(※個人的な見解も含んでいます。)
テストをする際には、必要に応じてオブジェクトをモック化する必要があり、その際メソッドを握りつぶし何らかの振る舞いを設定するのですが、そのテストの代用品自体がスタブとなります。モックオブジェクトは、モック化したスタブオブジェクトをテスト対象クラスに差し込み、差し込んだモックオブジェクトが想定通り使われているかを確認する為に使うことでモックオブジェクトとなります。
言葉だけだとイメージしにくいので図にしてみました。
スタブ
%e3%82%b9%e3%82%af%e3%83%aa%e3%83%bc%e3%83%b3%e3%82%b7%e3%83%a7%e3%83%83%e3%83%88-2016-12-14-10-59-32
モックオブジェクト
%e3%82%b9%e3%82%af%e3%83%aa%e3%83%bc%e3%83%b3%e3%82%b7%e3%83%a7%e3%83%83%e3%83%88-2016-12-12-18-37-26
モック化したオブジェクトをスタブと呼ぶかモックと呼ぶかはいまいちはっきりわからなかったので以下ではモックと統一して進めていきます。

モックの使い方

では具体的なモックの使い方を確認してみます。
20161024204138
PHPUnitのTestCaseクラスはモックの呼び出し処理をいくつかラップしているのでTestCaseを継承していればモックを利用することができます。
PHPUnitのモックの呼び出しは大きく以下の3つの方法があります。

  • 自分で1からプロパティを設定してモックを作成する「getMockBuilder」を使う方法
  • 予めプロパティを設定されたモックを取得する「createMock」を使う方法
  • モック化したいクラス名とプロパティ情報を引数で渡してモックを生成する「getMock」を使う方法(※現在非推奨)

その中でも普通に使う分には「createMock」を使う方法が一番楽で良いかと思います。ただこの場合コンストラクタが呼ばれないので、コンストラクタを呼びたい場合や他に設定を変えたい場合は「getMockBuilder」を使ってモック化を行います。

createMockの処理を簡単に確認する

createMock

  • getMockBuilderでモックを取得
  • 必要なプロパティをメソッドチェーンで設定する
  • MockBuilderはGeneratorを使って自身のプロパティを渡しモックを生成する
    ※ createMockを使う場合コンストラクタ含め例外を除く全メソッドが握りつぶされます。
protected function createMock($originalClassName)
{
    return $this->getMockBuilder($originalClassName)
                ->disableOriginalConstructor()
                ->disableOriginalClone()
                ->disableArgumentCloning()
                ->disallowMockingUnknownTypes()
                ->getMock();
}

createPartialMock

createPartialMockをいうメソッドも用意されているので見てみます。これは指定したメソッドだけを握りつぶすことができます。指定しない場合は全て元のまま呼び出せます。

protected function createPartialMock($originalClassName, array $methods)
{
    return $this->getMockBuilder($originalClassName)
                ->disableOriginalConstructor()
                ->disableOriginalClone()
                ->disableArgumentCloning()
                ->disallowMockingUnknownTypes()
                ->setMethods(empty($methods) ? null : $methods)
                ->getMock();
}

createConfiguredMock

createConfiguredMockも一応見てみると、これはcreateMock+メソッドの振る舞い設定の組み合わせのfunctionです。

protected function createConfiguredMock($originalClassName, array $configuration)
{
    $o = $this->createMock($originalClassName);
    foreach ($configuration as $method => $return) {
        $o->method($method)->willReturn($return);
    }
    return $o;
}

モック生成の具体例

実際にcreateMockとcreatePartialMockを使ったモック生成方法を以下の例を使って確認してみます。

テスト対象のクラス例

何でもない変なクラスですが一応補足しておくと確認しやすいように以下のメソッドを用意しています。

  • ただ処理するだけのメソッド
  • クラス内のgetterを呼び出して処理しているメソッド
  • クラス内のsetterを呼び出して処理しているメソッド
  • privateメソッド
  • staticメソッド
class Example {
    private $total = 0;
    private static $static_total = 1;
    public function __construct() {
        $this->total = 1;
        self::$static_total = 2;
    }
    public function plusA() {
        return $this->total += 1;
    }
    public function plusB() {
        $this->total = $this->getTotal();
        return $this->total += 1;
    }
    public function plusC() {
        $this->setTotal(10);
        return $this->total += 1;
    }
    private function plusD() {
        return $this->total += 1;
    }
    public function getTotal() {
        return $this->total;
    }
    public function setTotal($total) {
        $this->total = $total;
    }
    public static function getStaticTotal() {
        return self::$static_total;
    }
}

createMockで全モック

コンストラクタは呼ばれず全てのメソッドは握りつぶされます。その状態でメソッドを呼び出しても結果は取得できません。

/**
 * 全メソッド握りつぶす
 *
 * 引数:クラスのみ
 * コンストラクタ:呼ばれない
 */
public function test_createMock_all() {
    $this->example = $this->createMock(Example::class);
    $result = $this->example->plusA();
    $this->assertNull($result);
    $result = $this->example->plusB();
    $this->assertNull($result);
    $result = $this->example->plusC();
    $this->assertNull($result);
}

イメージ
全部呼び出せない。
%e3%82%b9%e3%82%af%e3%83%aa%e3%83%bc%e3%83%b3%e3%82%b7%e3%83%a7%e3%83%83%e3%83%88-2016-12-15-20-42-52

createMockで振る舞い設定

/**
 * 全メソッド握りつぶす
 * plusAだけに振る舞い設定
 *
 * 引数:クラスのみ
 * コンストラクタ:呼ばれない
 */
public function test_createMock_all_stub() {
    $this->example = $this->createMock(Example::class);
    $this->example->expects($this->once())->method('plusA')->willReturn('A');
    $result = $this->example->plusA();
    $this->assertEquals('A', $result);
}

イメージ
plusAだけ呼び出せて(実態はよばれない)結果は自分の設定した値になる。
%e3%82%b9%e3%82%af%e3%83%aa%e3%83%bc%e3%83%b3%e3%82%b7%e3%83%a7%e3%83%83%e3%83%88-2016-12-15-20-59-27

createPartialMockで部分モック

第一引数に対象のクラスと第二引数に握りつぶしたいメソッドを配列で指定すると部分的にモック化することができる。第二引数を指定しない場合(または空の配列を指定)すべてのメソッドは元のまま。

/**
 * 一部握りつぶす
 *
 * 引数:クラスとメソッド
 * コンストラクタ:呼ばれない
 */
public function test_createPartialMock_partial() {
    $this->example = $this->createPartialMock(Example::class, ['getTotal']);
    $result = $this->example->plusA();
    $this->assertEquals(1, $result);
    // getTotalは振る舞いを設定していないためnullとなるがエラーにはならない
    $result = $this->example->plusB();
    $this->assertEquals(1, $result);
    $result = $this->example->plusC();
    $this->assertEquals(11, $result);
}

イメージ
一部握りつぶし他は元のまま
%e3%82%b9%e3%82%af%e3%83%aa%e3%83%bc%e3%83%b3%e3%82%b7%e3%83%a7%e3%83%83%e3%83%88-2016-12-15-21-09-15
振る舞いを指定する場合は以下のようにする

/**
 * 一部スタブ化&振る舞い設定
 *
 * 引数:クラスとメソッド
 * コンストラクタ:呼ばれない
 */
public function test_createPartialMock_setStub() {
    $this->example = $this->createPartialMock(Example::class, ['getTotal']);
    $this->example->expects($this->once())->method('getTotal')->willReturn(3);
    $result = $this->example->plusA();
    $this->assertEquals(1, $result);
    $result = $this->example->plusB();
    $this->assertEquals(4, $result);
    $result = $this->example->plusC();
    $this->assertEquals(11, $result);
}

イメージ
一部握りつぶし振る舞いを設定し他は元のまま
%e3%82%b9%e3%82%af%e3%83%aa%e3%83%bc%e3%83%b3%e3%82%b7%e3%83%a7%e3%83%83%e3%83%88-2016-12-15-21-07-59
また、setterのようにどの引数で何回呼ばれたかを確認するには以下のように書きます。

/**
 * 一部スタブ化&期待値設定(1 test, 4 assertions)
 *
 * 引数:クラスとメソッド
 * コンストラクタ:呼ばれない
 */
public function test_createPartialMock_setStub2() {
    $this->example = $this->createPartialMock(Example::class, ['setTotal']);
    $this->example->expects($this->once())->method('setTotal')->with(10);
    $result = $this->example->plusA();
    $this->assertEquals(1, $result);
    $result = $this->example->plusA();
    $this->assertEquals(2, $result);
    // setTotalは期待値の設定はあるが振る舞いはないので実行はされない
    $result = $this->example->plusC();
    $this->assertEquals(3, $result);
}

モックの振る舞いを設定する処理について

最後にモックに振る舞いを設定する処理について簡単に触れておきます。モックの振る舞いを設定する処理はInvocationMockerクラスが担っていて、method、willReturn、expects、withなどを使って振る舞いや期待値を設定します。
20161024204136

method

振る舞いや期待値を設定するメソッドを指定する

willReturn

スタブメソッドにどのような値を返させるかを設定する

expects

スタブメソッドが何回呼ばれるかを設定する

with

スタブメソッドがどの引数を受け取るかを設定する

カバレッジレポート

これらのテストのカバレッジレポートを出力します。カバレッジレポートは以下のように出力することができます。サンプルソースはこちらにあります。
出力方法

phpunit --bootstrap Bootstrap.php --coverage-html ./report --whitelist src/ test/ExamplePhpUnitMockTest.php

カバレッジレポート
%e3%82%b9%e3%82%af%e3%83%aa%e3%83%bc%e3%83%b3%e3%82%b7%e3%83%a7%e3%83%83%e3%83%88-2016-12-16-17-25-14
%e3%82%b9%e3%82%af%e3%83%aa%e3%83%bc%e3%83%b3%e3%82%b7%e3%83%a7%e3%83%83%e3%83%88-2016-12-16-17-25-34
コンストラクタとprivateとstaticとgetTotalメソッドはテストを通していないので赤く塗られます。また、privateとstaticメソッドのモック化やテストは面倒なので別のモッキンフレームワークであるPhakeなどでやったほうがいいと思います。
これで大体のモック仕組みとの基本の使い方の確認の確認ができました。これらを使って組み合わせるだけでも多くのテストの実行がカバーできるかと思います。

今回使ったPHPUnitのバージョン

“phpunit/phpunit”: “5.6.*”