web-dev-qa-db-ja.com

継承を使ってクラスをテストする正しいアプローチは何ですか?

次の(単純化しすぎた)クラス構造があると仮定します:

class Base
{
  public:
    Base(int valueForFoo) : foo(valueForFoo) { };
    virtual ~Base() = 0;
    int doThings() { return foo; };
    int doOtherThings() { return 42; };

  protected:
    int foo;
}

class BarDerived : public Base
{
  public:
    BarDerived() : Base(12) { };
    ~BarDerived() { };
    int doBarThings() { return foo + 1; };
}

class BazDerived : public Base
{
  public:
    BazDerived() : Base(25) { };
    ~BazDerived() { };
    int doBazThings() { return 2 * foo; };
}

ご覧のとおり、_基本クラスdoThings関数は、fooの値が異なるため、各派生クラスで異なる結果を返します、doOtherThings関数はすべてのクラスで同じように動作します。

これらのクラスのユニットテストを実装する場合、doThingsdoBarThings/doBazThingsの処理は私には明らかです-派生クラスごとにカバーする必要があります。しかし、どのようにdoOtherThingsを処理する必要がありますか?両方の派生クラスでテストケースを本質的に複製することは良い習慣ですか? doOtherThingsのような6個以上の関数があり、さらに派生クラスがある場合、問題はさらに悪化します。

8
CharonX

BarDerivedのテストでは、BarDerivedのすべての(パブリック)メソッドが正しく機能することを証明する必要があります(テストした状況の場合)。 BazDerivedも同様です。

一部のメソッドが基本クラスに実装されているという事実は、BarDerivedおよびBazDerivedのこのテスト目標を変更しません。これは、Base::doOtherThingsは、BarDerivedBazDerivedの両方のコンテキストでテストする必要があり、その関数について非常によく似たテストを取得できます。

派生クラスごとにdoOtherThingsをテストする利点は、BarDerivedの要件がBarDerived::doOtherThingsは24を返す必要があります。その後、BazDerivedテストケースでのテストの失敗は、別のクラスの要件を満たしていない可能性があることを示しています。

しかし、OtherThingsはどのように処理する必要がありますか?両方の派生クラスでテストケースを本質的に複製することは良い習慣ですか?

私は通常、Baseが独自の仕様を持っていることを期待します。仕様は、派生クラスを含め、準拠する実装を確認できます。

void verifyBaseCompliance(const Base & systemUnderTest) {
    // checks that systemUnderTest conforms to the Base API
    // specification
}

void testBase () { verifyBaseCompliance(new Base()); }
void testBar () { verifyBaseCompliance(new BarDerived()); }
void testBaz () { verifyBaseCompliance(new BazDerived()); }
3
VoiceOfUnreason

ここで対立があります。

literal(const値)に依存するdoThings()の戻り値をテストするとします。

このために作成するテストは本質的に煮詰められますconst値のテスト、これは無意味です。


より意味のある例を示すため(C#の方が速いですが、原則は同じです)

_public class TriplesYourInput : Base
{
    public TriplesYourInput(int input)
    {
        this.foo = 3 * input;
    }
}
_

このクラスは有意義にテストできます:

_var inputValue = 123;

var expectedOutputValue = inputValue * 3;
var receivedOutputValue = new TriplesYourInput(inputValue).doThings();

Assert.AreEqual(receivedOutputValue, expectedOutputValue);
_

これはテストする方が理にかなっています。その出力は、それを与えるためにあなたが選択した入力に基づいています。このような場合、クラスに任意に選択された入力を与え、その出力を観察し、それが期待と一致するかどうかをテストできます。

このテスト原理のいくつかの例。私の例では、常にテスト可能なメソッドの入力を直接制御していることに注意してください。

  • 「Flater」と入力すると、GetFirstLetterOfString()が「F」を返すかどうかをテストします。
  • 「Flater」と入力すると、CountLettersInString()が6を返すかどうかをテストします。
  • 「Flater」と入力したときにParseStringThatBeginsWithAnA()が例外を返すかどうかをテストします。

これらのテストはすべて、期待値が入力内容と一致している限り、必要な値を入力できます

しかし、出力が定数値によって決定される場合は、一定の期待値を作成し、最初のものが2番目に一致するかどうかをテストする必要があります。これはばかげています、これはalwaysまたはneverのいずれかです。どちらも意味のある結果ではありません。

このテスト原理のいくつかの例。これらの例では、比較される値の少なくともoneを制御できないことに注意してください。

  • _Math.Pi == 3.1415..._かどうかをテスト
  • _MyApplication.ThisConstValue == 123_かどうかをテスト

これらのテストはone特定の値です。この値を変更すると、テストは失敗します。本質的に、有効な入力に対してロジックが機能するかどうかをテストするのではなく、誰かが正確にpredict制御できない結果をテストできるかどうかをテストするだけです。

これは基本的に、テストライターのビジネスロジックに関する知識をテストすることです。コードのテストではなく、作成者自身がテストします。


あなたの例に戻る:

_class BarDerived : public Base
{
  public:
    BarDerived() : Base(12) { };
    ~BarDerived() { };
    int doBarThings() { return foo + 1; };
}
_

なぜBarDerivedは常にfooが_12_に等しいのですか?これの意味は何ですか?

そして、これをすでに決定している場合、BarDerivedが常にfooが_12_と等しいことを確認するテストを作成することで何を獲得しようとしていますか?

これは、doThings()が派生クラスでオーバーライドされる可能性があることを考慮に入れ始めると、さらに悪化します。 AnotherDeriveddoThings()をオーバーライドして、常に_foo * 2_を返すようにしたとします。これで、Base(12)の値が24であるdoThings()としてハードコーディングされたクラスが作成されます。技術的にはテスト可能ですが、コンテキスト上の意味はありません。テストは包括的ではありません。

このハードコードされた値のアプローチを使用する理由は本当に思いつきません。 有効なユースケースがある場合でも、このハードコードされた値を確認するためのテストを作成しようとしている理由がわかりません。定数値が同じ定数値と等しいかどうかをテストすることによって得るものはありません。

テストの失敗は本質的にテストが間違っているであることを証明します。テストの失敗がビジネスロジックが間違っていることを証明する結果はありません。最初に確認するために作成されたテストを確認することは事実上不可能です。

あなたが疑問に思っていた場合、問題は継承とは何の関係もありません。 happenだけで、基本クラスのコンストラクタでconst値を使用しましたが、このconst値を他の場所で使用することはできなかったでしょう。継承されたクラスに関連している。


編集

ハードコードされた値が問題にならない場合があります。 (繰り返しますが、C#の構文で申し訳ありませんが、原則は同じです)

_public class Base
{
    public int MultiplyFactor;
    protected int InitialValue;

    public Base(int value, int factor)
    {
        this.InitialValue = value;
        this.MultiplyFactor= factor;
    }

    public int GetMultipliedValue()
    {
         return this.InitialValue * this.MultiplyFactor;
    }
}

public class DoublesYourNumber : Base
{
    public DoublesYourNumber(int value) :  base(value, 2) {}
}

public class TriplesYourNumber : Base
{
    public TriplesYourNumber(int value) : base(value, 3) {}
}
_

定数値(_2_/_3_)はGetMultipliedValue()の出力値に影響を与えていますが、クラスのコンシューマーもそれを制御しています!
この例でも、意味のあるテストを書くことができます:

_var inputValue = 123;

var expectedDoubledOutputValue = inputValue * 2;
var receivedDoubledOutputValue = new DoublesYourNumber(inputValue).GetMultipliedValue();

Assert.AreEqual(expectedDoubledOutputValue , receivedDoubledOutputValue);

var expectedTripledOutputValue = inputValue * 3;
var receivedTripledOutputValue = new TriplesYourNumber(inputValue).GetMultipliedValue();

Assert.AreEqual(expectedTripledOutputValue , receivedTripledOutputValue);
_
  • 技術的には、base(value, 2)のc​​onstが_inputValue * 2_のconstと一致するかどうかをチェックするテストをまだ書いています。
  • ただし、私たちは同時にこのクラスが正しくあることもテストしています任意の値にこの所定の係数を乗算

最初の箇条書きはテストには関係ありません。二つ目は!

1
Flater