it-swarm.com.de

Was ist der richtige Ansatz, um Klassen mit Vererbung zu testen?

Angenommen, ich habe die folgende (stark vereinfachte) Klassenstruktur:

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

Wie Sie sehen können, gibt die Funktion doThings in der Basisklasse aufgrund der unterschiedlichen Werte von foo in jeder abgeleitete Klasse unterschiedliche Ergebnisse zurück , während sich die Funktion doOtherThings in allen Klassen identisch verhält.

Wenn ich Komponententests für diese Klassen implementieren möchte, ist mir die Behandlung von doThings, doBarThings/doBazThings klar - sie müssen für jede abgeleitete Klasse abgedeckt werden. Aber wie soll doOtherThings behandelt werden? Ist es empfehlenswert, den Testfall in beiden abgeleiteten Klassen im Wesentlichen zu duplizieren? Das Problem wird schlimmer, wenn es ein halbes Dutzend Funktionen wie doOtherThings und mehr Abgeleitete Klassen gibt.

8
CharonX

In Ihren Tests für BarDerived möchten Sie beweisen, dass alle (öffentlichen) Methoden von BarDerived korrekt funktionieren (für die von Ihnen getesteten Situationen). Ähnliches gilt für BazDerived.

Die Tatsache, dass einige der Methoden in einer Basisklasse implementiert sind, ändert dieses Testziel für BarDerived und BazDerived nicht. Das führt zu dem Schluss, dass Base::doOtherThings sollte sowohl im Kontext von BarDerived als auch BazDerived getestet werden und dass Sie für diese Funktion sehr ähnliche Tests erhalten.

Der Vorteil des Testens von doOtherThings für jede abgeleitete Klasse besteht darin, dass sich die Anforderungen für BarDerived so ändern, dass BarDerived::doOtherThings muss 24 zurückgeben, dann zeigt der Testfehler im Testfall BazDerived an, dass Sie möglicherweise gegen die Anforderungen einer anderen Klasse verstoßen.

Aber wie soll man mit anderen Dingen umgehen? Ist es empfehlenswert, den Testfall in beiden abgeleiteten Klassen im Wesentlichen zu duplizieren?

Normalerweise würde ich erwarten, dass Base eine eigene Spezifikation hat, die Sie für jede konforme Implementierung überprüfen können, einschließlich der abgeleiteten Klassen.

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

Sie haben hier einen Konflikt.

Sie möchten den Rückgabewert von doThings() testen, der auf einem Literal (const value) basiert.

Jeder Test, den Sie dafür schreiben, läuft von Natur aus auf Testen eines konstanten Werts, was unsinnig ist.


Um Ihnen ein sinnlicheres Beispiel zu zeigen (ich bin schneller mit C #, aber das Prinzip ist das gleiche)

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

Diese Klasse kann sinnvoll getestet werden:

var inputValue = 123;

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

Assert.AreEqual(receivedOutputValue, expectedOutputValue);

Das Testen ist sinnvoller. Die Ausgabe basiert auf der Eingabe, die Sie ausgewählt gegeben haben. In einem solchen Fall können Sie einer Klasse eine willkürlich ausgewählte Eingabe geben, ihre Ausgabe beobachten und testen, ob sie Ihren Erwartungen entspricht.

Einige Beispiele für dieses Testprinzip. Beachten Sie, dass meine Beispiele immer die direkte Kontrolle über die Eingabe der testbaren Methode haben.

  • Testen Sie, ob GetFirstLetterOfString() "F" zurückgibt, wenn ich "Flater" eingebe.
  • Testen Sie, ob CountLettersInString() 6 zurückgibt, wenn ich "Flater" eingebe.
  • Testen Sie, ob ParseStringThatBeginsWithAnA() eine Ausnahme zurückgibt, wenn ich "Flater" eingebe.

Alle diese Tests können einen beliebigen Wert eingeben , sofern ihre Erwartungen mit den eingegebenen übereinstimmen.

Wenn Ihre Ausgabe jedoch durch einen konstanten Wert bestimmt wird, müssen Sie eine konstante Erwartung erstellen und dann testen, ob die erste mit der zweiten übereinstimmt. Was albern ist, wird entweder immer oder nie vergehen; Beides ist kein aussagekräftiges Ergebnis.

Einige Beispiele für dieses Testprinzip. Beachten Sie, dass diese Beispiele keine Kontrolle über mindestens einen der zu vergleichenden Werte haben.

  • Testen Sie, ob Math.Pi == 3.1415...
  • Testen Sie, ob MyApplication.ThisConstValue == 123

Diese Tests für eins bestimmten Wert. Wenn Sie diesen Wert ändern, schlagen Ihre Tests fehl. Im Wesentlichen testen Sie nicht, ob Ihre Logik für eine gültige Eingabe funktioniert, sondern nur, ob jemand in der Lage ist, ein Ergebnis, über das er keine Kontrolle hat, genau vorherzusagen.

Das testet im Wesentlichen das Wissen des Testautors über die Geschäftslogik. Es wird nicht der Code getestet, sondern der Autor selbst.


Zurück zu Ihrem Beispiel:

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

Warum hat BarDerived immer ein foo gleich 12? Was ist die Bedeutung davon?

Und da Sie dies bereits entschieden haben, was versuchen Sie zu erreichen, indem Sie einen Test schreiben, der bestätigt, dass BarDerived immer ein foo gleich 12 Hat?

Dies wird noch schlimmer, wenn Sie anfangen zu berücksichtigen, dass doThings() in einer abgeleiteten Klasse überschrieben werden kann. Stellen Sie sich vor, AnotherDerived würde doThings() überschreiben, sodass immer foo * 2 Zurückgegeben wird. Jetzt haben Sie eine Klasse, die als Base(12) fest codiert ist und deren Wert doThings() 24 beträgt. Obwohl sie technisch testbar ist, hat sie keine kontextbezogene Bedeutung. Der Test ist nicht nachvollziehbar.

Ich kann mir wirklich keinen Grund vorstellen, diesen fest codierten Wertansatz zu verwenden. Auch wenn es einen gültigen Anwendungsfall gibt, verstehe ich nicht, warum Sie versuchen, einen Test zu schreiben, um diesen fest codierten Wert zu bestätigen. Es gibt nichts zu gewinnen, wenn getestet wird, ob ein konstanter Wert dem gleichen konstanten Wert entspricht.

Jeder Testfehler beweist von Natur aus, dass der Test ist falsch. Es gibt kein Ergebnis, bei dem ein Testfehler beweist, dass die Geschäftslogik falsch ist. Sie können effektiv nicht bestätigen, welche Tests erstellt wurden, um sie überhaupt zu bestätigen.

Das Problem hat nichts mit Vererbung zu tun, falls Sie sich fragen. Sie haben gerade zufällig einen const-Wert im Basisklassenkonstruktor verwendet, aber Sie hätten diesen const-Wert an einer anderen Stelle verwenden können, und dann würde er es nicht tun mit einer geerbten Klasse verwandt sein.


Bearbeiten

Es gibt Fälle, in denen fest codierte Werte kein Problem darstellen. (Nochmals, entschuldigen Sie die C # -Syntax, aber das Prinzip ist immer noch dasselbe)

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) {}
}

Während der konstante Wert (2/3) Noch den Ausgabewert von GetMultipliedValue() beeinflusst, hat der Verbraucher Ihrer Klasse immer noch die Kontrolle über es auch!
In diesem Beispiel können noch aussagekräftige Tests geschrieben werden:

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);
  • Technisch gesehen schreiben wir immer noch einen Test, der prüft, ob die Konstante in base(value, 2) mit der Konstante in inputValue * 2 Übereinstimmt.
  • Wir testen jedoch gleichzeitig auch, ob diese Klasse korrekt ist multiplizieren Sie einen bestimmten Wert mit diesem vorgegebenen Faktor.

Der erste Aufzählungspunkt ist für den Test nicht relevant. Der zweite ist!

1
Flater