Symfony2のblogチュートリアルをリファクタリングしてみた(repositoryとformのテスト)
前回の記事の続きです。
今回は、テストコードの紹介が中心です。
動作確認環境
- Symfony 2.0.11
- PHP 5.3.10
- PHPUnit 3.6.10
目次
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