Testen nur mit öffentlichen Methoden ist schlecht

Insbesondere bei der Programmierung und bei TDD gibt es gute Grundsätze, die eingehalten werden sollten: TROCKEN und Testen mit öffentlichen Methoden. Sie haben sich in der Praxis wiederholt bewährt, aber in Projekten mit einem großen Legacy-Code können sie eine "dunkle Seite" haben. Sie können beispielsweise Code schreiben, der sich an diesen Prinzipien orientiert, und dann Tests zerlegen, die mehr als 20 Abstraktionen mit einer Konfiguration abdecken, die unvergleichlich größer als die getestete Logik ist. Diese "dunkle Seite" macht den Menschen Angst und hemmt die Verwendung von TDD in Projekten. Unter dem Strich diskutiere ich, warum das Testen mit öffentlichen Methoden schlecht ist und wie die Probleme, die aufgrund dieses Prinzips entstehen, reduziert werden können.

Haftungsausschluss
Ich möchte sofort den möglichen Eindruck zerstreuen. Einige spüren möglicherweise nicht einmal die Nachteile, die diskutiert werden, beispielsweise aufgrund der Größe ihrer Projekte. Auch diese Mängel sind meiner Meinung nach Teil der technischen Verschuldung und weisen dieselben Merkmale auf: Das Problem wird zunehmen, wenn es nicht beachtet wird. Daher ist es notwendig, entsprechend der Situation zu entscheiden.

Die dem Prinzip zugrunde liegende Idee klingt gut: Sie müssen das Verhalten testen, nicht die Implementierung. Dies bedeutet, dass Sie nur die Klassenschnittstelle testen müssen. In der Praxis ist dies nicht immer der Fall. Stellen Sie sich vor, Sie haben eine Methode, mit der die Kosten der in Schichtarbeit tätigen Arbeitnehmer berechnet werden, um das Wesentliche des Problems darzustellen. Dies ist eine nicht triviale Aufgabe, wenn es um Schichtarbeit geht Sie haben Tipps, Boni, Wochenenden, Feiertage, Unternehmensregeln usw. usw. Diese Methode führt intern viele Vorgänge aus und verwendet andere Dienste, die Informationen zu Feiertagen, Tipps usw. liefern Wenn Sie einen Komponententest dafür schreiben, müssen Sie eine Konfiguration für alle verwendeten Dienste erstellen, wenn sich der getestete Code irgendwo am Ende der Methode befindet. Gleichzeitig kann der getestete Code selbst nur teilweise oder überhaupt keine konfigurierbaren Dienste verwenden. Und es gibt bereits einige Unit-Tests, die auf diese Weise geschrieben wurden.

Minus 1: Überkonfiguration des Komponententests


Jetzt möchten Sie eine Reaktion auf eine neue Funktion hinzufügen, die nicht trivial logisch ist und auch irgendwo am Ende der Methode verwendet wird. Das Flag ist so beschaffen, dass es Teil der Servicelogik ist und gleichzeitig nicht Teil der Serviceschnittstelle. Im obigen Fall ist dieser Code nur für diese öffentliche Methode relevant und kann im Allgemeinen in die alte Methode eingeschrieben werden.

Wenn das Projekt die Regel übernommen hat, alles nur mit öffentlichen Methoden zu testen, kann der Entwickler höchstwahrscheinlich nur einen vorhandenen Komponententest kopieren und ein wenig optimieren. Im neuen Test werden weiterhin alle Dienste konfiguriert, um die Methode auszuführen. Einerseits haben wir das Prinzip eingehalten, andererseits haben wir einen Unit-Test mit Überkonfiguration erhalten. Wenn in Zukunft etwas kaputt geht oder eine Konfigurationsänderung erforderlich ist, müssen Sie die Affenarbeit ausführen, um die Tests anzupassen. Es ist langweilig, langwierig und bringt dem Kunden weder Freude noch offensichtlichen Nutzen. Es scheint, dass wir dem richtigen Prinzip folgen, aber wir befinden uns in der gleichen Situation, von der wir weg wollten, und weigern uns, private Methoden zu testen.

Minus 2: Unvollständige Abdeckung


Weiter kann ein menschlicher Faktor wie Faulheit eingreifen. Beispielsweise kann eine private Methode mit nicht trivialer Flag-Logik wie in diesem Beispiel aussehen.

private bool HasShifts(DateTime date, int tolerance, bool clockIn, Shift[] shifts, int[] locationIds) { bool isInLimit(DateTime date1, DateTime date2, int limit) => Math.Abs(date2.Subtract(date1).TotalMinutes) <= limit; var shiftsOfLocations = shifts.Where(x => locationIds.Contains(x.LocationId)); return clockIn ? shiftsOfLocations.Any(x => isInLimit(date, x.StartDate, tolerance)) : shiftsOfLocations.Any(x => isInLimit(date, x.EndDate, tolerance)); } 

Diese Methode erfordert 10 Überprüfungen, um alle Fälle abzudecken, von denen 8 von Bedeutung sind.

Dekodierung von 8 wichtigen Fällen
  • shiftOfLocations - 2 Werte - ob oder nicht
  • clockIn - 2 Werte - wahr oder falsch
  • Toleranz - 2 verschiedene Bedeutungen

Gesamt: 2 x 2 x 2 = 8

Wenn ein Komponententest geschrieben wird, um diese Logik zu testen, muss ein Entwickler mindestens 8 große Komponententests schreiben. Ich bin auf Fälle gestoßen, in denen die Unit-Test-Konfiguration mehr als 50 Codezeilen mit 4 Zeilen eines direkten Anrufs umfasste. Das heißt, Nur etwa 10% des Codes tragen eine Nutzlast. In diesem Fall ist die Versuchung groß, den Arbeitsaufwand durch weniger Unit-Tests zu reduzieren. Infolgedessen bleiben von 8 beispielsweise nur zwei Einheitentests für jeden clockIn-Wert übrig. Diese Situation führt dazu, dass es entweder mühsam und langwierig ist, alle erforderlichen Tests zu schreiben und die Konfiguration zu erstellen (Strg + C, V funktioniert, wo wäre es ohne), oder die Methode bleibt nur teilweise abgedeckt. Jede Option hat ihre unangenehmen Folgen.

Mögliche Lösungen


Neben dem Prinzip "Testverhalten" gibt es noch OCP (Open / Closed-Prinzip). Wenn Sie es richtig anwenden, können Sie vergessen, was „fragile Tests“ sind, und das interne Verhalten des Moduls testen. Wenn Sie ein neues Modulverhalten benötigen, schreiben Sie neue Komponententests für die neue Nachfolgeklasse, in der das gewünschte Verhalten geändert wird. Dann müssen Sie keine Zeit damit verbringen, vorhandene Tests erneut zu überprüfen und zu aktualisieren. In diesem Fall kann diese Methode als intern oder intern geschützt deklariert und durch Hinzufügen von InternalsVisibleTo zur Assembly getestet werden. In diesem Fall leidet Ihre IClass-Schnittstelle nicht, und die Tests sind am lakonischsten und unterliegen keinen häufigen Änderungen.

Eine andere Alternative wäre, eine zusätzliche Hilfsklasse zu deklarieren, in die unsere Methode gezogen werden kann, indem sie als öffentlich deklariert wird. Dann wird das Prinzip eingehalten und der Test wird präzise sein. Meiner Meinung nach zahlt sich dieser Ansatz nicht immer aus. Einige entscheiden sich beispielsweise dafür, auch nur eine Methode in eine Klasse zu ziehen, was zur Erstellung einer Reihe von Klassen mit einer Methode führt. Das andere Extrem kann darin bestehen, solche Methoden in eine Hilfsklasse abzulegen, die sich in eine GOTT-Hilfsklasse verwandelt. Diese Option mit einem Helfer ist jedoch möglicherweise die einzige, wenn die Arbeitsbaugruppe mit einem starken Namen signiert ist und Sie die Testbaugruppe aus irgendeinem Grund nicht signieren können. InternalsVisibleTo funktioniert, wenn beide Assemblys gleichzeitig signiert sind oder nicht.

Zusammenfassung


Und am Ende leidet aufgrund einer Kombination solcher Probleme die Idee von TDD- und Unit-Tests, weil niemand hat den Wunsch, volumetrische Tests zu schreiben und sie zu unterstützen. Ich freue mich über Beispiele dafür, wie die strikte Einhaltung dieses Prinzips zu Problemen und einer geringeren Motivation führte, Tests des Entwicklungsteams zu schreiben.

Source: https://habr.com/ru/post/de419873/


All Articles