taisablog

taisa's engineer blog

PHP

PHPのモッキンフレームワークPhakeの使い方

投稿日:December 20, 2016 更新日:

20161019203514

前回に引き続きPHPUnit関連の記事です。今回はPHPのモッキンフレームワークであるPhakeの使い方を確認します。

Phakeについて

作者は?

Mike Livelyという方のようです。

Phakeって?

こちらですね。

ファイル構成

一応ファイル構成を載せておきます。

.
├── Phake
│   ├── Annotation
│   │   ├── MockInitializer.php
│   │   └── Reader.php
│   ├── CallRecorder
│   │   ├── Call.php
│   │   ├── CallExpectation.php
│   │   ├── CallInfo.php
│   │   ├── IVerificationFailureHandler.php
│   │   ├── IVerifierMode.php
│   │   ├── OrderVerifier.php
│   │   ├── Position.php
│   │   ├── Recorder.php
│   │   ├── Verifier.php
│   │   ├── VerifierMode
│   │   │   ├── AtLeast.php
│   │   │   ├── AtMost.php
│   │   │   ├── Result.php
│   │   │   └── Times.php
│   │   └── VerifierResult.php
│   ├── ClassGenerator
│   │   ├── EvalLoader.php
│   │   ├── FileLoader.php
│   │   ├── ILoader.php
│   │   ├── InvocationHandler
│   │   │   ├── CallRecorder.php
│   │   │   ├── Composite.php
│   │   │   ├── FrozenObjectCheck.php
│   │   │   ├── IInvocationHandler.php
│   │   │   ├── MagicCallRecorder.php
│   │   │   └── StubCaller.php
│   │   └── MockClass.php
│   ├── Client
│   │   ├── Default.php
│   │   ├── IClient.php
│   │   └── PHPUnit.php
│   ├── Exception
│   │   ├── MethodMatcherException.php
│   │   └── VerificationException.php
│   ├── Facade.php
│   ├── IMock.php
│   ├── Matchers
│   │   ├── AbstractChainableArgumentMatcher.php
│   │   ├── AnyParameters.php
│   │   ├── ArgumentCaptor.php
│   │   ├── ChainedArgumentMatcher.php
│   │   ├── EqualsMatcher.php
│   │   ├── Factory.php
│   │   ├── HamcrestMatcherAdapter.php
│   │   ├── IArgumentMatcher.php
│   │   ├── IChainableArgumentMatcher.php
│   │   ├── IMethodMatcher.php
│   │   ├── IgnoreRemainingMatcher.php
│   │   ├── MethodMatcher.php
│   │   ├── PHPUnitConstraintAdapter.php
│   │   ├── ReferenceSetter.php
│   │   └── SingleArgumentMatcher.php
│   ├── Mock
│   │   ├── Freezer.php
│   │   ├── Info.php
│   │   └── InfoRegistry.php
│   ├── PHPUnit
│   │   ├── VerifierResultConstraint.php
│   │   └── VerifierResultConstraintV3d6.php
│   ├── Proxies
│   │   ├── AnswerBinderProxy.php
│   │   ├── AnswerCollectionProxy.php
│   │   ├── CallStubberProxy.php
│   │   ├── CallVerifierProxy.php
│   │   ├── StaticVisibilityProxy.php
│   │   ├── StubberProxy.php
│   │   ├── VerifierProxy.php
│   │   └── VisibilityProxy.php
│   ├── String
│   │   └── Converter.php
│   └── Stubber
│       ├── AnswerBinder.php
│       ├── AnswerCollection.php
│       ├── Answers
│       │   ├── ExceptionAnswer.php
│       │   ├── InvalidAnswerException.php
│       │   ├── LambdaAnswer.php
│       │   ├── NoAnswer.php
│       │   ├── ParentDelegate.php
│       │   ├── ParentDelegateCallback.php
│       │   ├── SelfAnswer.php
│       │   ├── SmartDefaultAnswer.php
│       │   └── StaticAnswer.php
│       ├── IAnswer.php
│       ├── IAnswerBinder.php
│       ├── IAnswerContainer.php
│       ├── SelfBindingAnswerBinder.php
│       └── StubMapper.php
└── Phake.php

PhakeはPhake.phpがファサードになっていてstaticに呼び出すのが特徴です。

Phakeの使い方

Phakeの使い方を前回同様以下の例を使って確認します。

例の補足

モック化の確認をしやすいように以下のパターンを用意しています。

  • 自分の中だけで完結する処理をするメソッド
  • クラス内の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;
    }
}

Phake::mockで全モック

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

/**
 * 全部握りつぶし
 *
 * 引数:クラスのみ
 * コンストラクタ:呼ばれない
 */
public function test_mock() {
    $example = Phake::mock(Example::class);
    $result = $example->plusA();
    $this->assertNull($result);
    $result = $example->plusB();
    $this->assertNull($result);
    $result = $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

Phake::mockで振る舞い設定

コンストラクタは呼ばれず全てのメソッドは握りつぶし、plusAにだけ返り値を設定します。

/**
 * 全部握りつぶし
 * plusAだけ振る舞い設定
 *
 * 引数:クラスのみ
 * コンストラクタ:呼ばれない
 */
public function test_mock_setting() {
    $example = Phake::mock(Example::class);
    Phake::when($example)->plusA()->thenReturn('A');
    $result = $example->plusA();
    $this->assertEquals('A', $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-59-27

Phake::partialMockで部分モック

partialMockだけを使った場合

まずpartialMockだけを使った場合、全ての振る舞いは元のままとなります。後述しますがこの場合verifyが使えます。

/**
 * 全部そのまま
 *
 * 引数:クラス(コンストラクタに値を渡す場合は引数追加)
 * コンストラクタ:呼ばれる
 */
public function test_partialMock_non() {
    $example = Phake::partialMock(Example::class);
    $result = $example->plusA();
    $this->assertEquals(2, $result);
    $result = $example->plusB();
    $this->assertEquals(3, $result);
    $result = $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-20-19-38-07

partialMockで振る舞いを設定する場合

partialMockを使った後に以下のように振る舞いを設定します。PHPUnitのモックオブジェクトのcreateMockメソッドとは違いコンストラクタは呼ばれます。

/**
 * 全部そのまま
 * 指定したメソッドのみ振る舞い設定
 *
 * 引数:クラス(コンストラクタに値を渡す場合は引数追加)
 * コンストラクタ:呼ばれる
 */
public function test_partialMock_partial() {
    $example = Phake::partialMock(Example::class);
    Phake::when($example)->getTotal()->thenReturn(3);
    $result = $example->plusA();
    $this->assertEquals(2, $result);
    $result = $example->plusB();
    $this->assertEquals(4, $result);
    $result = $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-20-19-45-49

どのように呼ばれたかを確認する

Phakeの場合どのように呼ばれたかのチェックは通常処理実行後にverifyを使って行います。getterやsetterがどの引数で何回呼ばれたかを確認するには以下のように書きます。

/**
 * 全部そのまま
 * 期待動作確認(1 test, 5 assertions)
 *
 * 引数:クラス(コンストラクタに値を渡す場合は引数追加)
 * コンストラクタ:呼ばれる
 */
public function test_partialMock_setStubMethod() {
    $example = Phake::partialMock(Example::class);
    $result = $example->plusA();
    $this->assertEquals(2, $result);
    $result = $example->plusB();
    $this->assertEquals(3, $result);
    $result = $example->plusC();
    $this->assertEquals(11, $result);
    Phake::verify($example, Phake::times(1))->getTotal();
    Phake::verify($example, Phake::times(1))->setTotal(10);
}

privateメソッドをモック化する

ここまではPHPUnitのモックオブジェクトとそれほど変わりませんが、Phakeを使えばprivateメソッドを簡単に実行したりモック化することができます。

Phake::makeVisibleを使って普通に実行する

Phake::makeVisibleを使うことでprivateメソッドを実行することができます。

/**
 * 全部握りつぶさない
 * privateメソッドを普通に実行
 *
 * 引数:クラスのみ
 * コンストラクタ:呼ばれる
 */
public function test_partialMock_private() {
    $example = Phake::partialMock(Example::class);
    // 直接は呼び出せないのでmakeVisibleにてプロキシして実行確認可能
    $result = Phake::makeVisible($example)->plusD();
    $this->assertEquals(2, $result);
}

モック化する

以下のように普通にprivateなスタブメソッドに振る舞いを設定することもできます。

/**
 * 全部握りつぶさない
 * privateメソッドの振る舞いを設定
 *
 * 引数:クラスのみ
 * コンストラクタ:呼ばれる
 */
public function test_partialMock_private2() {
    $example = Phake::partialMock(Example::class);
    // 普通に振る舞い設定
    Phake::when($example)->plusD()->thenReturn(5);
    // 直接は呼び出せないのでmakeVisibleにてプロキシして実行確認可能
    $result = Phake::makeVisible($example)->plusD();
    $this->assertEquals(5, $result);
}

whenStaticを使ってstaticメソッドをモック化する

PhakeならstaticメソッドもPhake::whenStaticを使って簡単にモック化できてしまいます。

/**
 * 全部握りつぶし
 * staticメソッドの振る舞いを設定
 *
 * 引数:クラスのみ
 * コンストラクタ:呼ばれない
 */
public function test_partialMock_static() {
    // そのまま実行する場合
    $result = Example::getStaticTotal();
    $this->assertEquals(1, $result);
    $example = Phake::partialMock(Example::class);
    // 直接はNG
    $result = $example::getStaticTotal();
    $this->assertNull($result);
    // 振る舞い設定
    Phake::whenStatic($example)->getStaticTotal()->thenReturn(3);
    $result = $example::getStaticTotal();
    $this->assertEquals(3, $result);
}

その他に便利なもの

thenCallParent

以下のようにPhake::mockで全部握りつぶしたとしてもthenCallParentを使えば元の処理を呼び出すことができます。

/**
 * 全部握りつぶし
 * privateメソッドをスタブ化する
 *
 * 引数:クラスのみ
 * コンストラクタ:呼ばれない
 */
public function test_mock_private2() {
    $example = Phake::mock(Example::class);
    Phake::when($example)->plusD()->thenCallParent();
    // 普通にスタブ化
    Phake::when($example)->plusD()->thenReturn(5);
    // 直接は呼び出せないのでmakeVisibleにてプロキシして実行確認可能
    $result = Phake::makeVisible($example)->plusD();
    $this->assertEquals(5, $result);
}

一括で振る舞いを設定する

モックに個別に振る舞いを設定するのが面倒なときは、Phake::ifUnstubbedを使って一括で設定できてしまいます。

/**
 * 全部スタブ化
 * 振る舞いを一括設定
 *
 * 引数:クラスのみ
 * コンストラクタ:呼ばれない
 */
public function test_mock_setting2() {
    $example = Phake::mock(Example::class, Phake::ifUnstubbed()->thenReturn('A'));
    $result = $example->plusA();
    $this->assertEquals('A', $result);
    $result = $example->plusB();
    $this->assertEquals('A', $result);
    $result = $example->plusC();
    $this->assertEquals('A', $result);
    $result = Phake::makeVisible($example)->plusD();
    $this->assertEquals('A', $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-20-20-35-31
%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-20-20-35-05

最後にカバレッジレポートを見てみます。ソースはこちらにあります。


きました、オールグリーン!もちろんモック確認の為のクラスなんでオールグリーンになったところでたいした意味はありませんが、Phakeを使えば簡単にprivateもstaticもモック化できてとにかく楽ちんです。

まとめ

ここまで簡単にモック化できてしまうとPHPのモッキンフレームワークはPhakeで十分ですね。また、期待動作の設定を処理実行後のverifyで行うのも直感的でわかりやすいです。

今回使ったPHPUnitとPhakeのバージョン
  • “phpunit/phpunit”: “5.6.*”,
  • “phake/phake”: “2.3.*”,

-PHP
-, , , ,

執筆者:

関連記事

PHPライブラリをPackagistに登録する方法

PHPのライブラリをPackagistに登録する方法を書いておきます。PackagistはPHPのパッケージリポジトリで、登録しておくとcomposerを使ってプロジェクトへインストールすることができます。ここではとあるプロジェクトをPackagistに登録する前提の流れで進めていきます。 Packagistに登録するプロジェクトを作成する 新規でプロジェクトを作成しcomposer initを実行します。 mkdir amazon-photo-formatter cd amazon-photo-formatter composer init composer initを実行すると色々と聞かれるので順番に進めていきます。まずはパッケージ名が聞かれます。<vendor>にはGitHubのアカウント名を指定し、<name>にはライブラリ名を記載します。ここではtaisa831/amazon-photo-formatterと記載しました。 Package name (/) [taisa831/packagist]: Descriptionはライブラリについての説明文なので、Format amazon photo file name to amazon photo’s format.と書きました。その他についてもサジェストされている内容とするか必要な内容を決めて進めていきます。 Description []: Author [Masaki Sato , n to skip]: Minimum Stability []: Package Type (e.g. library, project, metapackage, composer-plugin) []: library License []: MIT 次にこのライブラリが依存しているものがあればこの時点で指定することができます(後から手動で記載することも可能)。ここではphpunitを利用するのでrequire-devでphpunitを指定しました。 Would you like to define your dependencies (require) interactively [yes]? no Would you like to define your dev dependencies (require-dev) interactively [yes]? Search for a package: phpunit Found 15 packages matching phpunit [0] phpunit/phpunit [1] phpunit/phpunit-mock-objects Abandoned. Use instead. [2] phpunit/php-token-stream [3] phpunit/php-timer [4] phpunit/php-text-template [5] phpunit/php-file-iterator [6] phpunit/php-code-coverage [7] …

PHPの empty, isset, is_null の違いをしっかり理解する

PHPの isset、empty、is_null をしっかり理解して使おうと思い整理してみました。既にこのような記事「PHP isset, empty, is_null の違い早見表」もあるのでここではこれより少し踏み込んだところまで書いてみます。 empty, isset, is_nullの違い早見表 値 if ($var) empty isset is_null $var = 1 true false true false $var = array(1) true false true false $var = “” false true true false $var = “0” false true true false $var = 0 false true true false $var = array() false true true false $var = false false true true false $var = NULL; false true false true $var false true false true まず表ですが、順番を理解しやすい形に変えてみました。下記のように赤と青のグループで分けて考えておくと理解しやすいです。これをみると「if ($var)とempty」、「issetとis_null」が対になっているのがわかります。 感覚的には、if ($var)は値がありそうだなと思うものがtrueになり、emptyも値がなさそうだな思うものがtrueになる感じがします。issetは、何かしら値がセットされてばtrue(つまり値がfalseでも結果はtrue)、is_nullは値がnullであればtrueということになります。 実際の挙動の動作確認についてはPHPUnitを使ってテストしたものをGitHubにあげているので合わせて確認してみてください。https://github.com/taisa831/AimaiPHP empty, isset, is_null の Notice や Error 出力早見表 次に、PHPのerror_reportingをE_ALLにした場合に、indexのない配列にアクセスした場合やオブジェクトが空の変数や関数にアクセスした場合の挙動をまとめてみました。 値 if ($var) empty isset is_null $var = []; …

PHPによるDBUnit超入門

例えば簡単なWebサービスでMVCのフレームワークを使っていてビジネスロジックを書く用にコントローラとモデルの間にサービス層を追加して開発している場合、コントローラやサービスはモックを駆使しながらテストを書いていくことができます。ただ、例えばフレームワークをバージョンアップしたい、PHPをバージョンアップしたいなどの場合に既存のモデル層に影響がないかをテストで確認したいなんてことがあります。そのような場合には、DBUnitを導入してみてもいいかもしれません。ということで本記事ではPHPによるDBUnitの使い方を書いてみます。 事前情報 今phpunit/dbunitをインストールしようとすると以下の文言が出力されます。詳しくはこちらのissueに書いてありますが、どうもSebastianさんはdbunitのメンテナンスをやめるようです。ただそれを受けてforkしたプロジェクトが出てきているようなので大丈夫かと思います。今回はSebastianさんの純正dbunitを使っています。 Package phpunit/dbunit is abandoned, you should avoid using it. No replacement was suggested また、DBUnitに関する詳しい情報はマニュアルにありますのでご確認ください。https://phpunit.de/manual/6.5/ja/database.html#database.implementing-getdataset 作成したサンプルプロジェクト 今回は、dbunitの確認だけをしたいので、dietcakeのmessage-boardというサンプルプロジェクトを利用しました。今回作成したDBUnit用のサンプルプロジェクトは GitHub からダウンロードして確認できます。 git clone git@github.com:taisa831/phpunit-dbunit-sample.git cd phpunit-dbunit-sample composer install # mysqlサーバを立て`app/config/sql/board.sql`を実行する(SQLは下記に記載しています) # テスト実行 ./vendor/bin/phpunit PHPUnit 7.5.8 by Sebastian Bergmann and contributors. ….. 5 / 5 (100%) Time: 207 ms, Memory: 4.00 MB OK (5 tests, 14 assertions) アプリ用のDDLです。開発用DBとは違うのでboard_dbunitというテーブル名にしています。 — — — Create database — CREATE DATABASE IF NOT EXISTS board_dbunit; GRANT SELECT, INSERT, UPDATE, DELETE ON board.* TO board_root@localhost IDENTIFIED BY ‘board_root’; FLUSH PRIVILEGES; — — Create tables — USE board_dbunit; CREATE TABLE IF NOT EXISTS thread ( id INT UNSIGNED …

PHPUnitの使い方を仕組みから理解する

ここ数年仕事ではPHPを使って開発をしていますが、最近品質について考える機会が増えたこともあり、これを機にPHPUnitと周辺のモジュールの仕組みを理解してより楽にテストができるようにしたいと思います。 PHPUnitは? Sebastian Bergmann Created PHPUnit. Co-Founded thePHP.cc. Helps PHP developers build better software. PHPUnitの作者は、Sebastian Bergmannという方でthePHP.ccのファウンダーのようです。関連情報は以下にて確認してみてください。 TwitterアカウントPHPUnit GithubPHPUnitマニュアル PHPUnitの構成 PHPUnitは以下のような構成になっています。 phpunit php-code-coverage php-file-iterator php-text-template php-timer php-token-stream phpunit phpunit-mock-objects これらはGitHub上ではそれぞれ別々のリポジトリに分かれていますが、phpunitが本体でそれ以外はデフォルトの関連ライブラリという位置づけになるかと思います。 PHPUnitのsrc構成 モックオブジェクトなどを除いたphpunitだけのパッケージとクラス構成を見てみるとこんな感じになります。 ├── Exception.php ├── Extensions │   ├── GroupTestSuite.php │   ├── PhptTestCase.php │   ├── PhptTestSuite.php │   ├── RepeatedTest.php │   ├── TestDecorator.php │   └── TicketListener.php ├── ForwardCompatibility │   └── TestCase.php ├── Framework │   ├── Assert │   │   └── Functions.php │   ├── Assert.php │   ├── AssertionFailedError.php │   ├── BaseTestListener.php │   ├── CodeCoverageException.php │   ├── Constraint │   │   ├── And.php │   │   ├── ArrayHasKey.php │   │   ├── ArraySubset.php │   │   ├── Attribute.php │   │   …

GitLabのprivateなPHPライブラリをcomposer installするには

社内ツールでprivateなリポジトリに置いておきたいけど、いろんなプロジェクトでcomposer installしたいというケースは以外とあるんじゃないかと思います。そういう時は、composer.jsonにrepositoriesを追加して、GitLab(ここではGitLabとしています)のURLを指定するとインストールが可能になります。しかしそのままだとpublicなリポジトリしかだめですが、privateなリポジトリであれば、GitLabからPersonal AcessTokenを取得して、composer config –global –auth gitlab-token.gitlab.com [ACESS_TOKEN]を実行すればcomposer installが可能になります。 { “name”: “taisa831/sample-framework-app”, “license”: “MIT”, “authors”: [ { “name”: “taisa”, “email”: “g5.taisa831@gmail.com” } ], “require”: { “taisa831/sample-framework”: “dev-master” }, “repositories”: [ { “type”: “vcs”, “url”: “git@gitlab.com:taisa831/sample-framework.git” } ] } では、Webフレームワークをprivateなリポジトリに公開して利用するところまでをやってみます。 (今回は便宜上publicにしています) 事前準備 ここではサンプルのWebフレームワーク(実装なし)をプロジェクトにインストールできるようにすることにします。リポジトリは2つで、フレームワークの実態であるsample-frameworkとフレームワークの雛形となるsample-framework-appを用意しておきました。それぞれの構成は以下の通りです。 https://gitlab.com/taisa831/sample-framework.git # フレームワークの実体 . ├── README.md ├── composer.json ├── src ├── tests └── vendor https://gitlab.com/taisa831/sample-framework-app.git # フレームワークの雛形 . ├── README.md ├── composer.json ├── composer.lock ├── config ├── controllers ├── models ├── public │   └── index.php ├── routes ├── tests └── views このsample-framework-appのcomposer.jsonには上記でも記載した内容が書かれています。requireにtaisa831/sample-frameworkを指定し、repositoriesにGitLabのURLを指定することで探してくれるようになります。 { “name”: “taisa831/sample-framework-app”, “license”: “MIT”, “authors”: [ { “name”: “taisa”, “email”: “g5.taisa831@gmail.com” } ], “require”: { …