Soll ich geerbte Methoden testen?

30

Angenommen , ich habe einen Klasse - Manager von einer Basisklasse abgeleiteten Mitarbeitern und dass Mitarbeiter haben eine Methode getEmail () , die von geerbt wird Manager . Soll ich testen, ob das Verhalten der getEmail () -Methode eines Managers tatsächlich dem eines Mitarbeiters entspricht?

Zum Zeitpunkt der Erstellung dieser Tests ist das Verhalten dasselbe, aber irgendwann in der Zukunft könnte natürlich jemand diese Methode außer Kraft setzen, ihr Verhalten ändern und daher meine Anwendung unterbrechen. Es scheint jedoch ein bisschen seltsam, im Wesentlichen auf das Fehlen von Einmischcode zu prüfen .

(Beachten Sie, dass das Testen der Manager :: getEmail () -Methode die Codeabdeckung (oder andere Codequalitätsmetriken (?)) Erst verbessert, wenn Manager :: getEmail () erstellt / überschrieben wird.)

(Wenn die Antwort "Ja" lautet, sind einige Informationen zum Verwalten von Tests hilfreich, die zwischen Basisklassen und abgeleiteten Klassen geteilt werden.)

Eine äquivalente Formulierung der Frage:

Wenn eine abgeleitete Klasse eine Methode von einer Basisklasse erbt, wie können Sie ausdrücken (testen), ob Sie von der geerbten Methode Folgendes erwarten:

  1. Verhalten Sie sich genauso wie die Basis im Moment (wenn sich das Verhalten der Basis ändert, ändert sich das Verhalten der abgeleiteten Methode nicht).
  2. Verhalten Sie sich für alle Zeiten genauso wie die Basis (wenn sich das Verhalten der Basisklasse ändert, ändert sich auch das Verhalten der abgeleiteten Klasse). oder
  3. Benimm dich, wie es dir gefällt (das Verhalten dieser Methode ist dir egal, weil du sie nie aufrufst).
mjs
quelle
1
IMO eine ManagerKlasse abzuleiten, Employeewar der erste große Fehler.
CodesInChaos
4
@CodesInChaos Ja, das könnte ein schlechtes Beispiel sein. Das gleiche Problem tritt jedoch immer dann auf, wenn Sie eine Vererbung haben.
mjs
1
Sie müssen die Methode nicht einmal überschreiben. Die Basisklassenmethode ruft möglicherweise andere überschriebene Instanzmethoden auf, und die Ausführung desselben Quellcodes für die Methode führt immer noch zu einem anderen Verhalten.
gnasher729

Antworten:

23

Ich würde hier einen pragmatischen Ansatz verfolgen: Wenn in Zukunft jemand Manager :: getMail überschreibt, liegt es in der Verantwortung des Entwicklers, Testcode für die neue Methode bereitzustellen.

Das gilt natürlich nur, wenn Manager::getEmailwirklich derselbe Codepfad wie Employee::getEmail! Auch wenn die Methode nicht überschrieben wird, verhält sie sich möglicherweise anders:

  • Employee::getEmailkönnte eine geschützte virtuelle aufrufen, getInternalEmaildie in überschrieben wird Manager.
  • Employee::getEmailkönnte auf einen internen Status (z. B. ein Feld _email) zugreifen , der sich in den beiden Implementierungen unterscheiden kann: Die Standardimplementierung von Employeekönnte beispielsweise sicherstellen, dass dies _emailimmer der Fall ist [email protected], Managerist jedoch flexibler bei der Zuweisung von Mailadressen.

In solchen Fällen ist es möglich, dass sich ein Fehler nur in manifestiert Manager::getEmail, obwohl die Implementierung der Methode selbst dieselbe ist. In diesem Fall kann eine Manager::getEmailseparate Prüfung sinnvoll sein.

Heinzi
quelle
14

Ich würde.

Wenn Sie denken "gut, es ruft wirklich nur Employee :: getEmail () auf, weil ich es nicht überschreibe, also muss ich Manager :: getEmail () nicht testen", dann testen Sie das Verhalten nicht wirklich von Manager :: getEmail ().

Ich würde überlegen, was nur Manager :: getEmail () tun sollte, nicht, ob es vererbt oder überschrieben wird. Wenn das Verhalten von Manager :: getEmail () sein sollte, das zurückzugeben, was Employee :: getMail () zurückgibt, dann ist das der Test. Wenn das Verhalten "[email protected]" zurückgeben soll, ist das der Test. Es spielt keine Rolle, ob es durch Vererbung implementiert oder überschrieben wird.

Was wichtig ist, ist, dass wenn es sich in der Zukunft ändert, Ihr Test es fängt und Sie wissen, dass etwas entweder kaputt ist oder überdacht werden muss.

Einige Leute mögen mit der scheinbaren Redundanz in der Prüfung nicht einverstanden sein, aber mein Gegenargument wäre, dass Sie das Verhalten von Employee :: getMail () und Manager :: getMail () als unterschiedliche Methoden testen, unabhängig davon, ob sie geerbt wurden oder nicht überschrieben. Wenn ein zukünftiger Entwickler das Verhalten von Manager :: getMail () ändern muss, muss er auch die Tests aktualisieren.

Die Meinungen können jedoch variieren, ich denke, Digger und Heinzi haben vernünftige Gründe für das Gegenteil angegeben.

seggy
quelle
1
Ich mag das Argument, dass Verhaltenstests orthogonal zu der Art und Weise sind, wie eine Klasse konstruiert wird. Es scheint allerdings ein bisschen komisch, die Vererbung komplett zu ignorieren. (Und wie verwalten Sie die gemeinsamen Tests, wenn Sie viel Vererbung haben?)
mjs
1
Gut gesagt. Nur weil Manager eine geerbte Methode verwendet, bedeutet dies keinesfalls, dass diese nicht getestet werden sollte. Ein großartiger Hinweis von mir sagte einmal: "Wenn es nicht getestet wird, ist es kaputt." Zu diesem Zweck können Sie sicher sein, dass der Entwickler, der neuen Code für Manager eincheckt, der möglicherweise das Verhalten der geerbten Methode ändert, den Test an das neue Verhalten anpasst, wenn Sie die erforderlichen Tests für Mitarbeiter und Manager beim Einchecken des Codes vornehmen . Oder klopf an deine Tür.
HurricaneMitch
2
Sie möchten die Manager-Klasse wirklich testen. Die Tatsache, dass es sich um eine Unterklasse von Employee handelt und getMail () mithilfe der Vererbung implementiert wird, ist nur ein Implementierungsdetail, das Sie beim Erstellen von Komponententests ignorieren sollten. Im nächsten Monat stellen Sie fest, dass es eine schlechte Idee war, Manager von Employee zu erben, und Sie ersetzen die gesamte Vererbungsstruktur und implementieren alle Methoden neu. Ihre Komponententests sollten Ihren Code weiterhin ohne Probleme testen.
gnasher729
5

Testen Sie das Gerät nicht. Führen Sie einen Funktions- / Abnahmetest durch.

Unit-Tests sollten jede Implementierung testen. Wenn Sie keine neue Implementierung bereitstellen, halten Sie sich an das DRY-Prinzip. Wenn Sie sich hier etwas anstrengen möchten, können Sie den ursprünglichen Komponententest verbessern. Nur wenn Sie die Methode überschreiben, sollten Sie einen Komponententest schreiben.

Gleichzeitig sollte durch Funktions- / Akzeptanztests sichergestellt werden, dass Ihr gesamter Code am Ende des Tages das tut, was er soll, und hoffentlich jede Verrückung durch Vererbung auffängt.

MrFox
quelle
4

Robert Martins Regeln für TDD sind:

  1. Sie dürfen keinen Produktionscode schreiben, es sei denn, es wird ein fehlerhafter Einheitentest bestanden.
  2. Sie dürfen nicht mehr von einem Komponententest schreiben, als zum Scheitern ausreicht. und Kompilierungsfehler sind Fehler.
  3. Sie dürfen nicht mehr Seriencode schreiben, als ausreicht, um den einen fehlgeschlagenen Komponententest zu bestehen.

Wenn Sie diese Regeln befolgen, könnten Sie diese Frage nicht stellen. Wenn die getEmailMethode getestet werden muss, wäre sie getestet worden.

Daniel Kaplan
quelle
3

Ja, Sie sollten geerbte Methoden testen, da sie in Zukunft möglicherweise überschrieben werden. Außerdem können geerbte Methoden überschriebene virtuelle Methoden aufrufen , wodurch sich das Verhalten der nicht überschriebenen geerbten Methode ändert.

Ich teste dies, indem ich eine abstrakte Klasse erstelle, um die (möglicherweise abstrakte) Basisklasse oder Schnittstelle wie folgt zu testen (in C # mit NUnit):

public abstract class EmployeeTests
{
    protected abstract Employee CreateInstance(string name, int age);

    [Test]
    public void GetEmail_ReturnsValidEmailAddress()
    {
        // Given
        var sut = CreateInstance("John Doe", 20);

        // When
        string email = sut.GetEmail();

        // Then
        Assert.IsTrue(Helper.IsValidEmail(email));
    }
}

Dann habe ich eine Klasse mit spezifischen Tests für Managerund integriere die Tests des Mitarbeiters wie folgt:

[TestFixture]
public class ManagerTests
{
    // Other tests.

    [TestFixture]
    public class ManagerEmployeeTests : EmployeeTests
    {
        protected override Employee CreateInstance(string name, int age);
        {
            return new Manager(name, age);
        }
    }
}

Der Grund, warum ich das kann, ist Liskovs Substitutionsprinzip: Die Tests von Employeesollten immer noch bestehen, wenn ein ManagerObjekt übergeben wird, da es von stammt Employee. Daher muss ich meine Tests nur einmal schreiben und kann überprüfen, ob sie für alle möglichen Implementierungen der Schnittstelle oder Basisklasse funktionieren.

Daniel AA Pelsmaeker
quelle
Guter Punkt zu Liskovs Substitutionsprinzip: Wenn dies zutrifft, besteht die abgeleitete Klasse alle Tests der Basisklasse. LSP wird jedoch häufig verletzt, auch durch die setUp()Methode von xUnit selbst! Und so ziemlich jedes Web-MVC-Framework, das das Überschreiben einer "Index" -Methode beinhaltet, bricht auch LSP, was im Grunde genommen alles von ihnen ist.
mjs
Dies ist absolut die richtige Antwort - ich habe gehört, dass es das "abstrakte Testmuster" heißt. Warum sollte man aus Neugier eine verschachtelte Klasse verwenden, anstatt die Tests nur über einzumischen class ManagerTests : ExployeeTests? (dh Spiegeln der Vererbung der getesteten Klassen.) Ist es hauptsächlich ästhetisch, um die Anzeige der Ergebnisse zu erleichtern?
Luke Usherwood
2

Soll ich testen, ob das Verhalten der getEmail () -Methode eines Managers tatsächlich dem eines Mitarbeiters entspricht?

Ich würde nein sagen, da es meiner Meinung nach ein wiederholter Test wäre, würde ich einmal in den Mitarbeiter-Tests testen und das wäre es.

Zu dem Zeitpunkt, an dem diese Tests geschrieben werden, wird das Verhalten das gleiche sein, aber irgendwann in der Zukunft könnte jemand diese Methode außer Kraft setzen und ihr Verhalten ändern

Wenn die Methode überschrieben wird, benötigen Sie neue Tests, um das überschriebene Verhalten zu überprüfen. Das ist die Aufgabe der Person, die die übergeordnete getEmail()Methode implementiert .


quelle
1

Nein, Sie müssen geerbte Methoden nicht testen. Klassen und ihre Testfälle, die auf dieser Methode basieren, brechen trotzdem ab, wenn sich das Verhalten in geändert hat Manager.

Stellen Sie sich das folgende Szenario vor: Die E-Mail-Adresse lautet "[email protected]":

class Employee{
    String firstname, lastname;
    String getEmail() { 
        return firstname + "." + lastname + "@example.com";
    }
}

Sie haben das Gerät getestet und es funktioniert gut für Ihre Employee. Sie haben auch eine Klasse erstellt Manager:

class Manager extends Employee { /* nothing different in email generation */ }

Jetzt haben Sie eine Klasse, ManagerSortdie Manager anhand ihrer E-Mail-Adresse in einer Liste sortiert. Natürlich nehmen Sie an, dass die E-Mail-Generierung mit der folgenden identisch ist Employee:

class ManagerSort {
    void sortManagers(Manager[] managerArrayToBeSorted)
        // sort based on email address omitted
    }
}

Sie schreiben einen Test für Ihre ManagerSort:

void testManagerSort() {
    Manager[] managers = ... // build random manager list
    ManagerSort.sortManagers(managers);

    Manager[] expected = ... // expected result
    assertEquals(expected, managers); // check the result
}

Alles funktioniert gut. Jetzt kommt jemand und überschreibt die getEmail()Methode:

class Manager extends Employee {
    String getEmail(){
        // managers should have their lastname and firstname order changed
        return lastname + "." + firstname + "@example.com";
    }
}

Was passiert jetzt? Ihr testManagerSort()wird scheitern, weil das getEmail()von Managerüberschrieben wurde. Sie werden in dieser Ausgabe nachforschen und die Ursache finden. Und das alles, ohne einen separaten Testfall für die geerbte Methode zu schreiben.

Daher müssen Sie geerbte Methoden nicht testen.

Ansonsten zB in Java, würden Sie alle Methoden von geerbt testen Objectwie toString(), equals()usw. in jeder Klasse.

Uooo
quelle
Du sollst mit Unit-Tests nicht schlau sein. Sie sagen: "Ich muss X nicht testen, weil Y fehlschlägt, wenn X fehlschlägt." Unit-Tests basieren jedoch auf den Annahmen von Fehlern in Ihrem Code. Warum sollten Sie angesichts von Fehlern in Ihrem Code glauben, dass komplexe Beziehungen zwischen Tests so funktionieren, wie Sie es erwarten?
gnasher729
@ gnasher729 Ich sage "Ich muss X in der Unterklasse nicht testen, da es bereits in den Komponententests für die Superklasse getestet wurde ". Wenn Sie die Implementierung von X ändern, müssen Sie natürlich entsprechende Tests dafür schreiben.
Uooo