私家版 Slim Framework チュートリアル (6) 〜 テスト編


この記事について

PHP のマイクロフレームワークのひとつである、Slim Framework ですが、チュートリアルがいまいちイケてないので、お題を流用しつつ、初学者にももう少し分かりやすくなるよう、アレンジしてみようという試みです。

本記事は最終回で、前回はこちら。

私家版 Slim Framework チュートリアル (5) 〜 Controllerクラス編 - Qiita

概要

チケットに対する CRUD 機能を一通り実装したので、それらに対するテストを書いていきます。

使用するフレームワークは PHPUnit です。

本チュートリアルどおりにアプリケーションを作成していれば、すでにインストールされているはずですので、以下のコマンドを実行して、正しく実行されるか確認してください。

$ ./vendor/bin/phpunit --version
PHPUnit 5.7.27 by Sebastian Bergmann and contributors.

手元のバージョンは 5.7.27 でインストールされてましたので、本記事でも 5.7 系前提で進めます。

PHPUnit の詳細は以下からご確認ください。

PHPUnit マニュアル

12. 機能テストを書く

12.1. 何をテストするか

PHPUnit といえば名前の通り単体テスト向けのテスティングフレームワークですが、Slim Framework は、機能テスト(フィーチャーテストと呼ばれることもあります)もしやすいように設計されているので、インストール直後からあまり手を加えずとも、機能テストができます。

アプリケーションの雛形に tests/Functional というディレクトリがありますので、本記事では「機能テスト」という用語を使用します。

ここでいう「機能テスト」とは、アプリケーションにリクエストを渡して、返ってきたレスポンスを検証する、というもので、その間に実行される処理を機能(=フィーチャー)単位でまるっとテストしたいときに行います。

これまで以下の機能を作成しました。

  • GET /tickets - チケット一覧の表示
  • GET /tickets/create - チケットの新規作成用フォームの表示
  • POST /tickets - チケットの新規作成
  • GET /tickets/{id} - チケットの表示
  • GET /tickets/{id}/edit - チケット編集用フォームの表示
  • PATCH /tickets/{id} - チケットの更新
  • DELETE /tickets/{id} - チケットの削除

これらすべての機能に対してテストを書いていきます。

上の7つの機能は、大きく分けると以下に分類でき、

  • GET
  • POST/PATCH/DELETE

テストの仕方も、

  • レスポンスを検証する
  • データベースを検証する

というように異なったアプローチが必要です。

レスポンスを検証するタイプでは、単純に表示されるHTMLの中に期待される文字列が含まれていることを検証すればいいでしょう。

一方、データベースを検証するタイプはひと工夫必要です。

本記事では上の2つのタイプのうち、ひとつずつピックアップして、テストコードを載せます。

12.2. テストクラスの作成

tests/Functional 以下に TicketsTest.php ファイルを作成してください。

<?php

namespace Tests\Functional;

class TicketsTest extends BaseTestCase
{
}

BaseTestCase というクラスがあらかじめ用意されているので、これを継承します。

12.3. レスポンスの検証(一覧ページが正常に表示されることを確認する)

続いて、一覧ページが正常に表示されることを確認するテストを書きますが、「正常に表示されること」というのは、

  1. HTTPステータスコードが 200 で返ってくること
  2. レスポンス内に「チケット一覧」という文字が含まれていること

の2点をテストします。

テスト対象の機能は "GET /tickets" です。

testIndex というメソッドをつくってください( test で始まるメソッドが、自動的に実行される仕組みです)。

    public function testIndex()
    {
        $response = $this->runApp('GET', '/tickets');

        $this->assertEquals(200, $response->getStatusCode());
        $this->assertContains('チケット一覧', (string)$response->getBody());
    }

では、テストを実行してみましょう。

$ ./vendor/bin/phpunit tests/Functional/TicketsTest.php 
PHPUnit 5.7.27 by Sebastian Bergmann and contributors.

.                                                                   1 / 1 (100%)

Time: 121 ms, Memory: 4.00MB

OK (1 test, 2 assertions)

こんなかんじで出力されましたでしょうか?

OK の後に表示されている "1 test" は testIndex が実行されたことを示し、"2 assertions" はステータスコードの確認と、レスポンスデータの確認が行われたことを示しています。

12.4. データベースの検証(データが正常に登録されることを確認する)

次に、新規作成ページからデータをPOSTした状態を想定して、チケットデータが正常に登録されることを確認するテストを書きますが、「正常に登録されること」というのは、

  1. HTTPステータスコードが 302 で返ってくること(成功時にリダイレクトしているので)
  2. 一覧表示ページにリダイレクトされること
  3. データベース内に新しいレコードが登録されること

の3点をテストします。

テスト対象の機能は "POST /tickets" です。

testStore というメソッドをつくってください。

    public function testStore()
    {
        $response = $this->runApp('POST', '/tickets', ['subject' => 'テストチケット']);

        // 以下の3行はまだ正しく動作しない
        $id = $this->container['db']->lastInsertId();
        $stmt = $this->container['db']->query('SELECT * FROM tickets WHERE id = ' . $id);
        $ticket = $stmt->fetch();

        $this->assertEquals(302, $response->getStatusCode());
        $this->assertEquals('/tickets', (string)$response->getHeaderLine('Location'));
        $this->assertEquals('テストチケット', $ticket['subject']);
    }

データベースに正しくレコードが登録されたことを確かめるために、データベースにアクセスする必要がありますが、現時点ではアクセスする手段がありません。

そこで、上に書いたようにコンテナを使って、PDOオブジェクトを取得するように BaseTestCase に手を入れていきます。

まず、 setUp メソッドをオーバーライドして(これは PHPUnit_Framework_TestCase クラスのメソッドで、テストケースごとに呼ばれる初期化用メソッドです)、 テストクラスから $this->container でアクセスできるようにしていきます。

まず、 runApp メソッドから以下の箇所を setUp メソッドに移します。

before

    public function runApp($requestMethod, $requestUri, $requestData = null)
    {
    // ~中略~
        // Use the application settings
        $settings = require __DIR__ . '/../../src/settings.php';

        // Instantiate the application
        $app = new App($settings);

        // Set up dependencies
        require __DIR__ . '/../../src/dependencies.php';

after

    /**
     * @var App
     */
    private $app;

    /**
     * @var ContainerInterface
     */
    protected $container;

    protected function setUp()
    {
        parent::setUp();

        // Use the application settings
        $settings = require __DIR__ . '/../../src/settings.php';

        // Instantiate the application
        $app = new App($settings);

        // Set up dependencies
        require __DIR__ . '/../../src/dependencies.php';

        $this->app = $app;
        $this->container = $this->app->getContainer();
    }

ポイントは、 dependencies.php では変数 $app を参照していますので、 $this->app の初期化はその後に実行しないといけません。以下のように書くとエラーになります。

        // Instantiate the application
        $this->app = new App($settings);

        // Set up dependencies
        require __DIR__ . '/../../src/dependencies.php';
        // ↑↑ $app という変数がないのでエラーになる

続いて、再び runApp メソッドに戻り、以下のように先頭に一行加えます。

    public function runApp($requestMethod, $requestUri, $requestData = null)
    {
        $app = $this->app;

これで testStore が動くようになるはずです。再びターミナルから PHPUnit を実行してみましょう。

./vendor/bin/phpunit tests/Functional/TicketsTest.php
PHPUnit 5.7.27 by Sebastian Bergmann and contributors.

..                                                                  2 / 2 (100%)

Time: 63 ms, Memory: 4.00MB

OK (2 tests, 5 assertions)

2 tests, 5 assertions がすべて OK になりました。

12.5. トランザクションの導入

さて、今の状態だと、テストを実行するたびにレコードが追加されてしまい、何度も実行すると「テストチケット」という件名のレコードが大量にできてしまいます。

なので、トランザクションを使って、テストケースごとに検証が終わったらロールバックする仕組みを導入します。

先ほど、 $this->container['db'] で PDOオブジェクトにアクセスできるようにしたので、以下のコードを追加するだけで導入できます。

TicketsTest.php を開き、以下の2つのメソッドを追加してください。

class TicketsTest extends BaseTestCase
{
    protected function setUp()
    {
        parent::setUp();

        $this->container['db']->beginTransaction();
    }

    protected function tearDown()
    {
        parent::tearDown();

        $this->container['db']->rollback();
    }

いちど tickets テーブルのレコードをすべて手動で削除してから、もう一度 PHPUnit を実行します。

その後、再度 tickets テーブルを見てみてください。空の状態のままになっていると思います( testStore
実行時に1件つくられますが、実行後にロールバックされたので、前の状態に戻りました。ただし、auto_increment で定義された ID だけは、前の状態に戻すことはできず、次に割り当てられる ID は数値が増えていきますのでご注意ください)。

残りの testCreate, testShow, testEdit, testUpdate, testDelete については、ご自分で実装してみてください。

ここまでの変更のすべては以下から閲覧できますが、見る前にぜひ自分で実装してみてください。

まとめ

全6回に渡って、Slim Framework のチュートリアルを書いてきましたが、いかがだったでしょうか。
ちょっと使いたくなってきませんでしたか?(笑)

個人的には、フルスタックフレームワークなら Laravel、マイクロフレームワークなら Slim を推しているので、この記事を読んで使ってみたいと思ってくれたひとが増えたらうれしいです。

少しでもいいものにしたいので、編集リクエストやコメントにて、改善提案などどしどしお寄せいただけると助かります。

よろしくお願いします!