Lesson 12

デバッグとテスト

Lesson 12 Chapter 1
デバッグ

デバッグ用の関数

開発効率をあげるため、CakePHPには変数の内容を確認するデバッグ用の関数が用意されています。以下の3つはグローバル関数としてどこからでも呼び出すことが出来ます。いずれも第一引数に確認する変数を渡し、デバッグ関数が実行されるページをブラウザで開くことで確認することができます。

  • pr()

    print_r()の略式です。

  • debug()

    変数のデバッグ内容を表示し、変数を返します。

  • dd()

    変数のデバッグ内容を表示させ、システムを止めます

pr()については print_r と同様に、第二引数にtrueを取ることで、表示したデバッグデータを値として返すことが出来ます。

Debugger クラス

様々なデバッグ用のメソッドを持つ、デバッグ用のクラスも用意されています。

念のため、config/app_local.phpにて、debugが以下のようにtrueで設定されていることを確認しておきましょう。

app_local.php
'debug' => filter_var(env('DEBUG', true), FILTER_VALIDATE_BOOLEAN),

Debuggerクラスを呼び出す際には、\Cake\Error\Debuggerを使用します。例えば、TasksController.phpで使用する場合には以下のようにします。

TasksController.php

<?php

namespace App\Controller;
use App\Controller\AppController;
use \Cake\Error\Debugger; //use文でDebuggerクラスを呼び出しておく

class TasksController extends AppController

...
  public function index()
    {
      //検索の種類
      $tasks = $this->Tasks->find('all');

      //以下のようにDebuggerクラスからメソッドを呼び出し使用する。
      Debugger->dump($tasks);

      $this->set('tasks',$tasks);
    }

以上が使い方となります。次にDebuggerクラスの持つ主要なメソッドを見ていきます。

デバッグ用メソッド

Debuggerクラスの持つ代表的なメソッドを紹介していきます。

  • dump($var,$depth=3)

    $varに入れた変数の値を表示します。変数に想定した値が入っているかを確認するときなどに使用します。デフォルトで配列を何階層目まで表示するかを設定する第二引数が「$depth=3」となっており、2階層目まで表示し、3階層目の表示を省略します。そのため、より深い階層の値を確認したい場合には、第二引数で4以上を指定します。

  • log($var,$level,$depth=3)

    変数の値をdebug.logに出力します。ブラウザなどには表示されません。また、プログラムが実行されている時に、どの関数がどこで呼ばれているかを記録しているスタックトレースの内容も同時に出力します。例えば、以下のようにtry-catchの中で使用することで、エラーの内容や、どこでエラーが発生しているかをログに出力させることが出来ます。

    public function saveUser($data)
    {
        try {
            $user = $this->Users->newEntity($data);
            $this->Users->save($user);
        } catch (\Exception $e) {
            \Cake\Error\Debugger::log($e);
            // エラーメッセージを表示するなどの処理を行う
        }
    }

    第二引数のレベルは、ログの重要度を示す「ログレベル」を指定することが出来ますが、デフォルトの7がデバッグのログを出力する状態なので、基本的に操作する必要はありません。

  • pr(Debugger::trace());

    trace()メソッドはスタックトレースのデータを返します。pr()を使用することで、ブラウザにスタックトレースのデータを表示して確認することが出来ます。

これらの、デバッグようの関数とクラスを使用し効率的に開発を進めていくことが可能となります。

Lesson 12 Chapter 2
テストの概要とセットアップ

CakePHP のテスト機能

ここからは、作成したアプリケーションの品質を確保するために行われる「テスト」についてと、それらを効率的に行うために用意されている CakePHP の「テスト機能」についての説明をしていきます。

まず、ここでの「テスト」とは、アプリケーションの振る舞いや機能が仕様通りに動作するか確認することであり、プログラムにバグや欠陥がなどが無いか検証していくことを指します。

検証の方法としては、テストを行うメソッドや機能に対して、特定の入力値を与え、それに対して期待される結果が得られるかを確認していくのが基本となります。

またテストの種類として、アプリケーションを実際に操作して行う手動のテストのほかに、テスト用のクラスやデータベースなどを用意し、プログラムやコマンドを使用してテストを実行する、自動化テストがあり、今回はCakePHPの用意する機能を利用した「自動化されたテスト」を中心に説明をしていきます。

テストの自動化では、クラスとメソッドから成るテストケースと呼ばれるものを作成し、テスト内容や、入力する値などをプログラムで事前に決めておくことができます。
そのため、一度テスト用のプログラムを作成してしまえばテスト範囲の大小にかかわらず、容易に全く同じ条件でリピートしてテストを実行することも可能となります。

また、コードの変更や新しい機能が追加された場合にも、用意しておいたテストを実行するだけで、悪い影響を受けてしまった箇所などを早期に検出することが出来ます。

このように、自動化によってテストの効率性や一貫性が向上するというメリットを得ることが出来るのですが、手動テストよりもテストを行うために必要な作業量は多くなってしまうため、ケースバイケースで手動と自動のテストを使い分けていくことも重要となります。

次に、CakePHPで自動テストを効率よく行うために使用する、各種機能についての概要を説明をしていきます。

  • テストケース

    テスト実行時に呼び出されるクラスとメソッドを持つ、tests/TestCase 配下に置かれるファイルであり、実際のテスト内容が記述されます。

  • アサーションメソッド

    テストケース内で使用され、特定の入力値に対して期待した値が返ってくるかを判断する機能を持ちます。

  • コードカバレッジ

    テスト実行時に、テストの対象となっているコードがどの程度実行されたかを測定する機能であり、作成したテストケースなどの品質改善に役立てることができます。

  • フィクスチャ

    テストに必要なデータベースのデータをテストケースごとに設定し、それらをテスト実行時に呼び出したりすることが可能となります。

いずれもこの後に、テストの作成と実行を行なう中で登場する機能のため、使い方と合わせてその都度詳しく解説していきます。

テストの準備

PHPUnitのインストール

実際にCakePHPのテスト機能を使用するには、PHPUnitと呼ばれる PHP専用のテスト用フレームワークをインストールしておく必要があります。

インストールは Composer を通して簡単に行うことができます。コマンドライン上で todo4 配下に移動し、以下のコマンドを実行しインストールを行います。

composer require --dev phpunit/phpunit
                    

最後に以下のように インストールした PHPUnit のバージョンが明示され完了です。

Using version ^9.6 for phpunit/phpunit

正常にインストールされていれば、以下のコマンドで PHPUnit のバージョンを確認することができます。

vendor/bin/phpunit --version

以下のように表示されれば問題ありません。

PHPUnit 9.6.9 by Sebastian Bergmann and contributors.

テスト用データベースの作成

次に、テストによって正規のデータベースに変更が及んでしまわないように、テスト用のデータベースを新たに作成します。

MySQLのコマンドライン上で以下を実行し、test_todo データベースを作成します。

CREATE DATABASE test_todo;

//実行後は以下で作成されているかを確認
show databases;

test_todoデータベースの作成が完了したら、次に src/app_local.php にて、テスト用データベース接続の設定を行います。

app_local.php
/*
 * The test connection is used during the test suite.
 */
'test' => [
    'host' => 'localhost',
    //'port' => 'non_standard_port_number',
    'username' => 'root',
    'password' => '**********',
    'database' => 'test_todo',
    //'schema' => 'myapp',
    'url' => env('DATABASE_TEST_URL', 'sqlite://127.0.0.1/tests.sqlite'),
],

'Datasources'内をみていくと、上記のように'test'の設定部分を見つけることができるかと思います。username、password、databace の設定を編集しておきましょう。

Lesson 12 Chapter 3
テストケース

テストケースの規約

テストケースは実際のテスト内容が記述されたクラスとメソッドから成るファイルであり、テストを実行するために必ず必要なものとなります。

テストケースもまた、CakePHPにおける規約を持っているため確認をしておきましょう。

  1. ファイルは tests/TestCase 配下に置く

  2. ファイル名は「〜Test.php」で終える

  3. クラスにはテストの内容に合わせて、以下3つのうち最低1つを継承させる

    • Cake\TestSuite\TestCase

      特定のクラスやメソッドなどを単体でテストする場合に使用

    • Cake\TestSuite\IntegrationTestCase

      データベースやセッション、リクエスト、レスポンスなどを含む、アプリケーションの挙動などの結合テストをする場合に使用

    • \PHPUnit\Framework\TestCase

      CakePHPの持つ機能やアサーションではなく、PHPUnitの機能を利用する場合に使用

  4. クラス名とファイル名は一致させる。例えば、 TasksControllerTest.php が持つクラス名は TasksControllerTest となる。

  5. アサーションを含むテスト用のメソッド名は test から始まるキャメルケースを使用する。例えば testIndex() など。

テストケースの作成

これまで、テストケースを手動で作成したことはありませんでしたが、tests/TestCase 配下を見ると bakeコマンドを使用して作成したファイルについてはテストケースが既につくられていることが分かるかと思います。

試しに、tests/TestCase/Controller/UsersControllerTest.php を開いてみましょう。

UsersControllerTest.php
<?php
declare(strict_types=1);

namespace App\Test\TestCase\Controller;

use App\Controller\UsersController;
use Cake\TestSuite\IntegrationTestTrait;
use Cake\TestSuite\TestCase;

/**
 * App\Controller\UsersController Test Case
 *
 * @uses \App\Controller\UsersController
 */
class UsersControllerTest extends TestCase
{
  use IntegrationTestTrait;
  /**
   * Fixtures
   *
   * @var array
   */
  protected $fixtures = [
      'app.Users',
  ];
  /**
   * Test index method
   *
   * @return void
   * @uses \App\Controller\UsersController::index()
   */
  public function testIndex(): void
  {
      $this->markTestIncomplete('Not implemented yet.');
  }

  ...

まずuse文ですが、テスト対象の UsersController クラスがあり、その後に Cake\TestSuite\ 配下の IntegrationTestTrait と、規約にもあった TestCase クラスが指定されています。

IntegrationTestTrait はHTTPリクエストやレスポンスが正しく届いているかを確認するアサーションメソッドなどを提供し、コントローラのテストをより詳細に設定することを可能にしてくれます。

ただ基本的に IntegrationTestTrait の持つメソッドなどは、IntegrationTestCase の中に含まれているためIntegrationTestCase を継承する形にした場合には IntegrationTestTrait を使用する必要はありません。

次に、クラス名も規約に従い クラス名 + Test の形になっており、TestCase クラスを継承しています。

その後に、フィクスチャーの設定があり、続けて各アクションメソッドのテストが並んでいます。フィクスチャーについては、この後の Chapter5 にて改めて触れていきますが、今回はusersテーブルのフィクスチャを指定しており、tests/Fixture/UsersFixture.php を参照しています。

各テストメソッドの内容を見ると、markTestIncomplete()メソッドが使われており、テストが実行された際に、テストの実装が完了していないことを通知するようになっています。

このように、bakeコマンドで作成されたテストケースはある程度ひな型に沿って作られているため、いくつかの設定とテストケースの内容を作成するのみで済み、効率よくテストの作成を行うことができます。

そのため、このまま UsersControllerTest.php を編集しテストケースの作成を進めていきましょう。

まず最初に、コントローラのテストではメソッド単体よりも、レスポンスやセッション、データベースなどと連携をとって実行される結合された機能が多いため、extends の対象を IntegrationTestCase に変更します。

use App\Controller\UsersController;
//use Cake\TestSuite\IntegrationTestTrait;
//use Cake\TestSuite\TestCase;
use Cake\TestSuite\IntegrationTestCase; //追加

/**
 * App\Controller\UsersController Test Case
 *
 * @uses \App\Controller\UsersController
 */
class UsersControllerTest extends IntegrationTestCase

次に setUp()メソッドを追加します。setUp()メソッドはテストケースクラスの持つテストメソッドが呼び出されるたびに実行されるもので、テストに必要な初期設定を行わせることができます。

class UsersControllerTest extends IntegrationTestCase
{
    //use IntegrationTestTrait; //使用しない
    private $Users;
    
    public function setUp(): void
    {
        parent::setUp();
        $usersController = new UsersController();
        $this->Users = $usersController->Users;
    }

最初にプロパティとして $Users を宣言しておき、setUp()メソッド内で、Usersのテーブルクラスを代入することで簡単にテーブルデータにアクセスできるようにしています。

また、setUp()メソッドの先頭で、parent::setUp();を必ずいれておくようにしましょう。アプリケーション内で使用する定数などを読み込むなどの処理を行っています。

次に、テストケースの内容を編集していきます。testIndexアクションの内容を以下のように書き換えて、アサーションを含むテストを実装してみましょう。

UsersControllerTest
public function testIndex(): void
{
    $this->get('/users');
    $this->assertResponseOk();
}

$this->get('/users'); で users/index アクションを実行し、その後にアサーションである assertResponseOk()メソッドを呼び出し、レスポンスが成功しているかどうかを確認しています。

続けて、testView()メソッドの内容もアサーションを含むテストとなるよう編集を行います。

UsersControllerTest
public function testView(): void
{
    // テスト対象のアクションにリクエストを送信
    $this->get('/users/view/1');

    // レスポンスのステータスコードを確認
    $this->assertResponseOk();
    // レスポンスのビューテンプレートをが表示されたか確認
    $this->assertTemplate('view');

    // レスポンスのビューテンプレートに渡された変数を取得
    $user = $this->viewVariable('user');
    // 取得した変数の値を確認
    $this->assertSame(1, $user->id);
    // レスポンスのコンテンツを確認
    $this->assertResponseContains('ユーザーネーム');
}

いくつか新たなアサーションが登場しています。 assertTemplate()メソッド は引数にビューテンプレートの名前を取り、正常にビューテンプレートが表示されたかどうかを確認します。

assertSame()メソッドでは、第一引数と第二引数の値が型を含めて同じ値となっているかどうかをみます。上記では、直前にviewVariable()メソッドを使用して、ビューテンプレートに渡された変数を取得しており、その内容である'id'が間違いなく'1'となっているかを確認しています。

最後の assertResponseContains()メソッドでは、レスポンスのbody内を検索し 'ユーザーネーム' という文字列が入っているかどうかを確認しており、ビューテンプレートの内容に問題がないかをテストします。

このように、アサーションメソッドには様々な種類のものが用意されており、テスト内容に合わせてその都度最適なものを使用していくこととなります。

Lesson 12 Chapter 4
テストの実行

すべてのテストを実行する

UsersControllerTest クラスを編集し、testIndex() と、testView() メソッドの2つにアサーションを含むテストを実装したので、早速テストを実行してみましょう。

コマンドライン上でtodo4配下に移動し、以下のコマンドを入力します。

vendor/bin/phpunit

返ってきた内容をみると、テストの結果を端的に表す、以下のようなピリオドやアルファベットを使用した記号が表示されます。

...FWFFFF....IIIWII

上記の記号はそれぞれ、以下のような意味を持ちます。

  • "." (ドット): テストが成功したことを示します。各ドットは1つのテストケースを表しています。

  • "F" (エフ): テストが失敗したことを示します。この場合、該当するテストケースにおいて少なくとも1つのアサーションが失敗しています。

  • "E" (イー): テストがエラー終了したことを示します。テストの実行中に例外が発生し、予期しない状況が発生しています。

  • "W" (ダブリュー): テストが警告終了したことを示します。テストの実行中に非致命的な警告が発生しています。

  • "I" (アイ): テストが不完全な状態で終了したことを示します。テストケースが未実装であるか、実行されていない場合に表示されます。

その後に、各種エラーの発生箇所やその内容の表示があり、最後には実行されたテストやアサーションと、発生したエラーや警告の総数が表示されて終了となります。

エラーなどはもちろん解決していくべきですが、数が多い場合などは、テストケースを一つずつ実行して見ていくことも可能です。

一部のテストのみ実行する

先ほどの テスト実行コマンドに --filter オブションを付け加えることで、クラス単位や、メソッド単位でテストを実行することが可能となります。以下のコマンドをそれぞれ実行してみましょう。

vendor/bin/phpunit  --filter UsersControllerTest

上記は UsersControllerTest クラスのみを対象にテストを行います。

vendor/bin/phpunit  --filter testIndex tests/TestCase/Controller/UsersControllerTest.php

上記は、testIndex のみを指定しており、メソッド名のあとに、メソッドが存在するファイルまでのパスが必要となります。

UsersControllerTest クラス単位で実行した際には、未実装のクラスがあるため、以下のように表示されます。

..III                                                               5 / 5 (100%)

Time: 00:01.111, Memory: 20.00 MB

OK, but incomplete, skipped, or risky tests!
Tests: 5, Assertions: 6, Incomplete: 3.

testIndexメソッド単体で指定した場合には、以下のようになり、テスト成功となります。

.                                                                   1 / 1 (100%)

Time: 00:00.608, Memory: 18.00 MB

OK (1 test, 2 assertions)

以上のように、作成したテストケースを実行し、エラーなどが出る場合には修正箇所とテストケースの内容を確認し、再度テストを実行...という流れでテストを完成させていくことになります。

コードカバレッジ

作成したテストが、正しくテストを行えているのかどうかの指標を簡単に確認する方法があり、コードカバレッジという機能となります。

コードカバレッジはテストが行われたコードの行数や、メソッドの数といったデータをまとめ、HTMLで出力、ブラウザで一覧表示することができます。早速、以下のコマンド入力しHTMLファイルを出力してみましょう。

phpdbg -qrr vendor/bin/phpunit --coverage-html webroot/coverage tests/TestCase/Controller/UsersControllerTest.php

ファイルの生成が成功すると、以下のような表示がされます。

Generating code coverage report in HTML format ... done [00:00.287]

実行したコマンドでは、UsersControllerTest.php におけるテストを対象にしており、HTMLファイルは webroot/coverage 配下のControllerフォルダ内となります。

12_4_1.png

ファイルを確認できたら早速ブラウザで開いてみましょう。以下のような画面が表示されます。

12_4_2.png

テスト対象のコードを行・メソッド・クラス で分けて管理しており、それぞれ個数とパーセンテージでカバー率を示しています。今回は、index と view のテストのみ作成しており、それぞれ100%となっているためテスト内容に大きな問題はないことがわかります。

コードカバレッジの数値がよければそれで良しというわけにはいきませんが、多数のテストの内容を一括で確認し、抜け漏れがある箇所を特定できることはテスト改善の効率改善につなげることができます。

Lesson 12 Chapter 5
フィクスチャ

フィクスチャとは

フィクスチャは、テストケースの内容がデータベースやモデルに依存する場合に、一時的に仮のデータベースファイルとして、テーブルの作成と、そのレコードのロードを行うことができるものです。

フィクスチャーはテストケース実行時に以下の挙動を取ります。

  1. 各フィクスチャーで必要なテーブルを作成

  2. フィクスチャーファイルで指定したデータがあれば、それらをテーブルに投入。

  3. テストメソッドが実行される

  4. フィクスチャーが作成したテーブルを空にする

このように、テーブルとレコードを仮で作成し、テスト終了後にはデータを空にするため、常に一定の条件下でのテストを行うことが可能となります。

また、フィクスチャを使用する際には、事前に toso4 の配下にある phpunit.xml ファイル を開き、以下のように PHPUnitExtension が追加されていることを確認しておきましょう。

<!-- Load extension for fixtures -->
<extensions>
  <extension class="Cake\TestSuite\Fixture\PHPUnitExtension"/>
</extensions>

フィクスチャの作成

フィクスチャのファイルは、tests/Fixture 配下に設置されます。今回は bake コマンドで Users の Model を作成しているので、自動でUsersFixture.php が作成されているかと思います。

12_5_1.png

UsersFixture.php を開いて確認すると、TestFixture クラスを継承しており、init()メソッドの中で、$this->records へと仮の値でレコードのデータを入れていることがわかります。

<?php
declare(strict_types=1);

namespace App\Test\Fixture;

use Cake\TestSuite\Fixture\TestFixture;

/**
 * UsersFixture
 */
class UsersFixture extends TestFixture
{
    /**
     * Init method
     *
     * @return void
     */
    public function init(): void
    {
        $this->records = [
            [
                'id' => 1,
                'username' => 'Lorem ipsum dolor sit amet',
                'password' => 'Lorem ipsum dolor sit amet',
                'created' => '2023-04-04 07:56:19',
                'modified' => '2023-04-04 07:56:19',
            ],
        ];
        parent::init();
    }
}

また、init()メソッド内ではレコードの値にメソッドなどを使い、データを動的に扱うことが可能です。そのため、created や modified は date('Y-m-d H:i:s')などのdate()メソッドにしても正常に動作します。

ちなみにですが、これまで行ってきたテストは上記の仮のデータが作成されていたために、問題なく実行することができていました。本来であれば、先にフィクスチャの内容を確認、もしくは作成をしてからテストを実行する流れとなります。

フィクスチャの読み込み

改めて、UsersControllerTest.php を開いてみましょう。以下のように各テストメソッドの前で、users を指定して フィクスチャを読み込んでいる箇所があるのが分かるかと思います。

/**
 * Fixtures
 *
 * @var array
 */
protected $fixtures = [
    'app.Users',
];

仮に複数のテーブルを読み込む必要がある場合には、「'app.Users','app.Tasks'」のように記述することが可能です。