web-dev-qa-db-ja.com

戦略パターンにリファクタリングされた関数を単体テストする方法は?

私のコードに次のような関数がある場合:

_class Employee{

    public string calculateTax(string name, int salary)
    {
        switch (name)
        {
            case "Chris":
                doSomething($salary);
            case "David":
                doSomethingDifferent($salary);
            case "Scott":
               doOtherThing($salary);               
       }
}
_

通常、私はこれをリファクタリングして、ファクトリクラスと戦略パターンを使用してPloymorphismを使用します。

_public string calculateTax(string name)
{
    InameHandler nameHandler = NameHandlerFactory::getHandler(name);
    nameHandler->calculateTax($salary);
}
_

TDDを使用している場合は、リファクタリングの前に、元のcalculateTax()で機能するいくつかのテストを用意します。

例:

_calculateTax_givenChrisSalaryBelowThreshold_Expect111(){}    
calculateTax_givenChrisSalaryAboveThreshold_Expect111(){}

calculateTax_givenDavidSalaryBelowThreshold_Expect222(){}   
calculateTax_givenDavidSalaryAboveThreshold_Expect222(){} 

calculateTax_givenScottSalaryBelowThreshold_Expect333(){}
calculateTax_givenScottSalaryAboveThreshold_Expect333(){}
_

リファクタリング後、FactoryクラスNameHandlerFactoryと、少なくとも3つのInameHandlerの実装ができます。

テストをリファクタリングするにはどうすればよいですか? EmployeeTestsからclaculateTax()の単体テストを削除して、InameHandlerの実装ごとにTestクラスを作成する必要がありますか?

Factoryクラスもテストする必要がありますか?

10
Songo

古いテストは、calculateTaxがまだ正常に機能することを確認するのに十分です。ただし、これには多くのテストケースは必要ありません。3つだけです(または、予期しないnameの値を使用してエラー処理もテストしたい場合は、さらにいくつかのテストケースが必要です)。

個々のケースのそれぞれ(現時点ではdoSomething et al。で実装されています)にも、各実装に関連する内部の詳細と特殊なケースをテストする独自のテストセットが必要です。新しいセットアップでは、これらのテストをそれぞれのストラテジークラスの直接テストに変換できます。

古いユニットテストは、それらが実行するコードと、それが実装する機能が完全に存在しなくなった場合にのみ削除することを好みます。それ以外の場合、これらのテストにエンコードされたナレッジは依然として関連性があり、テストのみをリファクタリングする必要があります。

更新

calculateTaxのテスト(それらを高レベルのテストと呼びましょう)と個々の計算戦略のテスト(低レベルのテスト)の間には重複があるかもしれません-実装によって異なります。

テストの元の実装は、特定の税計算の結果をアサートし、特定の計算戦略がそれを生成するために使用されたことを暗黙的に検証していると思います。このスキーマを維持すると、実際に重複が発生します。ただし、@ Kristofが示唆したように、モックを使用して高レベルのテストを実装し、正しい種類の(モック)戦略がcalculateTaxによって選択および呼び出されたことを確認することもできます。この場合、高レベルのテストと低レベルのテストの間に重複はありません。

したがって、影響を受けるテストをリファクタリングするのにあまりコストがかからない場合は、後者のアプローチを選択します。ただし、実際には、大規模なリファクタリングを行うときに、十分な時間を節約できれば、少量のテストコードの複製を許容できます。

Factoryクラスもテストする必要がありますか?

繰り返しますが、状況によって異なります。 calculateTaxのテストはファクトリを効果的にテストすることに注意してください。したがって、ファクトリコードが上記のコードのように簡単なswitchブロックである場合、これらのテストで十分です。しかし、ファクトリーがさらにトリッキーなことを行う場合は、そのテストを専用にテストすることもできます。問題は、問題のコードが実際に機能することを確信するために必要なテストの数です。コードの読み取り時、またはコードカバレッジデータの分析時に、テストされていない実行パスが表示される場合は、これらのテストを実行するためにさらにいくつかのテストを行います。次に、コードに完全に自信を持つまでこれを繰り返します。

6
Péter Török

まず、私はTDDやユニットテストの専門家ではないと言っておきますが、これをテストする方法を以下に示します(疑似ライクなコードを使用します)。

_CalculateTaxDelegatesToNameHandler()
{
    INameHandlerFactory fakeNameHandlerFactory = Fake(INameHandlerFactory);
    INameHandler fakeNameHandler = Fake(INameHandler);

    A.Call.To(fakeNameHandlerFactory.getHandler("John")).Returns(fakeNameHandler);

    Employee employee = new Employee(fakeNameHandlerFactory);
    employee.CalculateTax("John");

    Assert.That.WasCalled(fakeNameHandler.calculateTax());
}
_

したがって、従業員クラスのcalculateTax()メソッドがNameHandlerFactoryNameHandlerを正しく要求し、返されたcalculateTax()メソッドを呼び出すことをテストしますNameHandler

5
Kristof Claes

1つのクラス(すべてを行う従業員)を取り、3つのクラスグループを作成します。工場、従業員(戦略のみを含む)、および戦略です。

したがって、テストの3つのグループを作成します。

  1. 単独で工場をテストします。入力は正しく処理されますか?未知を渡すとどうなりますか?
  2. 従業員を個別にテストします。任意の戦略を設定でき、期待どおりに機能しますか?戦略や工場が設定されていない場合はどうなりますか? (それがコードで可能であれば)
  3. 戦略を単独でテストします。それぞれが期待する戦略を実行していますか?それらは一貫した方法で奇数境界入力を処理しますか?

もちろん、シバン全体の自動テストを作成することもできますが、これらは統合テストに似ているため、そのように扱う必要があります。

3
Telastyn

コードを書く前に、ファクトリーのテストから始めます。必要なものをあざけると、実装とユースケースについて考えさせられます。

私はファクトリーを実装し、各実装のテストを続行し、最後にそれらのテストの実装自体を実行します。

最後に、古いテストを削除します。

2
Patkos Csaba

私の意見では、何もしないでください。つまり、新しいテストを追加しないでください。

これは意見であり、実際にはオブジェクトからの期待をどのように認識するかによって異なります。クラスのユーザーは税計算の戦略を提供したいと思いますか?彼が気にしない場合、テストはそれを反映する必要があり、単体テストから反映される動作は、クラスが税を計算するために戦略オブジェクトを使用し始めたことを気にしないことです。

TDDを使用しているときに、実際にこの問題に何度か遭遇しました。主な理由は、外部リソース(ファイル、DB、リモートサービスなど)のようなアーキテクチャの境界の依存関係とは対照的に、戦略オブジェクトは自然な依存関係ではないためだと思います。これは自然な依存関係ではないため、通常、この戦略に基づいてクラスの動作を行うことはしません。私の本能は、クラスの期待が変わった場合にのみテストを変更するべきだということです。

Uncle Bobのすばらしい post があり、TDDを使用するときにこの問題について正確に説明しています。

それぞれの個別のクラスをテストする傾向がTDDを殺していると思います。 TDDの優れた点は、テストを使用して設計スキームを強化し、その逆ではないことです。

2
Rafi Goldfarb