Symfony2勉強会 #6 でLTをしてきました。
まとまりのないLTになってしまい、思い出すのも恥ずかしいのですが、LTの内容を補足します。
目次
- LTで話したこと
- 個人的に長いと思っているテストコード
- 分割した後のテストコード(動きません)
- containerから生成したtokenが不正
- テスト時のみ、csrfを無効にすると、様々な弊害が
- CSRF対策が有効かどうかを判定するif文
- DIした情報は、画面遷移で初期化されて消える
- 諸先輩からのアドバイス
- ふりかえり
LTで話したこと
- Symfony2のコントローラのファンクショナルテストは、複数アクションにまたがるので一つ一つのテストケースが長くなりがち。
- テストケースが長いとメンテナンスしづらいので、短くしたい。
- POSTリクエストのアクションをテストしようとして、CSRF対策に苦しんだ。
- SessionをDIしても、テストケース内で画面遷移が発生するとDIした情報が消える。
- 結局何も解決できていない。
個人的に長いと思っているテストコード
下記のように、ひとつのテストケースに複数の画面遷移が発生して、長くなっているテストを分割して短くしようと試みてみました。
public function test成功シナリオ()
{
$client = static::createClient();
// /reservation/new
$crawler = $client->request('GET', '/reservation/new');
$this->assertTrue($client->getResponse()->isSuccessful());
$this->assertSame(1, $crawler->filter('title:contains("予約")')->count());
$form = $crawler->filter('#reservation_new_submit')->form();
$data = array(
'reservation_location[departureAt][date][year]' => '2012',
'reservation_location[departureAt][date][month]' => '4',
'reservation_location[departureAt][date][day]' => '1',
'reservation_location[departureAt][time][hour]' => '12',
'reservation_location[departureAt][time][minute]' => '0',
'reservation_location[departureLocation]' => '1',
'reservation_location[returnAt][date][year]' => '2012',
'reservation_location[returnAt][date][month]' => '4',
'reservation_location[returnAt][date][day]' => '2',
'reservation_location[returnAt][time][hour]' => '17',
'reservation_location[returnAt][time][minute]' => '30',
'reservation_location[returnLocation]' => '1',
);
$crawler = $client->submit($form, $data);
// reservation/car
$crawler = $client->followRedirect();
$this->assertTrue($client->getResponse()->isSuccessful());
$this->assertSame(1, $crawler->filter('title:contains("車種選択")')->count());
$form = $crawler->filter('.car-class-box:first-child')->selectButton('この車種を選択する')->form();
$crawler = $client->submit($form);
// reservation/option
$crawler = $client->followRedirect();
$this->assertTrue($client->getResponse()->isSuccessful());
$this->assertSame(1, $crawler->filter('title:contains("予約/オプション選択")')->count());
$form = $crawler->selectButton('予約内容を確認する')->form();
$data = array(
'reservation_option[useInsurance]' => 1,
'reservation_option[note]' => 'メモ',
);
$crawler = $client->submit($form, $data);
// reservation/confirm
$crawler = $client->followRedirect();
$this->assertTrue($crawler->filter('h1:contains("予約内容確認")')->count() > 0);
$form = $crawler->selectButton('予約を確定する')->form();
$crawler = $client->submit($form);
// reservation/finish
$this->assertTrue($client->getResponse()->isSuccessful());
$this->assertSame(1, $crawler->filter('title:contains("予約完了")')->count());
}
分割した後のテストコード(動きません)
下記のように分割されたテストを目指したのですが、様々なところでつまづき、思うようにテストは動きませんでした。
public function test予約登録画面のGET()
{
$client = static::createClient();
// /reservation/new
$crawler = $client->request('GET', '/reservation/new');
$this->assertTrue($client->getResponse()->isSuccessful());
$this->assertSame(1, $crawler->filter('title:contains("予約")')->count());
}
public function test予約登録画面のPOST()
{
$client = static::createClient();
$container = $client->getContainer();
$datum = array();
$datum = $this->getLocationData();
$datum['_token'] = $container->get('form.csrf_provider')->generateCsrfToken('unknown');
$data['reservation_location'] = $datum;
$crawler = $client->request('POST', '/reservation/new', $data);
//csrf token エラーが発生してリダイレクトしない
$crawler = $client->followRedirect();
$this->assertTrue($client->getResponse()->isSuccessful());
$this->assertSame(1, $crawler->filter('title:contains("車種選択")')->count());
}
public function test車種選択画面のGET()
{
$client = static::createClient();
$container = $client->getContainer();
$container->get('session')->set('reservation/location', $this->getLocationData());
$crawler = $client->request('GET', '/reservation/car');
$this->assertTrue($client->getResponse()->isSuccessful());
$this->assertSame(1, $crawler->filter('title:contains("車種選択")')->count());
}
public function test車種選択画面のPOST()
{
$client = static::createClient();
$container = $client->getContainer();
$container->get('session')->set('reservation/location', $this->getLocationData());
$datum = array();
$datum = $this->getCarData();
$datum['_token'] = $container->get('form.csrf_provider')->generateCsrfToken('unknown');
$data['reservation_car'] = $datum;
$crawler = $client->request('POST', '/reservation/car', $data);
//csrf token エラーが発生してリダイレクトしない
//DI した Session が消えてリダイレクトしない
$crawler = $client->followRedirect();
$this->assertTrue($client->getResponse()->isSuccessful());
$this->assertSame(1, $crawler->filter('title:contains("予約/オプション選択")')->count());
}
//以下略
//...
private function getLocationData()
{
$data = array();
$data['departureAt'] = array(
'date' => array('year' => '2012', 'month' => '6', 'day' => '1'),
'time' => array('hour' => '0', 'minute' => '0'),
);
$data['departureLocation'] = '1';
$data['returnAt'] = array(
'date' => array('year' => '2012', 'month' => '6', 'day' => '2')
'time' => array('hour' => '0', 'minute' => '0'),
);
$data['returnLocation'] = '1';
return $data;
}
private function getCarData()
{
$data = array();
$data['carClass'] = '1';
return $data;
}
containerから生成したtokenが不正
下記コードでtokenを取得すると、生成されるtokenが異なるためエラーとなります。
containerから直接tokenを呼び出すと、同一セッションのtokenとみなされないことが原因でしょうか。
$container->get('form.csrf_provider')->generateCsrfToken('unknown');
テスト時のみ、csrfを無効にすると、様々な弊害が
テスト時のみcsrfを無効にすると、様々なところでエラーが起きます。ymlの設定ファイルで無効にできます。
# Symfony/app/config/config_test.yml
framework:
csrf_protection: false
エラーとなったコード
// その1 $form['_token'] がなくなる
$form = $this->createForm(new ReservationLocationType(), new Reservation());
$token = $form['_token']->getData();
Field "_token" does not exist.
// その2 オプション指定できなくなる
$form = $this->createForm($type, $reservation, array('csrf_protection' => false));
The option "csrf_protection" does not exist
//その3 twigでエラー その1と同じ 問題
<input type="hidden" name="reservation_car[_token]" value="{{ form._token.get('value') }}" />
Method "_token" for object
CSRF対策が有効かどうかを判定するif文
下記if文で判定可能ですが、プロダクトコードが下記のif文であふれるので、使用していません。
if ($this->container->hasParameter('form.type_extension.csrf.enabled')) {
}
DIした情報は、画面遷移で初期化されて消える
$container->get('session')->set('reservation/location', $this->getLocationData());
初回のリクエスト時はDIした情報が消えませんが、テストケース内で画面遷移が発生するとDIした情報が消えます。
諸先輩からのアドバイス
コントローラのテストを短くする試みについて、諸先輩からアドバイスいただきました。
- コントローラのファンクショナルテストが長くなるのはどうしようもない。そういうもの。ある程度諦める。
- コントローラのテストよりも、ドメインレイヤの設計をしっかりとやって、サービスにロジックを閉じ込め、そこを重点的にテストする。
- アプリケーションの挙動に主眼を置くなら、behatを学んで、試してみるとよい
コントローラのファンクショナルテストはそういうもの、と一旦バッサリ斬っていただき、別のアプローチを提案していただきました。
迷いながら進んでいたところでしたので、とても心が軽くなりました。
同時に、ドメインレイヤの設計について興味が湧きました。参考書籍を読んで勉強したいと思っています。
アドバイスしてくださった皆様、感謝の気持ちでいっぱいです。ありがとうございました。
追記 twitterにて下記の記事をご紹介いただきました。画面遷移しても、DIした情報を維持することができるようです。
Practical Symfony: モックオブジェクトによるページフローのテスト | PHPメンターズ
ふりかえり
よかったこと
- 困っていることを公開してみて、フィードバックをいただくことができた。
- DDDに興味が湧いた。
- ひどいLTだったけれど、チャレンジしてよかった。
反省点
- 準備不足。LT初心者のくせに、前日に慌ててスライド作ってたらダメ。
- どのくらい時間がかかるか把握できておらず、早口に。
- 一枚のスライドに載せる情報量が多かった。
- 5分しかないので、話すテーマはひとつに絞ったほうがよい。
- なれないうちはカンペ必要。
- もっと軽い内容にしてもよかったかも。
反省は次回に活かしたいと思います。