自動テストで大胆かつ高品質な開発を

プログラムのテストって大変ですよね。
長く改修しているシステムでは思わぬ箇所に影響が及んでいたりして、リリースしたら全く関係ない(と思っていた)ポイントでエラーが出てしまったという経験はプログラマーなら誰でもあるかと思います。
自動テストを整備することでこのようなリスクを軽減し、今やるべき目の前の開発に集中することができます。

自動テストのメリット・デメリット

自動テストの最大のメリットはやはり「機能改修のたびに毎回同じテストをヌケモレ無く実施できる」ことにあるでしょう。
初回リリースでは「全てのページがエラー無く表示されるか?」といったテストは行うと思いますが、ちょっとした改修のたびにそこまで毎回見ることは現実的ではありません。しかし自動テストを整備しておけば、コマンド1つでいつでも全ページでエラーが出ないことをテストできます。

2つ目のメリットは「大胆な改修ができる」ことにあると言えます。
影響範囲が広い改修はその広さに比例してテストすべき範囲も広がります。しかし自動テストが整備されていれば、それで賄える範囲はどれだけ影響が及ぼうともテストの手間はかかりません。影響があろうとなかろうとコマンド1つ叩くだけです。それによって「膨大なテストをしなければならない」といった懸念から解放されてダイナミックな改修でも安全に進められます。

一方で自動テストにはデメリットもあります。

まず当然ですが、テストコードも作成のためにコストがかかります。テストコードを書くだけの費用対効果があるのかは考えなければなりません。
1週間かけて書いたテストコードなのに1度だけ回してお役御免、では割に合いません。

またテストコードもプログラムなのでバグる可能性があります。
テストのためにテストコードを書いたら「テストコードが正しく機能すること」をテストしなければなりません。

またテストコードもメンテナンスが必要です。仕様変更による改修で過去のテストケースがNGになる事は普通に起こり得ます。テストコードがNGを返した場合、「間違っているのはアプリケーション本体のコードなのかテストコードなのか?」は都度考慮しなければなりません。

自動テストを書いてみよう

弊社でWebアプリケーションを開発する場合はLaravelを用いることが多いのですが、Laravelは標準でPHPUnitによる自動テストをサポートしています。
PHPUnitでは、例えば特定のURLにアクセスして200 OKが返ってくるか、といったテストを非常に簡単に自動化できます。
これをPHPUnitで記述すると以下のようになります。

namespace Tests\Feature;

use PHPUnit\Framework\Attributes\Test;
use Tests\TestCase;

class NormalAccessTest extends TestCase
{
    #[Test]
    public function testNormalResponse()
    {
        $routeName = 'route';
        $response = $this->get(route($routeName));
        $response->assertStatus(200);
    }
}

8行目の「#[Test]」という記述がポイントです。これはTestアトリビュートといい、TestCaseを継承したクラスでTestアトリビュートの付いた関数を定義すると、それがテストと認識されます。
なお、関数名の先頭か末尾にtestという単語を含んでいる場合はTestアトリビュートは不要です。

このテストコードはテスト対象のページへリクエストし、13行目のassertStatusで「レスポンスコードが200であるか」を確認しています。もしここで200以外が返ってくるとテストNGとなり、失敗した行数や事象が報告されます。

上記は1つのURLのみのテストですが、複数のURLで同様に正常なレスポンスコードを得られるかテストしたいというニーズは当然あるかと思います。
しかし以下のようなテストコードを書いてはいけません。

namespace Tests\Feature;

use PHPUnit\Framework\Attributes\Test;
use Tests\TestCase;

class NormalAccessTest extends TestCase
{
    #[Test]
    public function testNormalResponses()
    {
        $routes = ['route1', 'route2', 'route3',,,];
        foreach ($routes as $routeName) {
            $response = $this->get(route($routeName));
            $response->assertStatus(200);
        }
    }
}

上記は複数のURLで200が返るかテストしていますが、もしNGだったとしても"どのURLでNGになったか分からない"という問題があります。
テスト結果として得られる情報はtestNormalResponses()がOKだったかNGだったかだけであり、assertStatusでNGになったまでは分かってもどのURLでNGだったかは分かりません。また、例えば2番目のURLでNGになった場合3番目以降のテストが実施されない問題もあります。

このようにパラメータだけ変えて同じパターンのテストを繰り返す場合、データプロバイダーというPHPUnitの仕組みを使います。
データプロバイダーを使って上記を書き直すと以下のようになります。

namespace Tests\Feature;

use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Test;
use Tests\TestCase;

class NormalAccessTest extends TestCase
{
    public static function normalResponsesProvider()
    {
        return [
            ['route1', 200],
            ['route2', 200],
            ['route3', 200],
        ];
    }

    #[Test]
    #[DataProvider('normalResponsesProvider')]
    public function testNormalResponse($routeName, $status)
    {
        $response = $this->get(route($routeName));
        $response->assertStatus($status);
    }
}

public staticなデータプロバイダー関数を作成するのと、それをテスト関数で参照するのがポイントです。
データプロバイダーで2次元配列を返すと、各行の配列がテストケースとなり参照するテスト関数の引数にあてがわれます。
テスト関数の方ではDataProviderアトリビュートを記述することで、参照するデータプロバイダーを指定します。
このようにするとデータプロバイダーで返される要素数(上記の場合は3つ)のぶんtestNormalResponse自体が繰り返され、それぞれOK/NGが報告されます。
これにより問題箇所を素早く特定できます。

まとめ

自動テストを整備することでテストの工数を圧縮し、プログラムの品質と開発速度を両立することができます。
Laravelの場合はPHPUnitを簡単に利用できるので利用しない手はありません。

弊社ではWebアプリケーションの開発案件を承っております。お問い合わせフォームよりお気軽にお問い合わせ下さい。