はじめに

Symfonyでアプリケーションを開発する際、テストしたい状況ごとにデータベースのデータをYAMLのフィクスチャファイルとして用意しておくと開発がはかどります。Symfonyではバージョン2の頃から、nelmio/aliceおよびaliceを手軽に使うためのバンドルがよく使われています。先日Symfony 3.2がリリースされましたので、この記事では、Symfony 3.2のプロジェクトとしてaliceを使うプロジェクトの雛形を紹介します。

このプロジェクトは、Doctrineの単一テーブル継承(Single Table Inheritance)、および埋め込みエンティティ(ValueObjectまたはEmbedded)を使う例にもなっています。

利用バージョン

記事執筆時点では、以下のバージョンになっています。

インストール

Symfonyプロジェクトは、Symfony Installerでインストールすると楽ですが、記事執筆時点ではSymfony InstallerがSymfony 3.2に対応していなかったため、composerからインストールしました。

$ composer create-project symfony/framework-standard-edition sf32-app-sample201612
$ cd sf32-app-sample201612

これでSymfony Standard Editionのプロジェクトファイル群が作られます。続いて次のコマンドで、ユニットテストおよびYAMLフィクスチャを利用するのに必要なバンドル/ライブラリをインストールします。

$ composer require --dev phpunit/phpunit liip/functional-test-bundle hautelook/alice-bundle

バンドルの有効化

インストールしたバンドルを、app/AppKernel.php にて有効化します。

<?php

use Symfony\Component\HttpKernel\Kernel;
use Symfony\Component\Config\Loader\LoaderInterface;

class AppKernel extends Kernel
{
    public function registerBundles()
    {
        $bundles = [
            new Symfony\Bundle\FrameworkBundle\FrameworkBundle(),
            ...
        ];

        if (in_array($this->getEnvironment(), ['dev', 'test'], true)) {
            ...
+           $bundles[] = new Liip\FunctionalTestBundle\LiipFunctionalTestBundle();
+           $bundles[] = new Hautelook\AliceBundle\HautelookAliceBundle();
        }

テスト用コンフィギュレーションの追記

テストでsqliteを使うため app/config/config_test.yml に、以下を追記します。

+doctrine:
+    dbal:
+        default_connection: default
+        connections:
+            default:
+               driver:   pdo_sqlite
+               path:     "%kernel.cache_dir%/test.db"
+
+liip_functional_test:
+    cache_sqlite_db: true

エンティティの実装

以下の簡単な要件のエンティティを実装します。

  • 商品情報エンティティ
  • 商品名と価格、配送種別を持つ
  • 商品には通常の商品と特注品とがあり、それぞれに特殊なロジックが必要なので、別々のエンティティクラスに分けておきたい

また、今のところ大きな意味はありませんが、価格はValueObjectにしておきます。配送種別は、現段階では単に文字列で種別を格納しておきます。

Doctrineの単一テーブル継承を使って、Item および NormalItemCustomOrderItem、ValueObjectの ItemPrice は、それぞれ以下のようになります。

TIPS エンティティを実装する際、最初の段階から各フィールドの制約(Assert)まで書くようにしておくのは、開発時のドキュメンテーションとしての効果もあってオススメです。

<?php

namespace AppBundle\Entity;

use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;

/**
 * Item
 *
 * @ORM\Table(name="item")
 * @ORM\Entity(repositoryClass="AppBundle\Repository\ItemRepository")
 * @ORM\DiscriminatorColumn(name="item_type", type="string")
 * @ORM\DiscriminatorMap({
 *   "item_abstract": "AppBundle\Entity\Item",
 *   "normal":        "AppBundle\Entity\Item\NormalItem",
 *   "custom_order":  "AppBundle\Entity\Item\CustomOrderItem",
 * })
 * @ORM\InheritanceType(value="SINGLE_TABLE")
 */
abstract class Item
{
    /**
     * @var int
     *
     * @ORM\Column(name="id", type="integer")
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    private $id;

    /**
     * @var string
     *
     * @ORM\Column(name="name", type="string", length=255)
     * @Assert\NotBlank
     */
    private $name;

    /**
     * @var ItemPrice
     * @ORM\Embedded(class="AppBundle\Entity\ItemPrice", columnPrefix="price_")
     * @Assert\Valid
     */
    private $price;

    /**
     * @var string
     * @ORM\Column(name="delivery_time_type", type="string", length=255)
     * @Assert\NotBlank
     * @Assert\Choice(choices={"instant", "estimation"})
     */
    private $deliveryTimeType;

    /**
     * Get id
     *
     * @return int
     */
    public function getId()
    {
        return $this->id;
    }

    : 以下getter/setter
}
<?php
namespace AppBundle\Entity\Item;

use Doctrine\ORM\Mapping as ORM;
use AppBundle\Entity\Item;

/**
 * @ORM\Entity()
 */
class NormalItem extends Item
{

}
<?php
namespace AppBundle\Entity\Item;

use Doctrine\ORM\Mapping as ORM;
use AppBundle\Entity\Item;

/**
 * @ORM\Entity()
 */
class CustomOrderItem extends Item
{

}
<?php
namespace AppBundle\Entity;

use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;

/**
 * @ORM\Embeddable
 */
class ItemPrice
{
    /**
     * @var int
     *
     * @ORM\Column(name="price", type="integer")
     * @Assert\NotBlank
     * @Assert\GreaterThanOrEqual(0)
     */
    private $price;

    /**
     * @return int
     */
    public function getPrice()
    {
        return $this->price;
    }

    /**
     * @param int $price
     */
    public function setPrice($price)
    {
        $this->price = $price;
    }

    public function __toString()
    {
        return (string)$this->price;
    }
}

DBのテーブルを生成

ここで一旦、エンティティの実装および定義が有効か確認するために、データベースのスキーマを生成しておきましょう。

$ php bin/console doctrine:database:create
$ php bin/console doctrine:schema:create

テストディレクトリの準備

Symfony3では、アプリケーション/バンドルの標準ディレクトリ構成が変更になり、これまでは各バンドルの中に配置されていたテスト関係のファイルを、プロジェクトルートディレクトリの tests 配下へまとめるようになりました。 YAMLのフィクスチャファイルもテスト時に利用するものなので、tests 配下の方へ配置します。このサンプルでは、以下のような配置にしました。

tests
└── AppBundle
    ├── Functional
    │   └── Entity
    │       └── ItemTest.php
    └── Resources
        └── fixtures
            └── items.yml

YAMLフィクスチャファイル

商品情報を登録するYAMLフィクスチャファイルは、以下のようになります。ValueObjectを使っているItemPriceの部分がやや手間ですが、スッキリと定義できますね。aliceの細かな機能については、aliceのドキュメントを参照してください。

AppBundle\Entity\ItemPrice(local):
  price1:
    price: 100
  price2:
    price: 200
  price3:
    price: 300
  price4:
    price: 400
  price5:
    price: 500

AppBundle\Entity\Item\NormalItem:
  normal_item1:
    name: 商品1
    price: "@price1"
    deliveryTimeType: instant
  normal_item2:
    name: 商品2
    price: "@price2"
    deliveryTimeType: instant
  normal_item3:
    name: 商品3
    price: "@price3"
    deliveryTimeType: estimation

AppBundle\Entity\Item\CustomOrderItem:
  custom_order_item1:
    name: カスタム商品1
    price: "@price4"
    deliveryTimeType: estimation
  custom_order_item2:
    name: カスタム商品2
    price: "@price5"
    deliveryTimeType: estimation

フィクスチャを読み込むテスト

次は、YAMLのフィクスチャをテストコードから読み込んでみましょう。ItemTest.phpを以下のように作成します。

<?php
namespace AppBundle\Functional\Entity;

use Liip\FunctionalTestBundle\Test\WebTestCase;
use AppBundle\Entity\Item;

class ItemTest extends WebTestCase
{
    public function test()
    {
        $this->loadFixtureFiles([
            __DIR__.'/../../Resources/fixtures/items.yml',
        ]);

        $em = static::getContainer()->get('doctrine')->getManager();

        $items = $em->getRepository(Item::class)->findAll();
        foreach ($items as $item) {
            echo get_class($item) . PHP_EOL;
            echo sprintf('%s %s %s',
                $item->getName(),
                $item->getPrice(),
                $item->getDeliveryTimeType()) . PHP_EOL;
        }
    }
}

liip/functional-test-bundleでは、WebTestCaseクラスに loadFixtureFiles() メソッドがあり、バージョン1.5.0以降ではYAMLフィクスチャファイルのパスを引数にして loadFixtureFiles() メソッドを呼び出すだけで、フィクスチャのロードが完了します。

拙著『基本からしっかり学ぶ Symfony2入門』では、利用しているliip/functional-test-bundleのバージョンが古く、この時はわざわざDoctrineFixturesBundle形式のローダークラスを用意しないといけませんでした(そのためにDoctrineFixturesBundleのインストールも必要でした)。これらは今は不要です。便利になりましたね。

プロジェクト全体のテストを実行すると、以下のようにテキストが出力されます。

$ vendor/bin/phpunit
PHPUnit 4.8.29 by Sebastian Bergmann and contributors.

.AppBundle\Entity\Item\CustomOrderItem
カスタム商品1 400 estimation
AppBundle\Entity\Item\CustomOrderItem
カスタム商品2 500 estimation
AppBundle\Entity\Item\NormalItem
商品1 100 instant
AppBundle\Entity\Item\NormalItem
商品2 200 instant
AppBundle\Entity\Item\NormalItem
商品3 300 estimation


Time: 416 ms, Memory: 12.00MB

おわりに

この記事ではSymfony 3.2でYAMLフィクスチャを使う方法と、Doctrineの単一テーブル継承、ValueObjectを使う例をざっと紹介しました。この記事の応用編として、次回はInterfaceとそれを実装する複数の具象クラスから成るValueObjectを利用する方法を紹介する予定です。