codeception
PHP7
Symfony4
SymfonyDay 14

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

Symfony4のRepositoryのメソッドに対するテストで、公式の方法ではなく、Codeceptionを使ってテストする方法をご紹介します。

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でインストールします。

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ファイルを編集します。

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

SymfonyDoctrine2Dbモジュールを追加します。今回はSQLiteを使いましたが、MySQLの場合はdsnとuser, passwordを設定します。編集後、このモジュールを利用できるように以下のコマンドを実行します。

vendor/bin/codecept build

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

テストを作る

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

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

これで、tests/unit/Repository/ProductRepositoryTest.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のスタブを作ったりでき、これを機能テストやブラウザテストでも利用することができます。ぜひ一度おためしください!!