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


8

私が以下の(過度に単純化された)クラス構造を持っていると仮定します:

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; };
}

ご覧のとおり、の値が異なるためdoThingsBaseクラスの関数は各Derivedクラスで異なる結果を返しますがfoodoOtherThings関数はすべてのクラスで同じように動作します

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


dtorだけでよろしいvirtualですか?
デュプリケータ

@Deduplicator例を簡単にするために、はい。Baseクラスは抽象的である/べきです。派生クラスは追加の機能または特殊な実装を提供します。BarDerivedそしてBase一度同じクラスであったかもしれません。同様の機能が追加されると、共通部分がBaseクラスに移動し、それぞれのDerivedクラスに異なる特殊化が実装されました。
CharonX

それほど抽象的な例ではありませんが、標準に準拠したHTMLを書き込むクラスを想像してみてください。しかし、「バニラ」実装(いくつかのことを異なる方法で実行し、標準で提供されるすべての機能をサポートしているわけではありません)。(注:この質問につながる実際のクラスは、「ブラウザーに最適化された」HTMLの記述とは何の関係もないことを言って安心します)
CharonX

回答:


3

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

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

doOtherThings各派生クラスをテストする利点は、BarDerived変更の要件がBarDerived::doOtherThings24を返す必要がある場合、BazDerivedテストケースでのテストの失敗により、別のクラスの要件を満たしていない可能性があることが示されます。


2
また、共通のテストコードを単一の関数に因数分解して、両方の個別のテストケースから呼び出されることで、重複を減らすことを妨げるものは何もありません。
Sean Burton、

1
私の見たところ、この回答全体は@SeanBurtonのコメントを明確に追加することによってのみ良い回答になります。
Doc Brown、

3

しかし、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()); }

1

ここで対立があります。

リテラル(定数値)に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);

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

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

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

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

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

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

  • テストする Math.Pi == 3.1415...
  • テストする MyApplication.ThisConstValue == 123

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

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


あなたの例に戻る:

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

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

そして、これをすでに決定しているとすると、BarDerived常にがとfoo等しいことを確認するテストを書くことによって何を獲得しようとしてい12ますか?

因数分解を開始doThings()すると、派生クラスでオーバーライドされる可能性があるため、これはさらに悪化します。それが常に戻るようAnotherDerivedにオーバーライドdoThings()する場合を想像してくださいfoo * 2。あなたのようにハードコードされたクラスの必要があるとしている今、Base(12)その、doThings()値が技術的に検証可能なものの24であるが、それはどんな文脈の意味を欠いています。テストは包括的ではありません。

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

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

あなたが疑問に思っていた場合、問題は継承とは何の関係もありません。あなただけ起こる基底クラスのコンストラクタで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);
  • 技術的には、const inがconst inとbase(value, 2)一致するかどうかをチェックするテストをまだ書いていますinputValue * 2
  • ただし、同時に、このクラスが任意の値にこの所定の係数を正しく乗算していることもテストしています。

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


すでに述べたように、これはかなり単純化されたクラス構造です。そうは言っても、あまり抽象的なものではない可能性のある例を見てみましょう。HTML書き込みクラスを想像してみてください。HTMLタグをカプセル化する標準<>中括弧は誰もが知っています。残念ながら(狂気のため)「専用」ブラウザが代わりにを使用して![{おり}]!、IntitechはこのブラウザをHTMLライターでサポートする必要があると判断しました。たとえば、今までは-とが返されるgetHeaderStart()and getHeaderEnd()関数が<HEADER>あり<\HEADER>ます。
CharonX

@CharonXクラスがタグを正しく使用しているかどうかを有意義にテストするために、タグタイプ(列挙型、使用されたタグの2つの文字列プロパティ、または同等のもの)をパブリックに構成可能にする必要があります。そうしないと、テストが機能するために必要な文書化されていない定数値がテストに散らばります。
2018

一度-あなたは、単にコピー&ペーストのすべてで、クラスや関数を変更することができ<、他と![{。しかし、それはかなり悪いでしょう。あなたは場所で変数にセットを文字(s)は、インサートの各機能を持っているので、<>行く、その派生クラス作成します-彼らは、標準準拠や非常識な、appropiateを提供しているかによって<HEADER>、または![{HEADER}]!どちらもgetHeaderStart()関数が入力に依存し、依存しているが派生クラスの構築中に設定された定数。それでも、テストしても意味がないと言ったら不安になります...
CharonX 2018

@CharonXそれはポイントではありません。重要なのは、出力(テストがパスするかどうかを明らかに決定する)は、テスト自体が制御できない値に基づいているということです。それ以外の場合は、この隠されたconstに依存しない出力値をテストする必要があります。サンプルコードは、このconst値をテストしているだけで、他にはありません。これは正しくないか、出力の実際の目的を示さないほど単純化されています。
2018

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