Chatwork Creator's Note

ビジネスチャット「Chatwork」のエンジニアとデザイナーのブログです。

ビジネスチャット「Chatwork」のエンジニアとデザイナーのブログです。

読者になる

ChatWorkのPHPユニットテストについて

プロダクト開発部の田中(@cw-tanaka)です。 現在PHPerとして生きています。

このエントリーはChatWork Advent Calendar 8日目の記事です。 昨日は@cw-himuraREST APIを実行して学ぶRustでした。 今日はPHPの話をします。

ChatWorkは来年で7周年を迎え、Webサービスの中でも寿命は長い方なのではないかなと思ってます。 そんな歴史のあるChatWorkですが、コード規模もかなり膨れ上がってきており、それにともなってユニットテストの規模もどんどん大きくなってきました。 弊社は基本的なビジネスロジックの部分はユニットテストを書くようにしており、現在では6000ケース近くのテストをCIで走らせています。 (これ以外にもE2Eテスト等もあるため、総数はもっと多くなってます)

f:id:cw-tanaka:20171128093742p:plain

今回、弊社なりのPHPのユニットテストの書き方を紹介したいと思います。 以下、テストフレームワークはPHPUnitが前提です。

テストメソッドの書き方

まず、本家PHPUnitのサンプルコードを見てみましょう

<?php

    public function testCanBeCreatedFromValidEmailAddress(): void
    {
        $this->assertInstanceOf(
            Email::class,
            Email::fromString('user@example.com')
        );
    }

基本的にPHPUnitはメソッド名の最初がtestであるものをさがし、それをテストとして実行します。 これは、以下のような@testアノテーションを使っても表現できます。

<?php

    /**
     * @test
     */
    public function canBeCreatedFromValidEmailAddress(): void
    {
        $this->assertInstanceOf(
            Email::class,
            Email::fromString('user@example.com')
        );
    }

また、PHPはメソッド名を日本語でも表現できるため、以下のように書くことも可能です。

<?php

    /**
     * @test
     */
    public function メールアドレスからインスタンスを生成できる(): void
    {
        $this->assertInstanceOf(
            Email::class,
            Email::fromString('user@example.com')
        );
    }

弊社では上記の日本語メソッドの書き方を採用しています。 さらに、上記に加えてテスト対象を明示するため、メソッド名のprefixにテスト対象のメソッド名を付与しています。

<?php

    /**
     * @test
     */
    public function fromString_メールアドレスからインスタンスを生成できる(): void
    {
        $this->assertInstanceOf(
            Email::class,
            Email::fromString('user@example.com')
        );
    }

日本語のテストケースのメリット・デメリットについて

日本語でユニットテストを書くことについては賛否両論あると思ってます。 そこで、メリット・デメリットについてまとめてみました。

メリット

  • 開発者が日本人だけなら、どういったテストを書いているのかがわかりやすい
  • テスト名を考える時間が省ける

デメリット

  • 日本以外の方の参入障壁となりやすい(特にオープンソースだと受け入れられにくい)
  • IME変換が面倒

弊社ではデメリットよりもメリットによる利益の方が上回ると判断し、このような書き方となっています。

dataProviderを積極的に使う

渡された値が数値かどうかを検証するメソッドをテストしたいとしましょう。

<?php

    /**
     * @test
     */
    public function isNumber_数値として妥当な場合はtrueを返す(): void
    {
        $validator = new Validator();
        $this->assertTrue($validator->isNumber(1));
    }

上記は「数値かどうか」というテストに対して、「1は数値である」というテストしか網羅できていません。 閾値チェックとして、マイナス値、0、PHPのINT最大値、最小値等の値もチェックしたくなります。

<?php

    /**
     * @test
     */
    public function isNumber_数値として妥当な場合はtrueを返す(): void
    {
        $validator = new Validator();
        $this->assertTrue($validator->isNumber(1));
    }

    /**
     * @test
     */
    public function isNumber_0の場合はtrueを返す(): void
    {
        $validator = new Validator();
        $this->assertTrue($validator->isNumber(0));
    }

    /**
     * @test
     */
    public function isNumber_マイナス値の数値の場合はtrueを返す(): void
    {
        $validator = new Validator();
        $this->assertTrue($validator->isNumber(-1));
    }

    /**
     * @test
     */
    public function isNumber_最大値の場合はtrueを返す(): void
    {
        $validator = new Validator();
        $this->assertTrue($validator->isNumber(PHP_INT_MAX));
    }

    /**
     * @test
     */
    public function isNumber_マイナスの最大値の場合はtrueを返す(): void
    {
        $validator = new Validator();
        $this->assertTrue($validator->isNumber(PHP_INT_MIN));
    }

こういった、テストデータは違うのだけれど、テストコード自体が同じ場合はPHPUnitのdataProviderの機能を使います。

<?php

    /**
     * @return array
     */
    public function isNumberDataProvider(): array
    {
        return [
            [PHP_INT_MIN],
            [-1],
            [0],
            [1],
            [PHP_INT_MAX],
        ];
    }


    /**
     * @test
     * @dataProvider isNumberDataProvider
     *
     * @param int $number
     */
    public function isNumber_数値として妥当な場合はtrueを返す($v): void
    {
        $validator = new Validator();
        $this->assertTrue($validator->isNumber($v));
    }

上記のように@dataProviderアノテーションで指定されたメソッドが、その関数のデータプロバイダーとして扱われ、 そのメソッドで返される値がテストケースの引数にわたされます。

また、それぞれのテストデータについて、名前がつけられるので、基本はつけておくようにします。

<?php

    /**
     * @return array
     */
    public function isNumberDataProvider(): array
    {
        return [
            'PHPの整数の最小値は数値' => [PHP_INT_MIN],
            'マイナス値は数値' => [-1],
            '0は数値' => [0],
            '自然数は数値' => [1],
            'PHPの整数の最大値は数値' => [PHP_INT_MAX],
        ];
    }

たとえば上記であるテストデータで失敗した場合、以下のようにどのテストデータで失敗したかがわかりやすく表示されます。 下記は、あえて失敗するテストデータを追加した状態でテストを実行した結果です。

PHPUnit 6.5.2 by Sebastian Bergmann and contributors.

.....F                                                              6 / 6 (100%)

Time: 185 ms, Memory: 4.00MB

There was 1 failure:

1) ValidatorTest::isNumber_数値である場合はtrueを返す with data set "文字列は数値" ('hogehoge')
Failed asserting that false is true.

/work/test/Sample/Test/ValidatorTest.php:31

FAILURES!
Tests: 6, Assertions: 6, Failures: 1.

サンプルのソースコードを置いておきますので、何かの参考になれば。

github.com

とりあえず今日はこんなところで。