web-dev-qa-db-ja.com

ユニットテスト:テストごとの方法とデータプロバイダー

単体テスト(この場合はPHPUnitを使用)を行う「正しい」方法について、同僚の1人と少し哲学的な議論をしています。単体テストでは、実行するテストごとに1つのテストメソッドを記述する必要があると私は思います。

// Obviously a very contrived example!
class AdderTest extends TestCase
{
    private $object = NULL;

    protected function setup ()
    {
        $this -> object = new Adder ();
    }

    /**
     * Test that 2 + 2 = 4
     */
    public function testAddTwoTwoFour ()
    {
        $this -> assertEquals ($this -> object -> add (2, 2), 4);
    }

    /**
     * Test that 2 + 3 = 5
     */
    public function testAddTwoThreeFive ()
    {
        $this -> assertEquals ($this -> object -> add (2, 3), 5);
    }

    // and a bunch of other test cases
}

しかし、私の同僚は、データプロバイダーを使用する方が良いと考えています。

class AdderTest extends TestCase
{
    private $object = NULL;

    protected function setup ()
    {
        $this -> object = new Adder ();
    }

    /**
     * Test add method
     *
     * @dataProvider addTestDataSource
     */
    public function testAdd ($expected, $a, $b)
    {
        $this -> assertEquals ($this -> object -> add ($a, $b), $expected);
    }

    private function addTestDataSource ()
    {
        return [
            [4, 2, 2], // Equivalent to testAddTwoTwoFour
            [5, 2, 3], // Equivalent to testAddTwoThreeFive
        ];
    }
}

私の個人的な見解は、前者の方が優れているということです:

  • 1つのテスト方法= 1つのテスト。すべての単体テストでは、テスト対象のユニットに関する唯一の事実が正しいことを確認する必要があります。
  • 失敗したテストメソッドの名前が表示されるため、失敗を簡単に特定できます。
  • ユニットテストは、いかなる状況においても、テストの失敗がテスト中のコードが間違っているか、テスト自体が間違っているという意味で「賢い」ことを試みてはならないという意見です。テストは、失敗の原因としてテストを除外することがより困難になります。単体テストではなく、コードをデバッグすることになっています。
  • PHPUnitの場合は、「マジック」に依存して機能します(つまり、@ dataProviderタグをデコードするPHPUnit)
  • 異なるタイプのアサーションを実行する場合は、とにかく追加のテストメソッドを記述する必要があります。

私の同僚は彼の方法を好む:

  • ケースごとに1つの単体テストは、Do n't Repeat Yourselfに違反しています
  • ケースごとに1つの単体テストにより、コピーと貼り付けが促進されます。
  • テスト対象のユニットのインターフェイスが変更された場合、多くのテストメソッド(エラーが発生しやすい)を変更する必要がありますが、データプロバイダーの場合は1つのテストメソッドと1つのデータ提供メソッドのみを変更する必要があります。
  • 単体テストははるかに簡潔なので、理解しやすくなっています。

私は(当然)正しいと思いますが、私の同僚はいくつかの有効なポイントを挙げています。私のアプローチの方が優れていると思いますか、それとも彼のほうを好みますか?あなたが選択した理由は何ですか、特にその理由が上で提供したリストにない場合はどうですか?

*もちろん、単体テストフレームワーク自体にバグがないと仮定します。

6
GordonM

場合によります。

この特定のテストテストはコードのどのプロパティですか?このテストがグリーンであることを知っているとき、私は何を知っていますか?

これは哲学的な演習という意味ではありませんが、非常に実用的な意味でです。たとえば、加算器を見てみましょう。 「2と2を正しく追加できますか?」と尋ねることができます。 「2と3を正しく追加できますか?」したがって、次のテストを記述します。

public function knowsTwoPlusTwoIsFour ()
public function knowsTwoPlusThreeIsFive ()

しかし、あなたはそれを知りたくないかもしれません。多分あなたが本当に知りたいのは「あなたは2つの序数を正しく加えることができますか?」です。 (「通常」の意味については後で説明します)これで、テストではデータプロバイダーを使用する必要があります。テストは

public function canAddOrdinaryNumbers()

データソースには、通常の「通常の」ケースを含める必要があります。つまり、ゼロを追加する、負の数を追加する、その補数を追加します。

[4, 2, 2],    // Duh
[-1, 2, -3],  // negative summand
[0, 0, 0],    // just to make sure
[2, 2, 0],    // neutral element
[0, 2, -2],   // inverse

これで、テストによりAdderは通常の数値で問題がないことがわかります。これは基本的に1つの情報であるため、1つのテスト方法である必要があります。

普通ではない数字はどうですか?

public function canAddOneToNaN ()
public function knowsWhatInfPlusNegInfIs ()
public function croaksOnFLoatingPointInput ()
public function whatIsOnePlusSqrtOfMinusOneAnyway ()

これらのテストケースの実装は演習として読者に残し、この点に焦点を当てたいと思います。クラス内のすべてのユニットテストメソッドは、ほぼ同じ量の情報を伝える必要があります。もちろん、これは主観的な尺度ですが、私はそれが良いルールだと思います。テストを見て自分自身に問いかけます。これらのテストの一部は、他の人が以前に行ったことを私に伝えるだけですか?一部のテストは他のテストよりも意味があるように見えますか?もしそうなら、あなたはいくつかのリファクタリングをしたいかもしれません。

11
wallenborn

あなたの主張の多くは、あなたのテストフレームワークの弱点から来ています。 Spock を見ると、多くの懸念事項に対応しています。 Spockでテストを作成する方法を見てみましょう

@Unroll
final "add #a and #b gives #expected"() {
    expect:
        add(a, b) == expected
    where:
        a  | b  || expected
        2  | 2  || 4
        2  | 3  || 5
        // etc.
}

1つのテスト方法= 1つのテスト。すべての単体テストでは、テスト対象のユニットに関する唯一の事実が正しいことを確認する必要があります。

「唯一無二の事実」は曖昧な概念になり得る。プロパティベースのテストと同様に、データ駆動型テストを使用して、単一のテストケースでは実際に証明できないより高いレベルの事実を取得できます。 addは可換性/連想性/ 0でIDを持っています。

失敗したテストメソッドの名前が表示されるため、失敗を簡単に特定できます。

Spockでは、失敗したテスト名を値に依存させることができます。この機能がなくても、適切なアサートメッセージはどの値が失敗したかを明らかにします。

ユニットテストはいかなる状況でも決して「賢い」ことを試みるべきではないと私は思う

データ駆動型テストは、for-eachループよりも賢い必要はありません。実際、フレームワークのサポートをテストしなくても、単純なループでデータ駆動型テストを実装できます。また、フレームワークの「マジック」が賢すぎる場合は、for-eachループにいつでもフォールバックできます。それにもかかわらず、Spockのような魔法は非常に読みやすいテストを提供できます。

異なるタイプのアサーションを実行する場合は、とにかく追加のテストメソッドを記述する必要があります。

はい、しかし私はこれを問題とは見ていません。あなたが言うように、あなたは関係なく追加のメソッドを書く必要があり、データ駆動型テストのいいところは、データを抽出して複数のメソッドにフィードできることです。

一般的に、「DRY /コピー/ペーストの排除」という同僚のポイントと可読性は、データ駆動型テストの大きな勝利だと思います。データ駆動型テストのもう1つの大きなメリットは、テストケースの追加が非常に安価になることです。

5
walpen