taisablog

taisa's engineer blog

PHP

PHPによるDBUnit超入門

投稿日:March 29, 2019 更新日:

例えば簡単な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';
e

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

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

-PHP

執筆者:

関連記事

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 = []; …

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

前回はPHPUnitのメイン処理を確認しました。今回はPHPUnitデフォルトのモックオブジェクトの仕組みを確認してみます。公式ドキュメントでは、第9章 テストダブルが該当箇所となります。 PHPUnitのモックオブジェクトについて PHPUnitは以下のような構成ですが、その中の「phpunit-mock-objects」がPHPUnitデフォルトのモックライブラリとなります。 phpunitphp-code-coveragephp-file-iteratorphp-text-templatephp-timerphp-token-streamphpunitphpunit-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 │   …

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

前回に引き続きPHPUnit関連の記事です。今回はPHPのモッキンフレームワークであるPhakeの使い方を確認します。 Phakeについて 作者は? Mike Livelyという方のようです。 twitterGitHub Phakeって? こちらですね。 PhakeGitHub ファイル構成 一応ファイル構成を載せておきます。 . ├── 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 │   …

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”: { …