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';
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

関連記事