Symfony4のRepositoryをCodeceptionのDoctrine2モジュールを使ってUnitテストする

Symfony4のRepositoryのメソッドに対するテストで、[公式の方法](https://symfony.com/doc/current/testing/doctrine.html)ではなく、[Codeception](https://codeception.com/)を使ってテストする方法をご紹介します。

## Codeceptionとは
CodeceptionはPHPのテスティングフレームワークです。以下のようなテストを一括で管理、実行できます。

- Unitテスト
- 機能テスト
- フレームワークでの動作テスト
- ブラウザでのテスト
- APIのテスト

これらのテストを一元管理できるのが好きで、このCodeceptionをよく使っています。
詳しくはこちらのスライドをご覧ください。

https://speakerdeck.com/ippey/tesutotozhong-liang-kusurutamefalsecodeceptionfalsehazimekata-number-phpkansai?slide=36


## サンプルプログラム
テストするためのサンプルプログラムを作っていきます。今回商品テーブルのデータを扱うProductRepository,Productエンティティを用意しました。Productエンティティは

```
name: 商品名
price: 価格
isPublished: 公開・非公開
createdAt: 登録日時
updatedAt : 更新日時
```

として、Repositoryに

```src/Repository/ProductRepository.php
<?php

namespace App\Repository;

use App\Entity\Product;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Symfony\Bridge\Doctrine\RegistryInterface;

/**
 * @method Product|null find($id, $lockMode = null, $lockVersion = null)
 * @method Product|null findOneBy(array $criteria, array $orderBy = null)
 * @method Product[]    findAll()
 * @method Product[]    findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
 */
class ProductRepository extends ServiceEntityRepository
{
    public function __construct(RegistryInterface $registry)
    {
        parent::__construct($registry, Product::class);
    }

    /**
     * 公開フラグで検索
     * 
     * @param $isPublished
     * @return mixed
     */
    public function findByIsPublished($isPublished)
    {
        return $this->createQueryBuilder('p')
            ->where('p.isPublished = :isPublished')
            ->setParameter(':isPublished', $isPublished)
            ->orderBy('p.id', 'ASC')
            ->getQuery()
            ->getResult();
    }
}
```

公開・非公開のフラグの値で検索するメソッドを作成しました。このメソッドをCodeceptionでUnitテストしてみます。
（データベース・テーブル作成については割愛します。）

## Codeceptionの準備

### インストール
まずはインストールです。composerでインストールします。

```shell
cd your-project
composer require codeception/codeception
# わーっとインストールが進んだ後
Symfony operations: 2 recipes (c93d57139f97ec473429f36a0ba32009)
  - Configuring phpunit/phpunit (>=4.7): From github.com/symfony/recipes:master
  -  WARNING  codeception/codeception (>=2.3): From github.com/symfony/recipes-contrib:master
    The recipe for this package comes from the "contrib" repository, which is open to community contributions.
    Review the recipe at https://github.com/symfony/recipes-contrib/tree/master/codeception/codeception/2.3

    Do you want to execute this recipe?
    [y] Yes
    [n] No
    [a] Yes for all packages, only for the current installation session
    [p] Yes permanently, never ask again for this project
    (defaults to n): y # ここでyを選択
```
### 設定　
unit.suite.ymlファイルを編集します。

```yaml:tests/unit.suite.yml
# Codeception Test Suite Configuration
#
# Suite for unit or integration tests.

actor: UnitTester
modules:
    enabled:
        - Asserts
        - \App\Tests\Helper\Unit
        - Symfony:
              app_path: 'src'
              environment: 'dev'
        - Doctrine2:
              depends: Symfony
        - Db:
              dsn: "sqlite:var/data.db"
              user: ""
              password: ""
              populate: false
```

[Symfony](https://codeception.com/docs/modules/Symfony)、[Doctrine2](https://codeception.com/docs/modules/Doctrine2)、[Db](https://codeception.com/docs/modules/Db)モジュールを追加します。今回はSQLiteを使いましたが、MySQLの場合はdsnとuser, passwordを設定します。編集後、このモジュールを利用できるように以下のコマンドを実行します。

```shell
vendor/bin/codecept build
```

これで、Codeceptionの準備は完了です。

## テストを作る
まず、以下のコマンドでUnitTestのファイルを作成します。

```shell
vendor/bin/codecept g:test unit Repository/ProductRepository
```

これで、tests/unit/Repository/ProductRepositoryTest.phpが作成されるので、編集していきます。

```php:tests/unit/Repository/ProductRepositoryTest.php
<?php namespace App\Tests\Repository;

use App\Entity\Product;
use Codeception\Module\Doctrine2;

class ProductRepositoryTest extends \Codeception\Test\Unit
{
    /**
     * @var \App\Tests\UnitTester
     */
    protected $tester;
    
    protected function _before()
    {
    }

    protected function _after()
    {
    }

    // tests

    /**
     * @throws \Codeception\Exception\ModuleException
     */
    public function testSomeFeature()
    {
        $this->createTestData();

        /** @var Doctrine2 $module */
        $module = $this->getModule('Doctrine2');
        $repository = $module->em->getRepository(Product::class);

        $rows = $repository->findByIsPublished(true);
        $this->assertCount(2, $rows);

        $rows = $repository->findByIsPublished(false);
        $this->assertCount(1, $rows);

    }

    /**
     * @throws \Codeception\Exception\ModuleException
     */
    protected function createTestData()
    {
        /** @var Doctrine2 $module */
        $module = $this->getModule('Doctrine2');
        $now = new \DateTime();
        $module->haveInRepository(Product::class, [
            'name' => '商品1',
            'price' => 100,
            'isPublished' => true,
            'createdAt' => $now,
            'updatedAt' => $now,
        ]);
        $module->haveInRepository(Product::class, [
            'name' => '商品2',
            'price' => 200,
            'isPublished' => false,
            'createdAt' => $now,
            'updatedAt' => $now,
        ]);
        $module->haveInRepository(Product::class, [
            'name' => '商品3',
            'price' => 300,
            'isPublished' => true,
            'createdAt' => $now,
            'updatedAt' => $now,
        ]);
    }
}
```

``` $this->getModule('Doctrine2'); ``` でDoctrine2モジュールを取得します。
``` $module->haveInRepository('クラス名', 'データ配列'); ``` で、テストデータを作成します。なお、毎テスト時にトランザクションを開始し、テスト後にロールバックしているのでここで作成したテストデータは保存されることはありません。

``` $module->em ``` は、EntityManagerです。これを利用してProductRepositoryインスタンスを取得します。あとは、インスタンスからメソッドを実行し、想定されたデータが取得できているかテストします。

### 備考
ほんとうは、 haveInRepositoryなどのDoctrine2のメソッドは ``` $this->tester->haveInRepository(); ``` などのように$this->testerから実行できるたんですが、Codeception, Symfonyのバージョンば上がったためか、実行できなくなってました。このあたり、設定が悪いのかもしれないので、設定方法などご存知の方は教えていただきたいです。🙏

## まとめ
今回はデータ取得系のテストでしたが、Doctrine2モジュールにはこの他にもデータが存在するかテストしたり、Repositoryのスタブを作ったりでき、これを機能テストやブラウザテストでも利用することができます。ぜひ一度おためしください！！