同じ動作を示す複数のオブジェクトの単体テストをどのように構成しますか?


9

多くの場合、私はいくつかの動作を持つ既存のクラスがあるかもしれません:

class Lion
{
    public void Eat(Herbivore herbivore) { ... }
}

...そして単体テストがあります...

[TestMethod]
public void Lion_can_eat_herbivore()
{
    var herbivore = buildHerbivoreForEating();
    var test = BuildLionForTest();
    test.Eat(herbivore);
    Assert.IsEaten(herbivore);
}

さて、私はライオンの行動と同じ行動をするTigerクラスを作成する必要があります:

class Tiger
{
    public void Eat(Herbivore herbivore) { ... }
}

...そして私は同じ動作をしたいので、同じテストを実行する必要があります、私はこのようなことをします:

interface IHerbivoreEater
{
    void Eat(Herbivore herbivore);
}

...そして私は私のテストをリファクタリングします:

[TestMethod]
public void Lion_can_eat_herbivore()
{
    IHerbivoreEater_can_eat_herbivore(BuildLionForTest);
}


public void IHerbivoreEater_can_eat_herbivore(Func<IHerbivoreEater> builder)
{
    var herbivore = buildHerbivoreForEating();
    var test = builder();
    test.Eat(herbivore);
    Assert.IsEaten(herbivore);
}

...次に、新しいTigerクラスに別のテストを追加します。

[TestMethod]
public void Tiger_can_eat_herbivore()
{
    IHerbivoreEater_can_eat_herbivore(BuildTigerForTest);
}

...そして、私のクラスLionTigerクラスをリファクタリングします(通常は継承によるが、時には構成による):

class Lion : HerbivoreEater { }
class Tiger : HerbivoreEater { }

abstract class HerbivoreEater : IHerbivoreEater
{
    public void Eat(Herbivore herbivore) { ... }
}

...そしてすべてが順調です。ただし、機能がHerbivoreEaterクラスにあるため、各サブクラスでこれらの各動作のテストを行うことに問題があるように感じます。しかし、それは実際に消費されているサブクラスだし、それは彼らが重なって行動を共有するために(起こることのみ実装の詳細だLionsTigers、たとえば、全く異なる最終用途を有していてもよいです)。

同じコードを複数回テストすることは冗長に思われますが、サブクラスが基本クラスの機能をオーバーライドできる場合とオーバーライドする場合があります(そうです、それはLSPに違反する可能性がありますが、それに直面しようとすると、IHerbivoreEater単に便利なテストインターフェイスです-それはエンドユーザーには関係ない場合があります)。したがって、これらのテストにはある程度の価値があると思います。

この状況で他の人は何をしますか?テストを基本クラスに移動するだけですか、または予想される動作についてすべてのサブクラスをテストしますか?

編集

@pdrからの回答に基づいて、私はこれを考慮する必要があると思います。これIHerbivoreEaterは単なるメソッドシグネチャコントラクトです。動作は指定しません。例えば:

[TestMethod]
public void Tiger_eats_herbivore_haunches_first()
{
    IHerbivoreEater_eats_herbivore_haunches_first(BuildTigerForTest);
}

[TestMethod]
public void Cheetah_eats_herbivore_haunches_first()
{
    IHerbivoreEater_eats_herbivore_haunches_first(BuildCheetahForTest);
}

[TestMethod]
public void Lion_eats_herbivore_head_first()
{
    IHerbivoreEater_eats_herbivore_head_first(BuildLionForTest);
}

議論のために、あなたはAnimalを含むクラスを持っていないのですEatか?すべての動物は食べるので、Tigerand Lionクラスは動物から継承できます。
マフィンマン

1
@ニック-それは良い点ですが、それは別の状況だと思います。@pdrが指摘したEatように、基本クラスに動作を配置すると、すべてのサブクラスが同じEat動作を示すはずです。しかし、私はふたつの動作を共有している比較的無関係な2つのクラスについて話している。例えば、考えてみてFlyの行動をBrickしてPersonいる、我々は、前提と同様の飛行挙動を示すが、それは必ずしも彼らが共通の基本クラスから派生持っている意味がないことがあります。
スコットウィットロック

回答:


6

これは、テストが設計に対する考え方を真に推進する方法を示しているので、すばらしいことです。あなたはデザインの問題を感知し、適切な質問をしています。

これには2つの見方があります。

IHerbivoreEaterは契約です。すべてのIHerbivoreEatersには、草食動物を受け入れるEatメソッドが必要です。さて、あなたのテストはそれがどのように食べられるかを気にしません。あなたのライオンはおしりから始まり、虎は喉から始まるかもしれません。あなたのテストが気にしているのは、Eatを呼び出した後、草食動物が食べられることです。

一方、あなたが言っていることの一部は、すべてのIHerbivoreEatersが完全に同じ方法で草食動物を食べるということです(したがって基本クラス)。その場合、IHerbivoreEater契約を結んでも意味がありません。それは何も提供しません。HerbivoreEaterから継承することもできます。

あるいは、ライオンとタイガーを完全に排除するかもしれません。

しかし、ライオンとタイガーが食生活を除いてあらゆる点で異なる場合は、複雑な継承ツリーで問題が発生するかどうか疑問に思う必要があります。両方のクラスをネコから派生したい場合、またはライオンのクラスのみをKingOfItsDomainから派生したい場合(おそらく、サメとともに)。これがLSPの真の出番です。

一般的なコードをよりカプセル化することをお勧めします。

public class Lion : IHerbivoreEater
{
    private IHerbivoreEatingStrategy _herbivoreEatingStrategy;
    private Lion (IHerbivoreEatingStrategy herbivoreEatingStrategy)
    {
        _herbivoreEatingStrategy = herbivoreEatingStrategy;
    }

    public Lion() : this(new StandardHerbivoreEatingStrategy())
    {
    }

    public void Eat(Herbivore herbivore)
    {
        _herbivoreEatingStrategy.Eat(herbivore);
    }
}

タイガーも同じです。

さて、これが美しいものです(私が意図していなかったので美しい)。そのプライベートコンストラクターをテストで利用できるようにするだけで、偽のIHerbivoreEatingStrategyを渡して、メッセージがカプセル化されたオブジェクトに適切に渡されることをテストできます。

そして、最初に心配していた複雑なテストは、StandardHerbivoreEatingStrategyをテストするだけで済みます。1つのクラス、1つのテストセット、心配する重複コードはありません。

そして、後で、草食動物を別の方法で食べるようにタイガースに伝えたい場合でも、これらのテストを変更する必要はありません。新しいHerbivoreEatingStrategyを作成してテストするだけです。配線は統合テストレベルでテストされます。


+1戦略パターンは、質問を最初に頭に浮かんだことです。
StuperUser

非常に良いですが、私の質問の「単体テスト」を「統合テスト」に置き換えます。同じ問題が発生するのではないですか?IHerbivoreEater契約ですが、テストに必要なだけです。これは、アヒルのタイピングが本当に役立つ1つのケースのようです。両方を同じテストロジックに送信したいだけです。インターフェースが動作を約束するべきではないと思います。テストはそれを行うべきです。
スコットウィットロック

すばらしい質問、すばらしい答え。プライベートコンストラクターは必要ありません。IoCコンテナーを使用して、IHerbivoreEatingStrategyをStandardHerbivoreEatingStrategyにワイヤリングできます。
azheglov

@ScottWhitlock:「インターフェイスが動作を約束するべきではないと思います。テストはそれを行うべきです。」それがまさに私が言っていることです。動作が保証されている場合は、それを取り除き、(base-)クラスを使用する必要があります。テストにはまったく必要ありません。
pdr

@azheglov:同意しますが、私の答えはすでに十分に長かったです:)
pdr

1

同じコードを複数回テストすることは冗長に思われますが、サブクラスが基本クラスの機能をオーバーライドできる場合とオーバーライドする場合があります

回りくどい方法で、ホワイトボックスの知識を使用して一部のテストを省略することが適切かどうかを尋ねています。ブラックボックスの観点から、LionそしてTiger異なるクラスです。したがって、コードに精通していない誰かがそれらをテストしますが、実装に関する深い知識があれば、1匹の動物をテストするだけで済むことを知っています。

単体テストを開発する理由の1つは、後でリファクタリングできるようにするが、同じブラックボックスインターフェイスを維持できるようにすることです。ユニットテストは、クラスがクライアントとの契約を継続して満たすことを保証するのに役立ちます。または、少なくとも、契約がどのように変化するかを理解し、慎重に考えることを強制します。あなた自身がそれを理解するLionか、または後でTigerオーバーライドEatするかもしれません。これがリモートで可能である場合、サポートする各動物が以下の方法で食べることができる簡単な単体テストテスト:

[TestMethod]
public void Tiger_can_eat_herbivore()
{
    IHerbivoreEater_can_eat_herbivore(BuildTigerForTest);
}

実行するのは非常に簡単で十分であり、オブジェクトが契約を満たせない場合を確実に検出できます。


この質問は、ブラックボックステストとホワイトボックステストのどちらを優先するかということになるのでしょうか。私はブラックボックスキャンプに傾いています。それがおそらく私が自分のやり方をテストしている理由です。ご指摘いただきありがとうございます。
スコットウィットロック

1

あなたはそれを正しくやっています。単体テストは、新しいコードを1回使用したときの動作のテストと考えてください。これは、プロダクションコードから行う呼び出しと同じです。

その状況では、あなたは完全に正しいです。LionまたはTigerのユーザーは、両方がHerbivoreEatersであること、およびメソッドに対して実際に実行されるコードが基本クラスの両方に共通であることを(少なくとも、その必要はない)気にしません。同様に、HerbivoreEaterアブストラクト(具体的なLionまたはTigerによって提供される)のユーザーは、それがどちらであるかを気にしません。彼らが気にしているのは、ライオン、タイガー、または不明なHerbivoreEaterの具体的な実装が草食動物を正しくEat()することです。

つまり、基本的にテストしているのは、ライオンが意図したとおりに食べることと、タイガーが意図したとおりに食べることです。両方がまったく同じように食べられるとは限らないため、両方をテストすることが重要です。両方をテストすることで、変更したくないもの、変更したくないものを確認できます。これらは両方とも定義されたHerbivoreEatersであるため、少なくともCheetahを追加するまで、すべてのHerbivoreEatersが意図したとおりに食べるかどうかもテストしました。テストは、コードを完全にカバーし、適切に実行します(ただし、HerbivoreEaterがHerbivoreを食べることから生じるはずのすべての予想されるアサーションを作成する場合)。

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