技術メモなど

業務や日々のプログラミングのなかで気になったことをメモしています。PHP 成分多め。

PHPUnit のプラクティス

PHPUnit単体テストを書く上でのプラクティスについて調べたのでメモ。

環境情報

Arrange-Act-Assertを意識する

<?php
// Arrange-Act-Assertを意識しないコード
public function testDivision()
{    
  $this->assertEquals([1,2,3,4,5,6], division([2,4,6,8,10,12], 2));
 }

テストコードは、AAA(Arrange-Act-Assert) の区分を意識する。

  • Arrenge : 準備。テスト対象の処理を実行するための準備を行う区分。
  • Act : 実行。テスト対象を実際に実行する区分。
  • Assert : 検証。期待通りの結果を得られたかを検証する区分。

これにより、どの処理がテスト対象なのかがはっきりさせることができる。
また、利用するにあたってどのような準備が必要になるか明確になる。

<?php
    public function testDivision()
    {
        // Arrange
        $left = [2,4,6,8,10,12];
        $right = 2;
        $expected = [1,2,3,4,5,6];

        // Act
        $actual = division($left, $right);
    
        // Assert
        $this->assertEquals($expected, $actual);
    }

1テスト1アサーション

<?php
   // ひとつのテストで複数の内容をテストしている。
    public function testDivision()
    {
        $left = [2,4,6,8];
        $right = 2;
        $expected = [1,2,3,4];

        $actual = division($left, $right);
    
        $this->assertEquals($expected, $actual);

        $left = [2,4,6,8];
        $right = 0;
        $expected = [2,4,6,8];

        $actual = division($left, $right);

        $this->assertEquals($expected, $actual);

        $left = 4;
        $right = 2;
        $expected = 2;

        $actual = division($left, $right);

        $this->assertEquals($expected, $actual);
    }

ひとつのテストで複数の事柄をテストすると可読性が悪くなりがちだ。「何をテストしているのか」の意図も不明瞭になる。 また途中のアサーションでテストが落ちた場合、後続のアサーションは実行されないため成功/失敗の判断がつかなくなってしまう。

AAAに従ってテストを分割し、内容に応じた名前をつける。

<?php
   // 分割後のテスト
    public function testDivisionWithArray()
    {
        $left = [2,4,6,8];
        $right = 2;
        $expected = [1,2,3,4];

        $actual = division($left, $right);
    
        $this->assertEquals($expected, $actual);
    }

    public function testDivisionWithZero()
    {
        $left = [2,4,6,8];
        $right = 0;
        $expected = [2,4,6,8];

        $actual = division($left, $right);

        $this->assertEquals($expected, $actual);        
    }

    public function testDivisionWithInt()
    {
        $left = 4;
        $right = 2;
        $expected = 2;

        $actual = division($left, $right);

        $this->assertEquals($expected, $actual);
    }

dataProvider, testWith を利用する

さきほどの testDivision のようにテスト内容が同じ引数・結果の繰り返しであれば @dataProvider アノテーションを使うことを検討する。

<?php
    /**
     * @dataProvider divisionDataProvider
     */
    public function testDivision($left, $right, $expected)
    {
       $actual = division($left, $right);
   
       $this->assertEquals($expected, $actual);
    }

    public function  divisionDataProvider()
    {
        return [
            '左辺に配列を渡した場合' => [
                [2,4,6,8], 
                2,
                [1,2,3,4],                
            ],
            '右辺に0を渡した場合' => [
                [2,4,6,8],
                2,
                [1,2,4,8],
            ],
            '左辺に整数を渡した場合' => [
                4,
                2,
                2,                
            ],
        ];
    }

連想配列のキーの部分には任意のコメントがつけられる(日本語OK)。また途中のデータでテストが失敗しても後続のデータは引き続きテストされる。 デメリットとしては、テストの実施場所(testDivision)と準備するデータ(divisionProvider)が離れて配置されるため、場合によってはコードを追うのが辛くなるときがある。

準備するデータがシンプルであれば、コメント上に直接書ける @testWith も便利。

<?php

    /**
     * @testWith ['2019-04-30', false]
     *           ['2019-05-01', true]
     */
    public function testIsReiwa($date, $expected)
    {
        $era = new Era($date);

        $actual = $era->isReiwa();
       
       $this->assertEquals($expected, $actual);
    }

public メソッドのみテストする

public メソッドがクラスのインターフェースであるならば、private は実装の詳細だ。 private メソッドはその性質上、必ず 同じクラス内のpublic メソッドからのみ(直接・間接はあれど)利用される。 一方で、private メソッドがテストに成功したとしてもそれを利用する public メソッドのテストが成功することは保証されない。 テストは private メソッドをそれを利用する public メソッドに対し行うようにする。

独立してテストできるようにする。

<?php
    public function testFirst()
    {
        $hoge = new Hoge(0);

        $actual = $hoge->countup();

        $this->assertEquals(1, $actual);

        return $hoge;
    }

    /**
     * @depends testFirst
     */
    public function testSeccond($hoge)
    {
        $actual = $hoge->countdown();

        $this->assertEquals(0, $actual);
    }

@depends は便利なアノテーションであるが、各テストが依存関係を持ってしまうと以下のようなデメリットもある。

  • 依存先の処理は単体でテストできない。
  • 依存元のテストコードに誤りがあった場合、依存先のテストが(例え正しく動作するとしても)影響を受ける。

不必要な依存は避け、独立してテストできるようにする。

<?php
    public function testCountup()
    {
        $hoge = new Hoge(0);

        $actual = $hoge->countup();

        $this->assertEquals(1, $actual);
    }

    public function testCoundDown()
    {
        $hoge = new Hoge(1);

        $actual = $hoge->countdown();

        $this->assertEquals(0, $actual);
    }

実装途中のテストを明示する

テストが未完成であるときは、markTestIncomplete を使ってその旨を明示する。 markTestIncomplete メソッドが実行されたテストの結果にはI(= Incomplete)と表示される。

<?php
    public function testSample()
    {
        $this->assertTrue(true);
        $this->markTestIncomplete('このテストは実装途中です!!');
    }