Posted on

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の確認だけをしたいので、dietcakemessage-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 NOT NULL AUTO_INCREMENT,
title                   VARCHAR(255) NOT NULL,
created                 DATETIME NOT NULL,
PRIMARY KEY (id)
)ENGINE=InnoDB;
CREATE TABLE IF NOT EXISTS comment (
id                      INT UNSIGNED NOT NULL AUTO_INCREMENT,
thread_id               INT UNSIGNED NOT NULL,
username                VARCHAR(255) NOT NULL,
body                    TEXT NOT NULL,
created                 DATETIME NOT NULL,
PRIMARY KEY (id),
INDEX (thread_id, created)
)ENGINE=InnoDB;

モデルのテストは以下です。

assertEquals('1', $thread->id);
        $this->assertEquals('test', $thread->title);
        $this->assertEquals('2019-03-27 00:00:00', $thread->created);
    }
    public function test_getAll()
    {
        $threadAll = Thread::getAll();
        $this->assertEquals('1', $threadAll[0]->id);
        $this->assertEquals(1, $this->getConnection()->getRowCount('thread'));
    }
    public function test_getComments()
    {
        $thread = Thread::get(1);
        $comments = $thread->getComments();
        $this->assertEquals('1', $comments[0]->id);
    }
    public function test_create()
    {
        $comment = new Comment();
        $comment->thread_id = 1;
        $comment->username = 'user';
        $comment->body = 'comment';
        $thread = Thread::get(1);
        $thread->title = 'hoge';
        $thread->create($comment);
        $threadQueryTable = $this->getConnection()->createQueryTable('thread', 'select * from thread');
        $this->assertEquals(2, $threadQueryTable->getRowCount());
        $this->assertEquals('hoge', $threadQueryTable->getValue(1, 'title'));
        $commentQueryTable = $this->getConnection()->createQueryTable('comment', 'select * from comment');
        $this->assertEquals(2, $commentQueryTable->getRowCount());
        $this->assertEquals('user', $commentQueryTable->getValue(1, 'username'));
        $this->assertEquals('comment', $commentQueryTable->getValue(1, 'body'));
    }
    public function test_write()
    {
        $comment = new Comment();
        $comment->thread_id = 1;
        $comment->username = 'user';
        $comment->body = 'comment';
        $thread = Thread::get(1);
        $thread->write($comment);
        $commentQueryTable = $this->getConnection()->createQueryTable('comment', 'select * from comment');
        $this->assertEquals(2, $commentQueryTable->getRowCount());
        $this->assertEquals('user', $commentQueryTable->getValue(1, 'username'));
        $this->assertEquals('comment', $commentQueryTable->getValue(1, 'body'));
    }
    public function getTearDownOperation()
    {
        return \PHPUnit\DbUnit\Operation\Factory::TRUNCATE();
    }
}

カバレッジレポートはこのようになりました。

これで一通りの確認ができました。細かい書き方は色々あると思いますが、とにかくやりたいことはこれでできると思います。DBUnitではPDOを利用していますが、アプリケーションではPDOを使っていなくても大丈夫ですし、基本的にはモックなどを利用する必要もありませんので手軽に導入できると思います。

PDO を使ったアプリケーションじゃないと Database Extension を使えないの?
いいえ。PDO が必要なのは、フィクスチャの準備や後始末とアサーションのときだけです。 テスト対象のコード内では、なんでもお好みの方法でデータベースにアクセスできます。

プロジェクト作成までの流れ

それではここからプロジェクト作成までの流れをさらっとやってみます。

プロジェクトを作成する

git clone git@github.com:dietcake/dietcake-message-board.git
cd dietcake-message-board

`composer`の`require-dev`にライブラリを追加する

composer.jsonがないので`composer init`をしてファイルを作成します。そして以下のライブラリを追加し`composer install –dev`を実行します。

    "require-dev": {
        "phpunit/phpunit": "^7.5",
        "phpunit/dbunit": "^4.0"
    },

DBアクセスの為の`abstract`クラスを作成する

`DatabaseTestCase.php`という `abstract`クラスを作成します。`$GLOBALS`は後ほど作成する`phpunit.xml`に定義します。

conn === null) {
            if (self::$pdo == null) {
                self::$pdo = new PDO($GLOBALS['DB_DSN'], $GLOBALS['DB_USER'], $GLOBALS['DB_PASSWD']);
            }
            $this->conn = $this->createDefaultDBConnection(self::$pdo, $GLOBALS['DB_DBNAME']);
        }
        return $this->conn;
    }
}

モデルのテストクラスを作成する

フィクスチャはあらかじめ用意しておくデータのことです。まだテストを書いていない状態ですが、フィクスチャである`getDataSet`の実装とあと処理様の`getTearDownOperation`を実装しておきます。`getDataSet`を実装しておくと、テスト実行前にDBに値を入れてくれて、`getTearDownOperation`を実装するとテスト実行後にテーブルをクリアしてくれます。

<?php
require_once dirname(__FILE__) . '/../DatabaseTestCase.php';
require_once dirname(__FILE__) . '/../../../models/thread.php';
use PHPUnit\DbUnit\DataSet\YamlDataSet;
class ThreadTest extends DatabaseTestCase
{
    /**
     * Returns the test dataset.
     */
    protected function getDataSet()
    {
        return new YamlDataSet(dirname(__FILE__) . '/_files/thread.yml');
    }
    public function getTearDownOperation()
    {
        return \PHPUnit\DbUnit\Operation\Factory::TRUNCATE();
    }
}

フィクスチャを作成する

DBにあらかじめ入れておくレコード情報を作成します。様々なフォーマットが使えますが`Yaml`が一番扱いやすいので`Yaml`で記述しています。

thread:
  -
    id: '1'
    title: 'test'
    created: '2019-03-27 00:00:00'
comment:
  -
    id: '1'
    thread_id: '1'
    username: 'taro'
    body: 'comment'
    created: '2019-03-27 00:00:00'

`bootstrap.php`を作成する

`dietcake`が最低限動くようにテスト用の`bootstrap.php`を作成します。

<?php
define('ROOT_DIR', dirname(dirname(__DIR__)).'/../');
define('APP_DIR', ROOT_DIR.'app/');
require_once ROOT_DIR.'dietcake/dietcake.php';
require_once CONFIG_DIR.'bootstrap.php';
require_once CONFIG_DIR.'core.php';

ここまでで、最終的なディレクトリ構成はこのようになりました。

tests
├── dbunit
│   ├── DatabaseTestCase.php
│   ├── bootstrap.php
│   └── models
│       ├── ThreadTest.php
│       └── _files
│           └── thread.yml
└── unittest
    └── empty

`phpunit.xml`を作成する

場所はどこでもよいですが、プロジェクト直下に`phpunit.xml`ファイルを作成します。このファイルにはDBのアクセス情報、テスト対象、ホワイトリスト対象、bootstrapの指定などを行なっています。ホワイトリストディレクトリを指定しておくことでカバレッジレポートを確認することできるようになります。



    
        
        
        
        
    
    
        
            ./app/tests/dbunit/models
        
    
    
        
            ./app/models/
        
    

テストを作成する

やり方はいくつかあると思いますが、とにかくやりたいのは、modelsの処理を実行し、アサートチェックし、あと処理で元に戻すことなので、その目的を達成することを一番に考えました。このプロジェクトはthread.phpに処理が書かれているのでそれをテストします。データ取得とデータ作成処理があるので、そのパターンにおける確認ができます。
データ取得処理は、フィクスチャで入ったレコードをモデルで取得し結果をアサーションしました。

public function test_getTest()
{
    $thread = Thread::get(1);
    $this->assertEquals('1', $thread->id);
    $this->assertEquals('test', $thread->title);
    $this->assertEquals('2019-03-27 00:00:00', $thread->created);
}

書き込み処理は、モデルで書き込みをした後、DBUnitに用意されているcreateQueryTableを利用してデータを取得し結果をアサーションしました。データセットを利用したりテーブル結果をアサーションするような方法もあるようですが、ここでは利用していません。

public function test_write()
{
    $comment = new Comment();
    $comment->thread_id = 1;
    $comment->username = 'user';
    $comment->body = 'comment';
    $thread = Thread::get(1);
    $thread->write($comment);
    $commentQueryTable = $this->getConnection()->createQueryTable('comment', 'select * from comment');
    $this->assertEquals(2, $commentQueryTable->getRowCount());
    $this->assertEquals('user', $commentQueryTable->getValue(1, 'username'));
    $this->assertEquals('comment', $commentQueryTable->getValue(1, 'body'));
}

カバレッジレポートを出力する

以下のコマンドでカバレッジレポートを作成することができます。

./vendor/bin/phpunit -c phpunit.xml --coverage-html coverage

まとめ

調べたところDBUnit自体の記事が少なかったので、実際に利用しているサービスはあんまりないのではないかと思いましたが実際のところはどうなのかはわかりません。自分も今回初めて触ってみましたが、バージョンアップ時や新しくジョインしたプロジェクトなどモデル層に不安がある場合には導入してがっつりテストを書いてしまうなどはありかなと思いました。

Posted on

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

社内ツールでprivateなリポジトリに置いておきたいけど、いろんなプロジェクトでcomposer installしたいというケースは以外とあるんじゃないかと思います。そういう時は、composer.jsonrepositoriesを追加して、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-appcomposer.jsonには上記でも記載した内容が書かれています。requiretaisa831/sample-frameworkを指定し、repositoriesにGitLabのURLを指定することで探してくれるようになります。

{
    "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フレームワークを利用したプロジェクトを作成する

これでWebフレームワークを利用するプロジェクトを作成することができます。通常ならcomposer create-project --prefer-dist taisa831/sample-framework-app dev-masterこのようなコマンドでいけるのですが、packagistに登録していないので手動やります。(satisを使えばというのもありますがそこまでしたくはないので)

git clone git@gitlab.com:taisa831/sample-framework-app.git web-app
cd web-app
rm -rf .git
# privateなリポジトリの場合はここ(https://gitlab.com/profile/personal_access_tokens)からアクセストークンを取得し設定します。
# 今回はpublicなので不要です
composer config --global --auth gitlab-token.gitlab.com [ACESS_TOKEN]
composer install
git init
git remote add origin [新しいリポジトリ]

これでフレームワークの雛形と実体が合わさり環境構築の完了となります。

.
├── README.md
├── composer.json
├── composer.lock
├── config
├── controllers
├── models
├── public
│   └── index.php
├── routes
├── tests
├── vendor
│   ├── autoload.php
│   ├── composer
│   │   ├── ClassLoader.php
│   │   ├── LICENSE
│   │   ├── autoload_classmap.php
│   │   ├── autoload_namespaces.php
│   │   ├── autoload_psr4.php
│   │   ├── autoload_real.php
│   │   ├── autoload_static.php
│   │   └── installed.json
│   └── taisa831
│       └── sample-framework ← フレームワークの実体
│           ├── README.md
│           ├── composer.json
│           ├── src
│           └── tests
└── views
Posted on

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-devphpunitを指定しました。

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] symfony/phpunit-bridge
   [8] phpunit/phpunit-selenium
   [9] johnkary/phpunit-speedtrap
  [10] codedungeon/phpunit-result-printer
  [11] jean85/pretty-package-versions
  [12] brianium/paratest
  [13] codeception/stub
  [14] spatie/phpunit-snapshot-assertions
Enter package # to add, or the complete package name if it is not listed: 0
Enter the version constraint to require (or leave blank to use the latest version):
Using version ^7.5 for phpunit/phpunit

そうすることで次のようなcomposer.jsonファイルができあがり、このままgenerateするか、composer installするかを聞かれるのでyesとして進めていきます。実行が終わったらプロジェクト直下にsrctestsディレクトリを作成しておきます。

{
    "name": "taisa831/amazon-photo-formatter",
    "description": "Format amazon photo file name to amazon photo format.",
    "type": "library",
    "license": "MIT",
    "authors": [
        {
            "name": "taisa",
            "email": "g5.taisa831@gmail.com"
        }
    ],
    "require": {},
    "require-dev": {
        "phpunit/phpunit": "^7.5"
    }
}
Do you confirm generation [yes]? yes
Would you like to install dependencies now [yes]? yes
Loading composer repositories with package information
Updating dependencies (including require-dev)
Package operations: 28 installs, 0 updates, 0 removals
  - Installing sebastian/version (2.0.1): Loading from cache
  - Installing sebastian/resource-operations (2.0.1): Loading from cache
  - Installing sebastian/recursion-context (3.0.0): Loading from cache
  - Installing sebastian/object-reflector (1.1.1): Loading from cache
  - Installing sebastian/object-enumerator (3.0.3): Loading from cache
  - Installing sebastian/global-state (2.0.0): Loading from cache
  - Installing sebastian/exporter (3.1.0): Loading from cache
  - Installing sebastian/environment (4.1.0): Loading from cache
  - Installing sebastian/diff (3.0.2): Loading from cache
  - Installing sebastian/comparator (3.0.2): Loading from cache
  - Installing phpunit/php-timer (2.1.1): Loading from cache
  - Installing phpunit/php-text-template (1.2.1): Loading from cache
  - Installing phpunit/php-file-iterator (2.0.2): Loading from cache
  - Installing theseer/tokenizer (1.1.0): Loading from cache
  - Installing sebastian/code-unit-reverse-lookup (1.0.1): Loading from cache
  - Installing phpunit/php-token-stream (3.0.1): Loading from cache
  - Installing phpunit/php-code-coverage (6.1.4): Loading from cache
  - Installing doctrine/instantiator (1.1.0): Loading from cache
  - Installing symfony/polyfill-ctype (v1.10.0): Loading from cache
  - Installing webmozart/assert (1.4.0): Loading from cache
  - Installing phpdocumentor/reflection-common (1.0.1): Loading from cache
  - Installing phpdocumentor/type-resolver (0.4.0): Loading from cache
  - Installing phpdocumentor/reflection-docblock (4.3.0): Loading from cache
  - Installing phpspec/prophecy (1.8.0): Loading from cache
  - Installing phar-io/version (2.0.1): Loading from cache
  - Installing phar-io/manifest (1.0.3): Loading from cache
  - Installing myclabs/deep-copy (1.8.1): Loading from cache
  - Installing phpunit/phpunit (7.5.7): Downloading (100%)
sebastian/global-state suggests installing ext-uopz (*)
phpunit/php-code-coverage suggests installing ext-xdebug (^2.6.0)
phpunit/phpunit suggests installing phpunit/php-invoker (^2.0)
phpunit/phpunit suggests installing ext-xdebug (*)
Writing lock file
Generating autoload files

登録するライブラリを作成する

ここまででプロジェクトの形ができあがったのでsrc配下にソースファイルを作成します。今回は簡単なサンプルプログラムとしています。

.
├── composer.json
├── composer.lock
├── package-lock.json
├── vendor
├── src
└── tests
setCurrentDir($currentDir);
    }
    public function format()
    {
        $this->createFormatDir();
        $this->formatPictures();
    }
    public function createFormatDir()
    {
        if (file_exists('format') === false) {
            mkdir('format');
        }
    }
    /**
     * @throws \Exception
     */
    public function formatPictures()
    {
        foreach(glob($this->getCurrentDir() . '/{*.jpeg,*.jpg}', GLOB_BRACE) as $fileName) {
            if (is_file($fileName) === false) continue;
            $photoImg = file_get_contents($fileName);
            $ext = substr($fileName, strrpos($fileName, '.') + 1);
            $exif = @exif_read_data($fileName);
            $dateTime = new \DateTime($exif['DateTimeOriginal']);
            $formatFileName = $dateTime->format('Y-m-d_H-i-s');
            file_put_contents('./format/' . $formatFileName . '.' . $ext, $photoImg);
        }
    }
    /**
     * @return mixed
     */
    public function getCurrentDir()
    {
        return $this->currentDir;
    }
    /**
     * @param mixed $currentDir
     */
    public function setCurrentDir($currentDir)
    {
        $this->currentDir = $currentDir;
    }
}

unittestを作成する

続いて、`src`配下に先程作成したソースのテストを書いていきます。他の依存ライブラリを利用している場合は`bootstrap.php`ファイルも作成しておきます。

getMockBuilder(AmazonPhotoFormatter::class)
            ->setConstructorArgs([$dir])
            ->setMethods(null)
            ->getMock();
        $currentDir = $amazonPhotoFormatter->getCurrentDir();
        $this->assertEquals($dir, $currentDir);
    }
    public function test_format()
    {
        $amazonPhotoFormatter = $this->createPartialMock(AmazonPhotoFormatter::class, ['createFormatDir', 'formatPictures']);
        $amazonPhotoFormatter->expects($this->once())->method('createFormatDir');
        $amazonPhotoFormatter->expects($this->once())->method('formatPictures');
        $amazonPhotoFormatter->format();
    }
}

`bootstrap.php`を作成する場合はこんな感じ

<?php
error_reporting(E_ALL | E_STRICT);
require_once __DIR__ . '/../vendor/autoload.php';

これで`tests`配下で以下のコマンドを実行するとテストがいくつか実行されます。

../vendor/bin/phpunit AmazonPhotoFormatterTest.php
PHPUnit 7.5.7 by Sebastian Bergmann and contributors.
...                                                                 3 / 3 (100%)
Time: 51 ms, Memory: 4.00 MB
OK (3 tests, 3 assertions)

`phpunit.xml.dist`ファイルをプロジェクト直下に置いておくとプロジェクト直下で次のコマンドでも実行可能になります。

vendor/bin/phpunit
PHPUnit 7.5.7 by Sebastian Bergmann and contributors.
...                                                                 3 / 3 (100%)
Time: 25 ms, Memory: 4.00 MB
OK (3 tests, 3 assertions)


    
        
            ./tests/
        
    
    
        
            ./src/
        
    

Travis CIと連携する

Travis CIとGitHubのリポジトリを連携させ、以下のような.travis.ymlを作成しプロジェクト直下に配置することでgit pushした際にテストが実行されるようになります。

language: php
php:
  - 7.3
before_script:
  - composer self-update
  - composer install
script:
  - vendor/bin/phpunit

Packagistに登録する

作成したプロジェクトを自分のGitHubへアップしたら、Packagistへ登録をします。packagistにGitHubアカウントでログインした後、Submit 画面へ進み、登録したいリポジトリのURLを入力すると登録することができます。

GitHubとPackagistを連携する

最後にGitHubにプッシュしたらPackagistも自動的に更新するように連携しておきます。連携は、GitHubリポジトリのSettings>Webhooksから行うことができます。PackagistのプロフィールからAPI Tokenを取得しておき、GitHubのWebhooksへ次のように登録します。

  • Payload URL:https://packagist.org/api/github?username=[GitHubのユーザ名]
  • Content type:application/json
  • Secret:PackagistのAPI Token

ライブラリを使ってプロジェクトを作成してみる

ここまでで一通りの登録が完了したので、実際にプロジェクトから呼び出してみます。新規でプロジェクトを作成するには以下のコマンドを実行します。プロジェクト名の後ろにdev-master(masterブランチ)やGitHubのタグを指定することでバージョンを指定することが可能です。また、composer.jsonにライブラリ名とバージョンを記載してcomposer installして利用することも可能です。

composer create-project taisa831/amazon-photo-formatter project-name dev-master
composer create-project taisa831/amazon-photo-formatter project-name v1.0.0
{
    "name": "masakisato/testprj",
    "authors": [
        {
            "name": "taisa",
            "email": "g5.taisa831@gmail.com"
        }
    ],
    "require": {
            "taisa831/amazon-photo-formatter": "1.0.0"
    }
}
$ composer install
Loading composer repositories with package information
Updating dependencies (including require-dev)
Package operations: 1 install, 0 updates, 0 removals
  - Installing taisa831/amazon-photo-formatter (v1.0.0): Loading from cache
Writing lock file
Generating autoload files

参考

Packagistに登録するのはもう怖くない

Posted on

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」、「issetis_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_reportingE_ALLにした場合に、indexのない配列にアクセスした場合やオブジェクトが空の変数や関数にアクセスした場合の挙動をまとめてみました。

if ($var) empty isset is_null
$var = [];
$var[0];
Undefined offset: 0 true false Undefined offset: 0
$var = [];
$var[‘hoge’]
Undefined index: hoge true false Undefined index: hoge
$var = null;
$var[‘hoge’]
null true false null
$var = (object)[];
$var->var;
Undefined property: stdClass::$func true false Undefined property: stdClass::$func
$var = null;
$var->var;
Trying to get property of non-object true false Trying to get property of non-object
$var = (object)[];
$var->func();
Error: Call to undefined method stdClass::func() Error: Call to undefined method stdClass::func() 実行不可 Error: Call to undefined method stdClass::func()
$var = null;
$var->func();
Error: Call to undefined method stdClass::func() Error: Call to undefined method stdClass::func() 実行不可 Error: Call to undefined method stdClass::func()

結果は、上の表のようになりました。オブジェクトに対して未定義の関数にアクセスした場合やnullから関数を呼ぼうとした場合全てにおいてErrorになります。それ以外では、emptyissetを使うとNoticeは出でませんが、if ($var)is_nullを使うとNoticeがでるという違いがあります。この辺の違いを理解しておくとPHPによる開発が進めやすくなると思います。スマホだと表が切れてしまうので画像も貼っておきます。

Posted on

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

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

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.*”,
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.*”

Posted on

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

ここ数年仕事ではPHPを使って開発をしていますが、最近品質について考える機会が増えたこともあり、これを機にPHPUnitと周辺のモジュールの仕組みを理解してより楽にテストができるようにしたいと思います。
20161019203514

PHPUnitは?

Sebastian Bergmann

Created PHPUnit. Co-Founded thePHP.cc. Helps PHP developers build better software.

PHPUnitの作者は、Sebastian Bergmannという方でthePHP.ccのファウンダーのようです。関連情報は以下にて確認してみてください。

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
│   │   ├── Callback.php
│   │   ├── ClassHasAttribute.php
│   │   ├── ClassHasStaticAttribute.php
│   │   ├── Composite.php
│   │   ├── Count.php
│   │   ├── Exception.php
│   │   ├── ExceptionCode.php
│   │   ├── ExceptionMessage.php
│   │   ├── ExceptionMessageRegExp.php
│   │   ├── FileExists.php
│   │   ├── GreaterThan.php
│   │   ├── IsAnything.php
│   │   ├── IsEmpty.php
│   │   ├── IsEqual.php
│   │   ├── IsFalse.php
│   │   ├── IsFinite.php
│   │   ├── IsIdentical.php
│   │   ├── IsInfinite.php
│   │   ├── IsInstanceOf.php
│   │   ├── IsJson.php
│   │   ├── IsNan.php
│   │   ├── IsNull.php
│   │   ├── IsTrue.php
│   │   ├── IsType.php
│   │   ├── JsonMatches
│   │   │   └── ErrorMessageProvider.php
│   │   ├── JsonMatches.php
│   │   ├── LessThan.php
│   │   ├── Not.php
│   │   ├── ObjectHasAttribute.php
│   │   ├── Or.php
│   │   ├── PCREMatch.php
│   │   ├── SameSize.php
│   │   ├── StringContains.php
│   │   ├── StringEndsWith.php
│   │   ├── StringMatches.php
│   │   ├── StringStartsWith.php
│   │   ├── TraversableContains.php
│   │   ├── TraversableContainsOnly.php
│   │   └── Xor.php
│   ├── Constraint.php
│   ├── Error
│   │   ├── Deprecated.php
│   │   ├── Notice.php
│   │   └── Warning.php
│   ├── Error.php
│   ├── Exception.php
│   ├── ExceptionWrapper.php
│   ├── ExpectationFailedException.php
│   ├── IncompleteTest.php
│   ├── IncompleteTestCase.php
│   ├── IncompleteTestError.php
│   ├── InvalidCoversTargetException.php
│   ├── OutputError.php
│   ├── PHPUnit_Framework_CoveredCodeNotExecutedException.php
│   ├── PHPUnit_Framework_MissingCoversAnnotationException.php
│   ├── RiskyTest.php
│   ├── RiskyTestError.php
│   ├── SelfDescribing.php
│   ├── SkippedTest.php
│   ├── SkippedTestCase.php
│   ├── SkippedTestError.php
│   ├── SkippedTestSuiteError.php
│   ├── SyntheticError.php
│   ├── Test.php
│   ├── TestCase.php
│   ├── TestFailure.php
│   ├── TestListener.php
│   ├── TestResult.php
│   ├── TestSuite
│   │   └── DataProvider.php
│   ├── TestSuite.php
│   ├── UnintentionallyCoveredCodeError.php
│   ├── Warning.php
│   └── WarningTestCase.php
├── Runner
│   ├── BaseTestRunner.php
│   ├── Exception.php
│   ├── Filter
│   │   ├── Factory.php
│   │   ├── Group
│   │   │   ├── Exclude.php
│   │   │   └── Include.php
│   │   ├── Group.php
│   │   └── Test.php
│   ├── StandardTestSuiteLoader.php
│   ├── TestSuiteLoader.php
│   └── Version.php
├── TextUI
│   ├── Command.php
│   ├── ResultPrinter.php
│   └── TestRunner.php
└── Util
    ├── Blacklist.php
    ├── Configuration.php
    ├── ConfigurationGenerator.php
    ├── ErrorHandler.php
    ├── Fileloader.php
    ├── Filesystem.php
    ├── Filter.php
    ├── Getopt.php
    ├── GlobalState.php
    ├── InvalidArgumentHelper.php
    ├── Log
    │   ├── JSON.php
    │   ├── JUnit.php
    │   ├── TAP.php
    │   └── TeamCity.php
    ├── PHP
    │   ├── Default.php
    │   ├── Template
    │   │   └── TestCaseMethod.tpl.dist
    │   ├── Windows.php
    │   └── eval-stdin.php
    ├── PHP.php
    ├── Printer.php
    ├── Regex.php
    ├── String.php
    ├── Test.php
    ├── TestDox
    │   ├── NamePrettifier.php
    │   ├── ResultPrinter
    │   │   ├── HTML.php
    │   │   ├── Text.php
    │   │   └── XML.php
    │   └── ResultPrinter.php
    ├── TestSuiteIterator.php
    ├── Type.php
    └── XML.php

PHPUnitのメイン処理

PHPUnitでテストを実行するときのメイン処理をざくっと見てみます。
20161019200553
PHPUnit_TextUI_Command
PHPUnit_TextUI_TestRunner
TextUIのCommandとTestRunnerはphpunitのエントリポイントでphpunit実行時CommandからTestRunnerを呼び出し処理を開始します。
PHPUnit_Framework_Test(interface)
PHPUnit_Framework_TestSuite
TestRunnerはTestSuiteを作成しTestSuiteのメイン処理であるrun処理を実行します。
20161019195137
PHPUnit_Framework_Test(interface)
PHPUnit_Framework_Assert
PHPUnit_Framework_TestCase
PHPUnit_Framework_TestResult
20161019195138
TestSuiteはforeachにて順次TestCaseのテストを実行します。また、TestCaseの親クラスにはAssertがいてアサーションができるようになっています。

テストを実行する該当ソースをチェックする

TestSuite.php

TestRunnerから呼び出されたTestSuiteは、run処理にて対象テストをforeachして順次テストを実行します。

foreach ($this as $test) {
    if ($result->shouldStop()) {
        break;
    }
    if ($test instanceof PHPUnit_Framework_TestCase ||
        $test instanceof self) {
        $test->setbeStrictAboutChangesToGlobalState($this->beStrictAboutChangesToGlobalState);
        $test->setBackupGlobals($this->backupGlobals);
        $test->setBackupStaticAttributes($this->backupStaticAttributes);
        $test->setRunTestInSeparateProcess($this->runTestInSeparateProcess);
    }
    $test->run($result);
}

TestCase.php

TestCaseのrun処理ではTestResultのrun処理を実行します。

if ($result === null) {
    $result = $this->createResult();
}
  ...省略...
$result->run($this);

TestResult.php

TestResultのrun処理では、渡されたTestCaseのrunBare()を実行します。

$test->runBare();

TestCase.php

TestCaseのrunBare処理がテスト実行の最終地点で、before・after等のhookポイントを使いsetUp、テスト、tearDownを実行します。TestResultから呼び出しているのはテスト結果をTestResultが処理する為でしょう。実行後は順次TestResultが処理の呼び出し元に順に返され処理が終了します。

foreach ($hookMethods['before'] as $method) {
    $this->$method();
}
$this->assertPreConditions();
$this->testResult = $this->runTest();
$this->verifyMockObjects();
$this->assertPostConditions();

かなりざっくりではありますがphpunitのメイン処理を確認してみました。だいたいの処理の流れは分かったので次回はモックオブジェクトを見ていこうと思います。