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


10

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

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ます。

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

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

回答:


6

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

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

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

更新

のテストcalculateTax高レベルのテストと呼びましょう)と個々の計算戦略のテスト低レベルのテスト)の間には重複がある場合があります。これは、実装によって異なります。

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

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

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

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


コードを少し変更して、実際の実際のコードに近づけました。ここでsalary、関数への2番目の入力calculateTax()が追加されました。この方法で、元の関数と戦略クラスの3つの実装のテストコードを複製すると思います。
Songo 2012

@ソンゴ、私のアップデートを見てください。
ペーテルTörök

5

まず、私は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()、employeeクラスのメソッドがNameHandlerFactorya NameHandlerを正しく要求しcalculateTax()、返されたのメソッドを呼び出すことをテストしNameHandlerます。


hmmmmだから、代わりにテストを動作テストにして(特定の関数が呼び出されたことをテストする)、委任されたクラスの値をアサーションにする必要があるということですか?
Songo 2012

はい、それが私がすることです。確かに、NameHandlerFactoryとNameHandlerの個別のテストを作成します。それらがある場合、Employee.calculateTax()メソッドでそれらの機能を再度テストする理由はありません。そうすれば、新しいNameHandlerを導入するときに、従業員テストを追加する必要はありません。
Kristof Claes 2012

3

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

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

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

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


2

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

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

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


2

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

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

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

ボブおじさんからの素晴らしい投稿があり、TDDを使用するときにこの問題について正確に説明しています。

それぞれの個別のクラスをテストする傾向がTDDを殺していると思います。TDDの全体的な美しさは、テストを使用して設計スキームを促進し、その逆ではないことです。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.