karakaram-blog

Symfony2のblogチュートリアルをリファクタリングしてみた(repositoryとformのテスト)

 ツイート 0  シェア 0  Google+1 0  Hatena 1

symfony-logo

前回の記事の続きです。
今回は、テストコードの紹介が中心です。

動作確認環境

  • Symfony 2.0.11
  • PHP 5.3.10
  • PHPUnit 3.6.10

目次

  1. 動作確認環境
  2. Repositoryクラスのテストコード
  3. Formクラスのテストコード
  4. コントローラのテストコードのリファクタリング
  5. おわりに

Repositoryクラスのテストコード

Repositoryクラスのテストは、データベースを正しく操作できているか確認しています。

<?php
// src/My/BlogBundle/Tests/Repository/PostRepositoryTest.php

namespace My\BlogBundle\Tests\Repository;

use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
use Symfony\Bundle\DoctrineFixturesBundle\Common\DataFixtures\Loader;
use Doctrine\Common\DataFixtures\Executor\ORMExecutor;
use Doctrine\Common\DataFixtures\Purger\ORMPurger;
use My\BlogBundle\DataFixtures\ORM\LoadPostData;
use My\BlogBundle\Entity\Post;

class PostRepositoryTest extends WebTestCase
{
    /**
     * @var \Doctrine\ORM\EntityManager
     */
    private $em;

    /**
     * @var \My\BlogBundle\Repository\PostRepository
     */
    private $postRepository;

    /**
     * @var \Symfony\Component\DependencyInjection\ContainerInterface
     */
    private $container;

    public function setUp()
    {
        $kernel = static::createKernel();
        $kernel->boot();
        $this->container = $kernel->getContainer();
        $loader = new Loader($this->container);
        $loader->addFixture(new LoadPostData);
        $fixtures = $loader->getFixtures();
        $this->em = $this->container->get('doctrine.orm.entity_manager');
        $purger = new ORMPurger($this->em);
        $purger->setPurgeMode(ORMPurger::PURGE_MODE_TRUNCATE);
        $executor = new ORMExecutor($this->em, $purger);
        $executor->execute($fixtures);

        $this->postRepository = $this->em->getRepository('MyBlogBundle:Post');
    }

    public function testSearch()
    {
        $posts = $this->postRepository->search();
        $post = $posts[0];
        $this->assertSame($post->getTitle(), 'title');
    }

    public function testSearchOneById()
    {
        $post = $this->postRepository->searchOneById(1);
        $this->assertSame($post->getTitle(), 'title');
    }

    /**
     * @expectedException Doctrine\ORM\NoResultException
     */
    public function testSearchOneByIdに存在しないidを指定したら例外を投げる()
    {
        $post = $this->postRepository->searchOneById(-1);
    }

    public function testInsert()
    {
        $post = new Post();
        $post->setTitle('title');
        $post->setBody('bodybodybody');
        $this->assertTrue($this->postRepository->insert($post));

        $query = $this->postRepository
            ->createQueryBuilder('p')
            ->orderBy('p.id', 'DESC')
            ->getQuery()
            ->setMaxResults(1);
        $posts = $query->getResult();
        $post = $posts[0];
        $this->assertSame('title', $post->getTitle());
        $this->assertSame('bodybodybody', $post->getBody());
    }

    public function testDelete()
    {
        $this->assertTrue($this->postRepository->delete(1));

        $query = $this->postRepository
            ->createQueryBuilder('p')
            ->where('p.id = :id')
            ->setParameter('id', 1)
            ->getQuery();
        $posts = $query->getResult();
        $this->assertSame(array(), $posts);
    }

    public function testUpdate()
    {
        $post = $this->postRepository->searchOneById(1);
        $post->setTitle('edit_title');
        $post->setBody('edit_bodybodybody');
        $this->assertTrue($this->postRepository->update($post));

        $query = $this->postRepository
            ->createQueryBuilder('p')
            ->where('p.id = :id')
            ->setParameter('id', 1)
            ->getQuery();
        $posts = $query->getResult();
        $post = $posts[0];
        $this->assertSame('edit_title', $post->getTitle());
        $this->assertSame('edit_bodybodybody', $post->getBody());
    }

}

Formクラスのテストコード

Formクラスのテストは、入力値のバリデートが正しく動いているか確認しています。

文字数エラーのメッセージに {{ limit }} の文字が含まれています。twigに出力するためのメッセージと思いますが、この中身を取り出す方法がわかりませんでした。

<?php
// src/My/BlogBundle/Tests/Form/PostTypeTest.php

namespace My\BlogBundle\Form;

use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
use My\BlogBundle\Form\PostType;
use My\BlogBundle\Entity\Post;

class PostTypeTest extends WebTestCase
{
    private $container;
    private $token;

    public function setUp()
    {
        $kernel = static::createKernel();
        $kernel->boot();
        $this->container = $kernel->getContainer();
        $this->token = $this->container->get('form.csrf_provider')->generateCsrfToken('unknown');
    }

    public function test正常系()
    {
        $form = $this->container->get('form.factory')->create(new PostType, new Post());
        $data['title'] = 'title';
        $data['body'] = 'bodybodybody';
        $data['_token'] = $this->token;
        $form->bind($data);
        $this->assertTrue($form->isValid());
    }

    public function test必須チェック()
    {
        $form = $this->container->get('form.factory')->create(new PostType, new Post());
        $data['title'] = '';
        $data['body'] = '';
        $data['_token'] = $this->token;
        $form->bind($data);
        $this->assertFalse($form->isValid());

        $childForms = $form->getChildren();
        $this->assertTrue($childForms['title']->hasErrors());
        $errorForms = $childForms['title']->getErrors();
        $errorMessage = $errorForms[0]->getMessageTemplate();
        $this->assertSame($errorMessage, 'This value should not be blank');
        $this->assertTrue($childForms['body']->hasErrors());
        $errorForms = $childForms['body']->getErrors();
        $errorMessage = $errorForms[0]->getMessageTemplate();
        $this->assertSame($errorMessage, 'This value should not be blank');
    }

    public function test最小文字数チェック()
    {
        $form = $this->container->get('form.factory')->create(new PostType, new Post());
        $data['title'] = '1';
        $data['body'] = '1';
        $data['_token'] = $this->token;
        $form->bind($data);
        $this->assertFalse($form->isValid());

        $childForms = $form->getChildren();
        $this->assertTrue($childForms['title']->hasErrors());
        $errorForms = $childForms['title']->getErrors();
        $errorMessage = $errorForms[0]->getMessageTemplate();
        $this->assertSame($errorMessage, 'This value is too short. It should have {{ limit }} characters or more');
        $this->assertTrue($childForms['body']->hasErrors());
        $errorForms = $childForms['body']->getErrors();
        $errorMessage = $errorForms[0]->getMessageTemplate();
        $this->assertSame($errorMessage, 'This value is too short. It should have {{ limit }} characters or more');
    }

    public function test最大文字数チェック()
    {
        $form = $this->container->get('form.factory')->create(new PostType, new Post());
        $longCharacter = '';
        for ($i=0; $i < 51; $i++) {
            $longCharacter .= 'a';
        }
        $data['title'] = $longCharacter;
        $data['body'] = $longCharacter;
        $data['_token'] = $this->token;
        $form->bind($data);
        $this->assertFalse($form->isValid());

        $childForms = $form->getChildren();
        $this->assertTrue($childForms['title']->hasErrors());
        $errorForms = $childForms['title']->getErrors();
        $errorMessage = $errorForms[0]->getMessageTemplate();
        $this->assertSame($errorMessage, 'This value is too long. It should have {{ limit }} characters or less');
        $this->assertFalse($childForms['body']->hasErrors());
    }

}

コントローラのテストコードのリファクタリング

RepositoryクラスのテストとFormクラスのテストができましたので、コントローラのテストから重複部分を削除しました。

作業の途中で悩んだのが、コントローラのテストは何をテストするべきなのか?ということです。

Symfony2のマニュアルや、webのリファレンスを見て、とりあえず下記の観点でのテストのみ残すことにしました。

  • ルーティングのテスト
  • リクエストに対して、レスポンスの内容は期待通りか
  • コントローラから、モジュールはきちんと呼び出されているか

モジュールがコントローラから呼び出されているかのテストは、厳密にはできていません。

「リクエストに対してこのレスポンスがあれば、ひとまずモジュールの呼び出しはできているだろう」といったレベルのテストとなっています。

今後、よいテスト方法が見つかれば紹介したいと思います。

<?php
// src/My/BlogBundle/Tests/Controller/DefaultControllerTest.php

namespace My\BlogBundle\Tests\Controller;

use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
use Symfony\Bundle\FrameworkBundle\Client;
use Symfony\Component\DomCrawler\Form;
use Doctrine\Common\DataFixtures\Loader;
use Doctrine\Common\DataFixtures\Executor\ORMExecutor;
use Doctrine\Common\DataFixtures\Purger\ORMPurger;
use My\BlogBundle\DataFixtures\ORM\LoadPostData;

/**
 * DefaultControllerTest
 *
 * @uses \Symfony\Bundle\FrameworkBundle\Test\WebTestCase
 */
class DefaultControllerTest extends WebTestCase
{
    public function setUp()
    {
        $kernel = static::createKernel();
        $kernel->boot();
        $loader = new Loader($kernel->getContainer());
        $loader->addFixture(new LoadPostData);
        $fixtures = $loader->getFixtures();
        $em = $kernel->getContainer()->get('doctrine.orm.entity_manager');
        $purger = new ORMPurger($em);
        $purger->setPurgeMode(ORMPurger::PURGE_MODE_TRUNCATE);
        $executor = new ORMExecutor($em, $purger);
        $executor->execute($fixtures);
    }

    /**
     * 一覧画面が表示されるかテストする
     */
    public function test一覧画面が表示される()
    {
        $client = static::createClient();
        $crawler = $client->request('GET', '/blog/');
        $this->assertTrue($client->getResponse()->isSuccessful());
        $body = $client->getResponse()->getContent();
        $this->assertSame(1, substr_count($body, 'Blog posts'));
    }

    /**
     * 登録ができるかテストする
     *
     */
    public function test登録ができる()
    {
        $client = static::createClient();
        $crawler = $client->request('GET', '/blog/new');
        $this->assertTrue($client->getResponse()->isSuccessful());
        $body = $client->getResponse()->getContent();
        $this->assertSame(1, substr_count($body, 'Add Post'));
        $form = $crawler->selectButton('Save Post')->form();
        $form['post[title]'] = 'title';
        $form['post[body]'] = 'bodybodybody';
        $crawler = $client->submit($form);
        $this->assertTrue($client->getResponse()->isRedirection());
        $crawler = $client->followRedirect();
        $this->assertTrue($client->getResponse()->isSuccessful());
        $body = $client->getResponse()->getContent();
        $this->assertSame(1, substr_count($body, '記事を追加しました'));
    }

    /**
     * 登録画面のバリデーションが機能しているかテストする
     */
    public function test登録画面のバリデーションが機能する()
    {
        $client = static::createClient();
        $crawler = $client->request('GET', '/blog/new');
        $form = $crawler->selectButton('Save Post')->form();
        $this->登録画面と編集画面のバリデーションが機能する($client, $form);
    }

    /**
     * 詳細画面が表示されるかテストする
     */
    public function test詳細画面が表示される()
    {
        $client = static::createClient();
        $crawler = $client->request('GET', '/blog/1/show');
        $this->assertTrue($client->getResponse()->isSuccessful());
        $body = $client->getResponse()->getContent();
        $this->assertSame(1, substr_count($body, 'bodybodybody'));
    }

    /**
     * 削除ができるかテストする
     */
    public function test削除ができる()
    {
        $client = static::createClient();
        $crawler = $client->request('GET', '/blog/1/delete');
        $this->assertTrue($client->getResponse()->isRedirection());
        $crawler = $client->followRedirect();
        $this->assertTrue($client->getResponse()->isSuccessful());
        $body = $client->getResponse()->getContent();
        $this->assertSame(1, substr_count($body, '記事を削除しました'));
    }

    /**
     * 編集ができるかテストする
     */
    public function test編集ができる()
    {
        $client = static::createClient();
        $crawler = $client->request('GET', '/blog/1/edit');
        $this->assertTrue($client->getResponse()->isSuccessful());
        $body = $client->getResponse()->getContent();
        $this->assertSame(1, substr_count($body, 'Edit Post'));
        $form = $crawler->selectButton('Save Post')->form();
        $form['post[title]'] = 'edit_title';
        $form['post[body]'] = 'edit_bodybodybody';
        $crawler = $client->submit($form);
        $crawler = $client->followRedirect();
        $this->assertTrue($client->getResponse()->isSuccessful());
        $body = $client->getResponse()->getContent();
        $this->assertSame(1, substr_count($body, '記事を編集しました'));
    }

    /**
     * 編集画面のバリデーションが機能するかテストする
     */
    public function test編集画面のバリデーションが機能する()
    {
        $client = static::createClient();
        $crawler = $client->request('GET', '/blog/1/edit');
        $this->assertTrue($client->getResponse()->isSuccessful());
        $form = $crawler->selectButton('Save Post')->form();
        $this->登録画面と編集画面のバリデーションが機能する($client, $form);
    }

    /**
     * 登録画面と編集画面のバリデーションが機能するかテストする
     * 必須チェックのみテストし、他のパターンはformクラスのテストに委ねる
     *
     * @param Symfony\Bundle\FrameworkBundle\Client $client
     * @param Symfony\Component\DomCrawler\Form $form
     */
    private function 登録画面と編集画面のバリデーションが機能する(Client $client, Form $form)
    {
        //必須チェック
        $form['post[title]'] = '';
        $form['post[body]'] = '';
        $crawler = $client->submit($form);
        $body = $client->getResponse()->getContent();
        $this->assertSame(2, substr_count($body, 'This value should not be blank'));
    }

    /**
     * URLに不正な値を設定した時エラーとなるかテストする
     */
    public function testURLに不正な値を設定した時NotFoundを返す()
    {
        $client = static::createClient();
        $crawler = $client->request('GET', '/blog/a/show');
        $this->assertTrue($client->getResponse()->isNotFound());
        $crawler = $client->request('GET', '/blog/-1/show');
        $this->assertTrue($client->getResponse()->isNotFound());
        $crawler = $client->request('GET', '/blog/a/delete');
        $this->assertTrue($client->getResponse()->isNotFound());
        $crawler = $client->request('GET', '/blog/-1/delete');
        $this->assertTrue($client->getResponse()->isNotFound());
        $crawler = $client->request('GET', '/blog/a/edit');
        $this->assertTrue($client->getResponse()->isNotFound());
        $crawler = $client->request('GET', '/blog/-1/edit');
        $this->assertTrue($client->getResponse()->isNotFound());
    }

}

おわりに

4回に渡って紹介したSymfony2のblogチュートリアルの写経は、今回でいったん終了とします。

ソースコードはgithubにありますので、興味のある方はご自由にどうぞ。
https://github.com/karakaram/symfony2-blog-tutorial

 ツイート 0  シェア 0  Google+1 0  Hatena 1