はじめに
Symfonyでアプリケーションを開発する際、テストしたい状況ごとにデータベースのデータをYAMLのフィクスチャファイルとして用意しておくと開発がはかどります。Symfonyではバージョン2の頃から、nelmio/aliceおよびaliceを手軽に使うためのバンドルがよく使われています。先日Symfony 3.2がリリースされましたので、この記事では、Symfony 3.2のプロジェクトとしてaliceを使うプロジェクトの雛形を紹介します。
このプロジェクトは、Doctrineの単一テーブル継承(Single Table Inheritance)、および埋め込みエンティティ(ValueObjectまたはEmbedded)を使う例にもなっています。
利用バージョン
記事執筆時点では、以下のバージョンになっています。
- Symfony 3.2.0
- PHPUnit 4.8.29
- liip/functional-test-bundle 1.6.3
- hautelook/alice-bundle 1.3.1
インストール
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
および NormalItem
、CustomOrderItem
、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を利用する方法を紹介する予定です。