Wiz テックブログ

Wizは、最新のIoTやICTサービスをお客様に届ける「ITの総合商社」です。

【Laravel/TDD】100日後でも死なないテスト駆動開発のやり方〜Laravel8編〜

f:id:t-yone3:20211206131321j:plain こんにちは、Wizプロダクト開発チームの米山です。

なんだか名前負けしそうなタイトルですが(笑) 今回はTDD(テスト駆動開発)の、主にテスト手法について書いてみようと思います。

TDDは、弊社の開発手法にも取り入れており、通常のMVCモデルのソースコードの他、PHPUnitで動かすためのテストコードも作成しています。

テストコードの書き方は人それぞれなのですが、私が約半年TDDを実践してみて「だいたいこんな感じで良いかな」というようなスタンダードモデルができてきたので紹介したいと思います。

setUpBeforeClassとsetupは必ず作成する

setUpBeforeClassは全テストケースで最初に1度だけ、setUpはテストケース毎に動作するメソッドです。

私の場合は、public/index.phpで行っている操作(例えば、定数「LARAVEL_START」の定義など)をsetUpBeforeClassで行い、setUpでテストケース実行前にDBクリアを行うなどをしています。

public static function setUpBeforeClass(): void
{
    if (!defined('LARAVEL_START')) {
        define('LARAVEL_START', microtime(true));
    }
}

public function setup(): void
{
    parent::setUp();

    $this->clearDB();
}

メソッド名にケース番号とケース内容を入れてわかりやすくする

この辺は個人のやり方で異なる部分ですが、私の場合は「テストケースがいくつあって、何番目のケースなのか」がわかるよう、テストケースメソッドにはケース番号と確認したいことを入れています。

例えば

/**
 * @test
 */
public function Case1_Getするとステータス200が返る(): void
{
    // 実際のテストコード
}

こんな感じです。 こちらについては、別途「php-code-coverage」というライブラリも併用している関係で、あるコードはどのテストケースで通ったのかを視覚的に確認する時にも有効な手段です。

テストデータはFactoryを有効活用する

この記事の執筆時点のLaravelバージョンは8.67なのですが、Laravel8系になってFactoryはクラスとして定義できるようになりました。

各Modelクラスに「HasFactory」トレイトをuseすることで使用可能になります。

単に値を自動生成するだけでなく、テストケースによって固定値を入れておきたいケースもあると思います。

どちらでも対応できるよう、デフォルトの「definition」とは別にメソッドを作っておきます。

class ItemFactory extends Factory
{
    protected $model = Item::class;
    public function featureTestSetup(array $attributes = []): ItemFactory
    {
        $replaceData = collect($attributes);
        return $this->state(fn (array $attributes) => [
            'id' => $replaceData->get('id') ?? $this->faker->randomNumber(),
            'name' => $replaceData->get('name') ?? $this->faker->name(),
        }
    }
}

こうしておけば、引数$attributesに入っているものは固定値を使うことができますし、直接テストには関係ないが、DB上必要な項目はランダムにfakerを使って値を生成できます。

TestCaseとTestDataの責務を分ける

Laravelのテスト、すなわちPHPUnitには「DataProvider」という機能があります。

これを利用すると、テストケースメソッドの開始時にテストデータが入った状態(メソッドの引数にある状態)でテストが開始できます。

が、私は敢えてこれを使わずにコードを書きます。

理由はテストケースとテストデータ作成のコードを両方1つのクラスに書き込むことで、コードが肥大化しメンテナンスしづらくなるからです。

テストケースにはテストケースだけを書いておく。データ作成は別のところでやる。

私はこれをトレイトを使って実現しました。

例えば

class GetItemsTest extends TestCase
{
    /**
     * @test
     */
    public function Case1_Getするとステータス200が返る(): void
    {
        // 実際のテストコード
    }
}

というテストコードがあったとして、DataProvidorを使うと

class GetItemsTest extends TestCase
{
    /**
     * @test
     * @dataProvider case1_data
     */
    public function Case1_Getするとステータス200が返る($data): void
    {
        // 実際のテストコード
    }

    public function case1_data(): array
    {
        return [
            // ここにデータを書く
        ];
    }
}

となるのですが、これをトレイトに置き換えると

class GetItemsTest extends TestCase
{
    use ItemTestData;

    /**
     * @test
     */
    public function Case1_Getするとステータス200が返る(): void
    {
        $this->createTestData();

        // 実際のテストコード
    }
}

というコードにすることができます。

「なんで便利な機能(DataProvider)があるのに使わないんだ?」とギモンを持つ方もいるかもしれません。

コードを分けることは前述した通り、メンテナンスしやすくすることが目的ですが、トレイトにすることでいくつかのテストで共有して利用することが可能です。

例えば、上の例では「ItemTestData」というトレイトを使い、テストデータを作成しますが、他のケースでもこの「ItemTestData」を利用したければuseすることで使用可能になります。

テストケースごとにDataProviderを書く必要はありません。

ここまでのまとめ

いかがだったでしょうか。テスト駆動開発といっても人によって、あるいは言語によって様々なスタイルがあると思いますが、今回はLaravel(PHPUnit)でのやり方を紹介しました。

  • setUp系のメソッド(初期化処理)は必ず行う
  • テストケースメソッド名はケース番号とやりたいことを書き、わかりやすくする
  • データ作成はFactory機能を使って柔軟に作成
  • DataProviderではなくtraitを使ってテストデータを作成する

1つできれば使いまわして量産することも可能です。参考になれば幸いです。


Wizではエンジニアを募集しております。

興味のある方、ぜひご覧下さい。

careers.012grp.co.jp