PHPUnitの応用範囲を広げるテストダブル(モック、スタブ)


自分の中でも課題だった。

ヾ(・ω<)ノ" 三三三● ⅱⅲ コロコロ♪

------------------- ↓ 余談はここから ↓-------------------
テストを実行する上で、
こんなことを思ったことはないだろうか?

・本番環境だけ動くテストをしたい
・仕様書しか出来てないAPIやデータベースの結果を使ったテストをしたい
・コード中の例外発生時のテストをしたい

筆者は仕事柄レガシィコードを扱うことが多い。
すでに本番環境で動作しているコードを変更する際は、
現在の動作が確実に動くことを再現・確認するということが非常に重要になってくる。

その際、本番環境だけ実行されるもの、
APIやデータベースからデータをとってくるもの、
例外発生時のログ調整など、
単純なユニットテストでは普通にテストできないものも多々ある。

それらをテストする際に使うのが

テストダブル(モック、スタブ、スパイ)

テストダブルとは?

テストダブル (Test Double) とは、ソフトウェアテストにおいて、テスト対象が依存しているコンポーネントを置き換える代用品のこと。

特定のオブジェクトに想定した動きを振る舞ってもらうとか、
特定のメソッドに指定した値を返却してもらうなど応用範囲は広い。

環境は以下の通り。
PHP5.6
PHPUnit5.7

------------------- ↓ 本題はここから ↓-------------------

準備

例によってインストールから行うが、
今回はインストールするものはPHPUnitしかないので、
以下が実行できていればよい。

$ composer require  "phpunit/phpunit=5.7.*"   # PHP5.x
$ phpunit -v
PHPUnit 5.7.27 by Sebastian Bergmann and contributors.

6, 7, 8とバージョンはあるが、
それぞれ
7.0, 7.1, 7.2用となっている。

スタブ

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

単発で実行内容を偽装したい場合に使う場合が多い。
偽装のパターンは以下のような単純な物だけではないが、
全部記述するとページがいくらあっても足りないので、
マニュアルを参照のこと。

テスト対象プログラム

<?php
define('APPENV', 'staging');

class SomeClass
{
  public function isProduction()
  {
    return (APPENV !== 'production') ? false : true;
  }
}

テストケース

<?php

class SomeClassTest extends \PHPUnit\Framework\TestCase
{
  public static function setUpBeforeClass()
  {
    require_once('SomeClass.php');
  }

  public function testIsProduction()
  {
      $stub = $this->createMock(SomeClass::class);

      $stub->expects($this->any())
          ->method('isProduction')
          ->willReturn(true);

      $this->assertSame(false, $stub->isProduction());
  }
}

実行

⋊> phpunit SomeClassTest
PHPUnit 5.7.27 by Sebastian Bergmann and contributors.

F
1 / 1 (100%)

Time: 183 ms, Memory: 4.00MB

There was 1 failure:

1) SomeClassTest::testIsProduction
Failed asserting that true matches expected false.

/path/to/SomeClassTest.php:18

FAILURES!
Tests: 1, Assertions: 1, Failures: 1.

設定値は stagingにも関わらず、
isProductionがtrueを返却していて、
テストが失敗していることわかる。

モック

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

クラス全体の動きをテスト実行の時だけ任意のものに変え、
本来テスト動きをテストする手法
スタブと違ってコードが複雑化するが、
二次的に呼ばれるメソッドを制御することもでき、
メインで使うのはモックとなるのではなかろうか。

テスト対象プログラム

<?php
define('APPENV', 'develop');

class SomeClass
{
  public function isProduction()
  {
    return (APPENV !== 'production') ? false : true;
  }

  public function getEnv()
  {
    return (!$this->isProduction()) ? 'develop' : 'production';
  }
}

テストケース

<?php
class SomeClassTest extends \PHPUnit\Framework\TestCase
{
  public static function setUpBeforeClass()
  {
    require_once('SomeClass.php');
  }

  public function testGetEnv()
  {
      $mock = $this->getMockBuilder(SomeClass::class)
          ->setMethods(['isProduction'])
          ->getMock();

      $mock->expects($this->any())
          ->method('isProduction')
          ->will($this->returnValue(true));

      $this->assertSame('develop', $mock->getEnv());
  }
}

実行

⋊> phpunit SomeClassTest
PHPUnit 5.7.27 by Sebastian Bergmann and contributors.

F
1 / 1 (100%)

Time: 74 ms, Memory: 4.00MB

There was 1 failure:

1) SomeClassTest::testGetEnv
Failed asserting that two strings are identical.
--- Expected
+++ Actual
@@ @@
-develop
+production

/path/to/SomeClassTest.php:20

FAILURES!
Tests: 1, Assertions: 1, Failures: 1.

設定値は developにも関わらず、
isProductionがtrueを返却していて、
テストが失敗していることわかる。


これらのようにメソッドの動きを偽装して、
実際の動作テストをテスト実行環境で行うことに成功している。

上記テストを応用すれば、
本番環境用のコンフィグ値呼び出しをテストするとか、
本番環境用の通知先が切り替わっているかどうかをテストすることができる。

------------------- ↓ 後書はここから ↓-------------------

上記で偉そうに書いているが、
筆者はなかなかこのテストダブルという概念が理解できず、
使う場面に出会ってもスルーしていた。
とある時期に本格的にやってみようとエイヤと持ち込んだのが上手くいったので今回記事にしている。

大体テストの環境がないことが多い上、
PHPUnitのバージョンも3.7であることが珍しくない。
(なんかコードが複雑になる嫌いもあるし)

テストケースのコードはなるべく複雑にならないようにするのが筆者のポリシーではあるが、
テストダブルを行うとなると複雑化するのは必至で悩ましいところである。