Doctrineを使ったライブラリの単体テストを書くためにしたこと


## はじめに
`Doctrine ORM`の`Entity`や`Repository`だけのライブラリがあり、動作確認はそのライブラリを利用しているアプリケーション側で行っていました。
プロジェクトに参画したばかりで、そのライブラリに変更を加えるのは少々怖かったので、単体テストを追加してみました。

## ディレクトリ構成
単体テストがない状態のディレクトリ構成は以下のようなイメージです。

```bash
$ tree -d .
.
├── Entity
├── Repository
└── vendor # composer installで自動的に追加される
```

## したこと
Symfony本家のBundleやライブラリを参考にしました。

- e.g. [symfony/framework-bundle](https://github.com/symfony/framework-bundle)

### phpunit の追加
`composer`で`phpunit`を追加します。`--dev`を付けておきましょう。

```bash
$ composer require --dev phpunit/phpunit 
```

### `Tests`ディレクトリの追加、`autoload`しないように除外
テストコードを格納するために`Tests`ディレクトリを追加します。
そのままだと、通常の実行時にもテストコードが読み込まれてしまうので、下記のように`exclude-from-classmap`を指定します。

```json
# composer.json
    "autoload": {
        "psr-4": {"Namespace\\To\\Library\\": ""},
        "exclude-from-classmap": [
            "/Tests/"
        ]
    },
```

念のため、`autoload`ファイルに変更を反映しておきます。

```bash
$ composer dump-autoload
```

### phpunit.xml の作成
下記のように`phpunit.xml`を作成します。

```xml
<?xml version="1.0" encoding="UTF-8"?>

<!-- https://phpunit.de/manual/current/en/appendixes.configuration.html -->
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:noNamespaceSchemaLocation="http://schema.phpunit.de/4.8/phpunit.xsd"
         backupGlobals="false"
         colors="true"
         bootstrap="vendor/autoload.php"
>
    <php>
        <env name="DATABASE_DRIVER" value="pdo_pgsql" />
        <env name="DATABASE_HOST" value="localhost" />
        <env name="DATABASE_PORT" value="" />
        <env name="DATABASE_USER" value="postgres" />
        <env name="DATABASE_PASSWORD" value="" />
        <env name="DATABASE_NAME" value="example" />
    </php>

    <testsuites>
        <testsuite name="Project Test Suite">
            <directory>./Tests/</directory>
        </testsuite>
    </testsuites>

    <filter>
        <whitelist>
            <directory>./</directory>
            <exclude>
                <directory>./Tests/</directory>
                <directory>./vendor/</directory>
            </exclude>
        </whitelist>
    </filter>
</phpunit>
```

いくつかポイントがあります。

#### `bootstrap="vendor/autoload.php"`
`vendor/autoload.php`を指定することで、必要なソースが読み込まれるようになります。

#### 環境変数にて、データベースの接続情報を設定
テスト実行時に必要な環境変数を`<php>`要素の下に記述します。

#### whitelist
コードカバレッジ対象のファイルのホワイトリストを記述します。
`Tests`と`vendor`ディレクトリは除外しておきましょう。

#### 参考URL
- [XML 設定ファイルを用いたテストスイートの構成](https://phpunit.de/manual/current/ja/organizing-tests.html#organizing-tests.xml-configuration)
- [PHP INI 項目や定数、グローバル変数の設定](https://phpunit.de/manual/current/ja/appendixes.configuration.html#appendixes.configuration.php-ini-constants-variables)
- [コードカバレッジ対象のファイルのホワイトリスト](https://phpunit.de/manual/current/ja/appendixes.configuration.html#appendixes.configuration.whitelisting-files)


### `EntityManager`の生成
`Doctrine ORM`を利用するために、`EntityManager`を生成します。
実装方法は色々あると思いますが、今回は`PHPUnit_Framework_TestCase`を継承した基底クラスにて実装しました。

```php
<?php

namespace Namespace\To\Library\Tests;

use Doctrine\DBAL\Types\Type;
use Doctrine\ORM\EntityManager;
use Doctrine\ORM\Tools\Setup;

abstract class AbstractTestCase extends \PHPUnit_Framework_TestCase
{
    /** @var EntityManager */
    private $entityManager;

    /**
     * @return EntityManager
     */
    protected function getEntityManager()
    {
        if (empty($this->entityManager)) {
            $this->entityManager = $this->createEntityManager();
        }

        return $this->entityManager;
    }

    /**
     * @return EntityManager
     */
    private function createEntityManager()
    {
        $entityPaths = [realpath(__DIR__. '/../Entity')];
        $idDevMode = false;
        $proxyDir = null;
        $cache = null;
        $isSimpleMode = false;

        $dbOptions = [
            'driver' => getenv('DATABASE_DRIVER'),
            'host' => getenv('DATABASE_HOST'),
            'port' => getenv('DATABASE_PORT'),
            'user' => getenv('DATABASE_USER'),
            'password' => getenv('DATABASE_PASSWORD'),
            'dbname' => getenv('DATABASE_NAME'),
        ];

        // @see http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/configuration.html
        // @see http://stackoverflow.com/questions/15099060/doctrine2-class-is-not-a-valid-entity-or-mapped-super-class
        $config = Setup::createAnnotationMetadataConfiguration($entityPaths, $idDevMode, $proxyDir, $cache, $isSimpleMode);
        $config->addEntityNamespace('Library', '\\Namespace\\To\\Library\\Entity');
        $config->setAutoGenerateProxyClasses(true);

        return EntityManager::create($dbOptions, $config);
    }
}
```

基本的には、下記のドキュメントを参考にしましたが、2つほどハマりポイントがあったのでメモしておきます。
[2. Installation and Configuration — Doctrine 2 ORM 2 documentation](http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/configuration.html)

#### `addEntityNamespace`
例えば、ライブラリ内で、下記のように、エイリアス付きで`getRepository`している場合は、`Configuration::addEntityNamespace`にて、名前解決する必要があります。

```php
$entityManager = $this->getEntityManager();
$repository = $entityManager->getRepository('Library:User');
```

名前解決できないと、`ORMException`が発生します。

```
Doctrine\ORM\ORMException: Unknown Entity namespace alias 'Library'.
```

#### `MappingException`が発生する
今回、この例外を回避するのに一番ハマりました。
下記のように、`MappingException`が発生する場合は、`Setup::createAnnotationMetadataConfiguration`の第5引数`$useSimpleAnnotationReader`に`false`を指定します。

```
Doctrine\ORM\Mapping\MappingException: Class "Namespace\To\Library\Entity\User" is not a valid entity or mapped super class.
```

```php
// vendor/doctrine/orm/lib/Doctrine/ORM/Tools/Setup.php

/**
 * Creates a configuration with an annotation metadata driver.
 *
 * @param array   $paths
 * @param boolean $isDevMode
 * @param string  $proxyDir
 * @param Cache   $cache
 * @param bool    $useSimpleAnnotationReader
 *
 * @return Configuration
 */
public static function createAnnotationMetadataConfiguration(array $paths, $isDevMode = false, $proxyDir = null, Cache $cache = null, $useSimpleAnnotationReader = true)
{
    $config = self::createConfiguration($isDevMode, $proxyDir, $cache);
    $config->setMetadataDriverImpl($config->newDefaultAnnotationDriver($paths, $useSimpleAnnotationReader));

    return $config;
}
```

`SimpleAnnotationReader`ではなくて、`AnnotationReader`を使うことでこの問題を回避します。
詳細については、下記の記事が大変参考になります。

- [CodeIgniter + Doctrine 2 + Twig を利用した Web アプリ開発メモ | Netsphere Engineering](http://blog.technolize.net/post/67183734559/codeigniter-doctrine-2-twig-%E3%82%92%E5%88%A9%E7%94%A8%E3%81%97%E3%81%9F-web-%E3%82%A2%E3%83%97%E3%83%AA%E9%96%8B%E7%99%BA%E3%83%A1%E3%83%A2)

### テストケースの実装、テストの実行
あとは、必要なテストケースを実装して、実行するのみです。

```bash
$ vendor/bin/phpunit
PHPUnit 5.7.5 by Sebastian Bergmann and contributors.

..                                                                  2 / 2 (100%)

Time: 346 ms, Memory: 10.00MB

OK (2 tests, 5 assertions)
```

### CIでテスト実行するためには
ローカルでテストを実行するのと同様に、データベースの構築が必要となります。
ダンプしたSQLをリストアするか、マイグレーションを実行するなどして、CI上のコンテナに構築する必要があります。

## おわりに
そもそも`Entity`と`Repository`だけのライブラリを作るユースケースがそんなに無いかもしれませんが、複数のアプリケーション（コンポーネント）にまたがって、同じデータにアクセスする場合は、かなり有効だと感じています。
例えば、ユーザー向けアプリと管理アプリでレポジトリが別とか。
バッチ処理だけ別とか。APIだけ別とか。
で、コードが共通化されて、スッキリする分、影響範囲が大きくなってしまいます。
単体テストでがっちり、[心理的安全性](http://workswitch-ibs.inte.co.jp/project-aristoteles/)を担保しましょう。

ではでは。