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.
quelle
Antworten:
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.
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.
quelle
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.
quelle
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.
quelle