Spröde Unit-Tests aufgrund übermäßiger Verspottung


21

Ich habe Probleme mit unseren Unit-Tests, die wir in meinem Team implementieren. Wir versuchen, Komponententests in Legacy-Code einzufügen, der nicht gut entworfen wurde, und obwohl wir keine Schwierigkeiten mit der tatsächlichen Hinzufügung der Tests hatten, beginnen wir damit zu kämpfen, wie sich die Tests entwickeln.

Als Beispiel für das Problem nehmen wir an, Sie haben eine Methode, die im Rahmen ihrer Ausführung 5 andere Methoden aufruft. Ein Test für diese Methode kann darin bestehen, zu bestätigen, dass ein Verhalten auftritt, wenn eine dieser fünf anderen Methoden aufgerufen wird. Da ein Komponententest aus einem Grund und nur aus einem Grund fehlschlagen sollte, möchten Sie potenzielle Probleme beseitigen, die durch Aufrufen dieser anderen 4 Methoden verursacht werden, und sie verspotten. Groß! Der Komponententest wird ausgeführt, die gespielten Methoden werden ignoriert (und ihr Verhalten kann im Rahmen anderer Komponententests bestätigt werden), und die Überprüfung funktioniert.

Es gibt jedoch ein neues Problem: Der Komponententest hat genaue Kenntnisse darüber, wie Sie dieses Verhalten bestätigt haben und welche Änderungen an diesen anderen 4 Methoden in Zukunft vorgenommen wurden oder welche neuen Methoden zur übergeordneten Methode hinzugefügt werden müssen Dies führt dazu, dass der Gerätetest geändert werden muss, um mögliche Fehler zu vermeiden.

Natürlich könnte das Problem etwas gemildert werden, indem einfach mehr Methoden weniger Verhalten bewirken, aber ich hatte gehofft, dass es vielleicht eine elegantere Lösung gibt.

Hier ist ein Beispiel für einen Unit-Test, der das Problem erfasst.

Kurz gesagt, 'MergeTests' ist eine Unit-Test-Klasse, die von der von uns getesteten Klasse erbt und das Verhalten nach Bedarf überschreibt. Dies ist ein 'Muster', das wir in unseren Tests verwenden, um Aufrufe an externe Klassen / Abhängigkeiten zu überschreiben.

[TestMethod]
public void VerifyMergeStopsSpinner()
{
    var mockViewModel = new Mock<MergeTests> { CallBase = true };
    var mockMergeInfo = new MergeInfo(Mock.Of<IClaim>(), Mock.Of<IClaim>(), It.IsAny<bool>());

    mockViewModel.Setup(m => m.ClaimView).Returns(Mock.Of<IClaimView>);
    mockViewModel.Setup(
        m =>
        m.TryMergeClaims(It.IsAny<Func<bool>>(), It.IsAny<IClaim>(), It.IsAny<IClaim>(), It.IsAny<bool>(),
                         It.IsAny<bool>()));
    mockViewModel.Setup(m => m.GetSourceClaimAndTargetClaimByMergeState(It.IsAny<MergeState>())).Returns(mockMergeInfo);
    mockViewModel.Setup(m => m.SwitchToOverviewTab());
    mockViewModel.Setup(m => m.IncrementSaveRequiredNotification());
    mockViewModel.Setup(m => m.OnValidateAndSaveAll(It.IsAny<object>()));
    mockViewModel.Setup(m => m.ProcessPendingActions(It.IsAny<string>()));

    mockViewModel.Object.OnMerge(It.IsAny<MergeState>());    

    mockViewModel.Verify(mvm => mvm.StopSpinner(), Times.Once());
}

Wie ist der Rest von Ihnen damit umgegangen oder gibt es keine einfache Möglichkeit, damit umzugehen?

Update - Ich freue mich über jedes Feedback. Leider, und es ist keine Überraschung, scheint es keine großartige Lösung, Muster oder Übung zu geben, die man beim Testen von Einheiten befolgen kann, wenn der getestete Code schlecht ist. Ich habe die Antwort markiert, die diese einfache Wahrheit am besten erfasst.


Wow, ich sehe nur ein Mock-Setup, keine SUT-Instanziierung oder so, testen Sie hier eine tatsächliche Implementierung? Wer soll StopSpinner anrufen? OnMerge? Sie sollten alle Abhängigkeiten verspotten, die es
Joppe

Es ist ein bisschen schwer zu sehen, aber das Mock <MergeTests> ist das SUT. Wir setzen das CallBase-Flag, um sicherzustellen, dass die 'OnMerge'-Methode für das tatsächliche Objekt ausgeführt wird. Wir verspotten jedoch die von' OnMerge 'aufgerufenen Methoden, die dazu führen könnten, dass der Test aufgrund von Abhängigkeitsproblemen usw. fehlschlägt. Das Ziel des Tests ist die letzte Zeile - Um zu überprüfen, haben wir in diesem Fall den Spinner gestoppt.
PremiumTier

MergeTests klingt wie eine andere instrumentierte Klasse, nicht etwas, das in der Produktion lebt, daher die Verwirrung.
Joppe


1
Abgesehen von Ihren anderen Problemen scheint es mir falsch, dass Ihr SUT ein Mock <MergeTests> ist. Warum würdest du einen Mock testen? Warum testen Sie die MergeTests-Klasse nicht selbst?
Eric King

Antworten:


18
  1. Korrigieren Sie den Code, um ihn besser zu gestalten. Wenn Ihre Tests diese Probleme aufweisen, tritt in Ihrem Code ein größeres Problem auf, wenn Sie versuchen, Änderungen vorzunehmen.

  2. Wenn Sie nicht können, müssen Sie vielleicht weniger ideal sein. Test gegen die Vor- und Nachbedingungen der Methode. Wen kümmert es, wenn Sie die anderen 5 Methoden verwenden? Vermutlich haben sie eigene Komponententests, die klar machen, was den Fehler verursacht hat, wenn die Tests fehlschlagen.

"Unit-Tests sollten nur einen Grund zum Scheitern haben" ist eine gute Richtlinie, aber meiner Erfahrung nach unpraktisch. Schwer zu schreibende Tests werden nicht geschrieben. Fragile Tests werden nicht geglaubt.


Ich stimme voll und ganz der Festlegung des Code-Designs zu, aber in der weniger idealen Welt der Entwicklung für ein großes Unternehmen mit engen Zeitplänen kann es schwierig sein, die technischen Schulden, die bei früheren Teams oder schlechten Entscheidungen entstanden sind, abzuzahlen Einmal. Bis zu Ihrem zweiten Punkt ist ein Großteil der Verspottung nicht nur darauf zurückzuführen, dass der Test nur aus einem Grund fehlschlagen soll, sondern auch, dass der auszuführende Code nicht ausgeführt werden darf, ohne zuvor eine große Anzahl von Abhängigkeiten zu behandeln, die in diesem Code erstellt wurden . Es tut mir leid, dass Sie die Torpfosten auf diesen verschoben haben.
PremiumTier

Wenn ein besseres Design nicht realistisch ist, stimme ich zu: "Wen interessiert es, ob Sie die anderen 5 Methoden anwenden?" Stellen Sie sicher, dass die Methode die erforderliche Funktion ausführt und nicht, wie sie das tut.
Kwebble

@Kwebble - Verstanden, jedoch bestand das Ziel der Frage darin, festzustellen, ob es eine einfache Möglichkeit gibt, das Verhalten einer Methode zu überprüfen, wenn Sie auch andere in der Methode aufgerufene Verhaltensmerkmale ausspotten müssen, um den Test überhaupt auszuführen. Ich möchte das 'wie' entfernen, aber ich weiß nicht wie :)
PremiumTier

Es gibt keine magische Silberkugel. Es gibt keine "einfache Möglichkeit", schlechten Code zu testen. Entweder muss der zu testende Code überarbeitet werden, oder der Testcode selbst ist ebenfalls schlecht. Entweder ist der Test schlecht, weil er zu spezifisch für die internen Details ist, auf die Sie gestoßen sind, oder Sie können die Tests , wie bereits vorgeschlagen, in einer Arbeitsumgebung ausführen, aber dann sind die Tests viel langsamer und komplexer. In beiden Fällen sind die Tests schwieriger zu schreiben, schwieriger zu warten und anfällig für falsch-negative Ergebnisse.
Steven Doggart

8

Die Aufteilung großer Methoden in konzentrierte kleine Methoden ist definitiv eine bewährte Methode. Sie sehen es als Schmerz an, wenn Sie das Verhalten von Komponententests überprüfen, aber Sie erleben den Schmerz auch auf andere Weise.

Das heißt, es ist eine Häresie, aber ich persönlich bin ein Fan von realistischen temporären Testumgebungen. Das heißt, anstatt alles zu verspotten, was in diesen anderen Methoden verborgen ist, stellen Sie sicher, dass es eine einfach einzurichtende temporäre Umgebung gibt (komplett mit privaten Datenbanken und Schemata - SQLite kann hier helfen), mit der Sie all diese Dinge ausführen können. Die Verantwortung dafür, zu wissen, wie diese Testumgebung erstellt / heruntergefahren wird, liegt bei dem Code, der dies erfordert, sodass Sie bei Änderungen nicht den gesamten Komponententestcode ändern müssen, der von ihrer Existenz abhängt.

Aber ich stelle fest, dass dies eine Ketzerei meinerseits ist. Menschen, die sich intensiv mit Unit-Tests beschäftigen, befürworten "reine" Unit-Tests und nennen das, was ich beschrieben habe, "Integrationstests". Ich persönlich mache mir keine Sorgen um diesen Unterschied.


3

Ich würde in Betracht ziehen, die Mocks zu lockern und nur Tests zu formulieren, die die Methoden enthalten könnten, nach denen sie aufgerufen werden.

Testen Sie nicht das Wie , testen Sie das Was . Auf das Ergebnis kommt es an, gegebenenfalls auch auf die Untermethoden.

Aus einem anderen Blickwinkel könnten Sie einen Test formulieren, ihn mit einer großen Methode bestehen, umgestalten und nach dem Umgestalten einen Methodenbaum erhalten. Sie müssen nicht jeden einzeln testen. Auf das Endergebnis kommt es an.

Wenn die Untermethoden das Testen einiger Aspekte erschweren, sollten Sie sie in getrennte Klassen aufteilen, damit Sie sie dort sauberer verspotten können, ohne dass Ihre zu testende Klasse stark instrumentiert / gesäumt ist. Es ist schwer zu sagen, ob Sie tatsächlich eine konkrete Implementierung in Ihrem Beispieltest testen.


Das Problem ist, dass wir uns über das Wie lustig machen müssen, um das Was zu testen. Es ist eine Beschränkung, die durch das Design des Codes auferlegt wird. Ich möchte mich auf keinen Fall über das Wie lustig machen, da dies den Test spröde macht.
PremiumTier

Wenn ich mir die Methodennamen ansehe, denke ich, dass Ihre getestete Klasse einfach zu viele Aufgaben übernimmt. Informieren Sie sich über das Prinzip der Einzelverantwortung. Das Ausleihen bei MVC kann hilfreich sein. Ihre Klasse scheint sowohl die Benutzeroberfläche als auch die Infrastruktur und die geschäftlichen Belange zu berücksichtigen.
Joppe

Ja :( Das wäre der schlecht designte Legacy-Code, auf den ich mich bezog. Wir arbeiten an der Neugestaltung und dem Refactor, aber wir
hielten
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.