Unit-Tests erfolgreich Strategien für Unit-Tests

Die Testansätze hängen davon ab, wo Sie sich im Projekt befinden, und Ihr „Budget“ hängt von Zeit, Geld, Arbeitskräften, Bedarf usw. ab. Im Idealfall werden Unit-Tests in den Entwicklungsprozess einbezogen, realistisch gesehen stoßen wir jedoch häufig auf vorhandene oder ältere Programme die wenig oder keine Codeabdeckung haben, jedoch aufgerüstet oder gewartet werden müssen. 

Das schlimmste Szenario ist ein Produkt, das sich derzeit in der Entwicklung befindet, jedoch während seiner Entwicklung eine zunehmende Anzahl von Fehlern aufweist, die wiederum nur wenig oder keine Codeabdeckung aufweisen. Als Produktmanager ist es wichtig, entweder zu Beginn einer Entwicklungsarbeit oder aufgrund der Übergabe einer vorhandenen Anwendung, eine vernünftige Komponententeststrategie zu entwickeln. 

Denken Sie daran, dass Komponententests Ihrem Projekt messbare Vorteile bringen sollten, um die Haftung ihrer Entwicklung, Wartung und eigenen Tests auszugleichen. Darüber hinaus kann die Strategie, die Sie für Ihre Gerätetests verwenden, die Architektur Ihrer Anwendung beeinflussen. Obwohl dies fast immer eine gute Sache ist, kann dies unnötigen Overhead für Ihre Bedürfnisse verursachen.

Ausgehend von den Anforderungen

Wenn Sie eine ausreichend komplexe Anwendung von einem sauberen Plan aus starten und Ihnen nur eine Reihe von Anforderungen zur Verfügung steht, beachten Sie die folgenden Hinweise.

Priorisierung der rechnerischen Anforderungen

Priorisieren Sie die rechnerischen Anforderungen der Anwendung, um festzustellen, wo die Komplexität liegt. Die Komplexität kann bestimmt werden, indem die Anzahl der Zustände ermittelt wird, die eine bestimmte Berechnung berücksichtigen muss, oder sie kann das Ergebnis eines großen Satzes von Eingabedaten sein, die zur Durchführung der Berechnung erforderlich sind, oder sie könnte einfach algorithmisch komplex sein, z. B. eine Fehlerfallanalyse auf einem Redundanzring eines Satelliten. Berücksichtigen Sie auch, wo sich der Code in der Zukunft aufgrund unbekannter Anforderungen ändern kann. Während das klingt, als ob Hellsehen erforderlich wäre, kann ein erfahrener Softwarearchitekt Code in allgemeine Zwecke (Lösung eines allgemeinen Problems) und domänenspezifisch (Lösung eines spezifischen Anforderungsproblems) einordnen. Letzteres wird ein Kandidat für zukünftige Veränderungen.

Während das Schreiben von Komponententests für triviale Funktionen in der Anzahl der Testfälle, die das Programm durchläuft, einfach, schnell und erfreulich ist, sind sie die kostengünstigsten Tests. Sie brauchen Zeit zum Schreiben, da sie höchstwahrscheinlich korrekt geschrieben werden Am Anfang werden sie sich wahrscheinlich im Laufe der Zeit nicht ändern. Sie sind am wenigsten nützlich, wenn die Codebasis der Anwendung wächst. Konzentrieren Sie Ihre Komponententeststrategie stattdessen auf den domänenspezifischen und komplexen Code.

Wählen Sie eine Architektur aus

Wenn Sie ein Projekt aus einer Reihe von Anforderungen starten, können Sie die Architektur (oder eine Architektur eines Drittanbieters) als Teil des Entwicklungsprozesses erstellen. Frameworks von Drittanbietern, mit denen Sie Architekturen wie die Umkehrung der Kontrolle (und das zugehörige Konzept der Abhängigkeitseinspritzung) sowie formale Architekturen wie Model-View-Controller (MVC) und Model-View-ViewModel (MVVM) nutzen können Komponententest aus dem einfachen Grund, dass eine modulare Architektur normalerweise einfacher zu testen ist. Diese Architekturen unterscheiden sich:

  • Die Präsentation (Ansicht).
  • Das Modell (verantwortlich für Persistenz und Datendarstellung).
  • Der Controller (wo die Berechnungen stattfinden sollen).

Während einige Aspekte des Modells möglicherweise Kandidaten für Komponententests sind, werden die meisten Komponententests wahrscheinlich gegen Methoden im Controller- oder View-Modell geschrieben, in denen die Berechnungen für das Model oder die View implementiert werden.

Wartungsphase

Unit-Tests können auch dann von Vorteil sein, wenn Sie an der Wartung einer Anwendung beteiligt sind. Dies erfordert entweder das Hinzufügen neuer Funktionen zu einer vorhandenen Anwendung oder das einfache Beheben von Fehlern einer älteren Anwendung. Es gibt verschiedene Ansätze, die Sie für eine vorhandene Anwendung anwenden können, sowie Fragen, die diesen Ansätzen zugrunde liegen, um die Kosteneffizienz von Komponententests zu bestimmen:

  • Schreiben Sie Komponententests nur für neue Funktionen und Fehlerbehebungen? Ist der Feature- oder Bugfix etwas, das von Regressionstests profitieren kann, oder handelt es sich um ein einmaliges, isoliertes Problem, das während des Integrationstests leichter getestet wird?
  • Beginnen Sie mit dem Schreiben von Komponententests anhand vorhandener Funktionen? Wenn ja, wie priorisieren Sie die zu testenden Funktionen?
  • Funktioniert die vorhandene Codebasis gut mit Unit-Tests, oder muss der Code zuerst umgestaltet werden, um Code-Units zu isolieren??
  • Welche Setups oder Teardowns werden für die Funktions- oder Fehlerprüfung benötigt?
  • Welche Abhängigkeiten können über die Codeänderungen ermittelt werden, die zu Nebenwirkungen in anderem Code führen können, und sollten die Komponententests erweitert werden, um das Verhalten von abhängigem Code zu testen??

Die Wartungsphase einer älteren Anwendung, der keine Einheitentests fehlen, ist nicht trivial - Planung, Überlegung und Untersuchung des Codes erfordern oft mehr Ressourcen als das Beheben des Fehlers. Die vernünftige Verwendung von Komponententests kann jedoch kostengünstig sein, und auch wenn dies nicht immer einfach zu bestimmen ist, lohnt sich die Übung, wenn auch aus keinem anderen Grund, als ein tieferes Verständnis der Codebasis zu erlangen.


Bestimmen Sie Ihren Prozess

In Bezug auf den Unit-Test-Prozess können drei Strategien verfolgt werden: „Testgetriebene Entwicklung“, „Code First“ und, obwohl dies dem Thema dieses Buchs, dem „No Unit Test“ -Prozess, entgegensteht.

Testgetriebene Entwicklung

Ein Camp ist "Test-Driven Development", zusammengefasst im folgenden Workflow:

Schreiben Sie unter Berücksichtigung einer Berechnungsanforderung (siehe vorheriger Abschnitt) zunächst einen Stub für die Methode.

  • Wenn Abhängigkeiten von anderen, noch nicht implementierten Objekten erforderlich sind (Objekte, die als Parameter an die Methode übergeben werden oder von der Methode zurückgegeben werden), implementieren Sie diese als leere Schnittstellen.
  • Wenn Eigenschaften fehlen, implementieren Sie Stubs für Eigenschaften, die zur Überprüfung der Ergebnisse erforderlich sind.
  • Schreiben Sie die Anforderungen für den Setup- oder Abrißtest.
  • Schreiben Sie die Tests. Die Gründe für das Schreiben von Stubs Vor Schreiben des Tests sind: erstens, um IntelliSense beim Schreiben des Tests zu nutzen; zweitens, um festzustellen, dass der Code noch kompiliert wird; und drittens, um sicherzustellen, dass die getestete Methode, ihre Parameter, Schnittstellen und Eigenschaften hinsichtlich der Benennung synchronisiert sind.
  • Führen Sie die Tests durch und stellen Sie sicher, dass sie fehlschlagen.
  • Kodieren Sie die Implementierung.
  • Führen Sie die Tests durch und stellen Sie sicher, dass sie erfolgreich sind.

In der Praxis ist das schwieriger als es aussieht. Es ist leicht, dem Schreiben von Tests zu verfallen, die nicht kostengünstig sind, und oft stellt man fest, dass die getestete Methode nicht ausreichend feinkörnig ist, um tatsächlich ein guter Kandidat für einen Test zu sein. Möglicherweise macht die Methode zu viel, erfordert zu viel Setup oder Teardown oder hängt von zu vielen anderen Objekten ab, die alle in einen bekannten Zustand initialisiert werden müssen. Dies sind alles Dinge, die beim Schreiben des Codes leichter zu erkennen sind, nicht beim Test.

Ein Vorteil gegenüber einem testgetriebenen Ansatz besteht darin, dass der Prozess die Disziplin des Komponententests einleitet und die Komponententests zuerst schreibt. Es ist leicht festzustellen, ob der Entwickler den Prozess verfolgt. Mit Übung kann man leicht werden, den Prozess kostengünstiger zu gestalten.

Ein weiterer Vorteil eines testgetriebenen Ansatzes ist, dass er naturgemäß eine Art Architektur erzwingt. Es wäre absurd, aber machbar, einen Unit-Test zu schreiben, der ein Formular initialisiert, Werte in ein Steuerelement einfügt und dann eine Methode aufruft, von der erwartet wird, dass sie einige Berechnungen an den Werten durchführt, da dieser Code dies tatsächlich erfordern würde (hier gefunden):

private void btnCalculate_Click (Objektsender, System.EventArgs e) double Principal, AnnualRate, InterestEarned; double FutureValue, RatePerPeriod; int NumberOfPeriods, CompoundType; Principal = Double.Parse (txtPrincipal.Text); AnnualRate = Double.Parse (txtInterest.Text) / 100; if (rdoMonthly.Checked) CompoundType = 12; else if (rdoQuarterly.Checked) CompoundType = 4; else if (rdoSemiannually.Checked) CompoundType = 2; else CompoundType = 1; NumberOfPeriods = Int32.Parse (txtPeriods.Text); doppeltes i = AnnualRate / CompoundType; int n = CompoundType * NumberOfPeriods; RatePerPeriod = Jahresrate / Anzahl von Perioden; FutureValue = Principal * Math.Pow (1 + i, n); InterestEarned = FutureValue - Principal; txtInterestEarned.Text = InterestEarned.ToString ("C"); txtAmountEarned.Text = FutureValue.ToString ("C"); 

Der vorhergehende Code ist nicht testbar, da er mit dem Ereignishandler und der Benutzeroberfläche verwickelt ist. Vielmehr könnte man die Zinsberechnungsmethode schreiben:

public enum CompoundType jährlich = 1, halbjährlich = 2, vierteljährlich = 4, monatlich = 12 private double CompoundInterestCalculation (Doppelter Principal, Doppeljahressatz, CompoundType-CompoundType, Int-Perioden) DoppeljahresRateDecimal = Jahrespreis / 100.0; double i = annualRateDecimal / (int) compoundType; int n = (int) compoundType * Perioden; double ratePerPeriod = annualRateDecimal / perioden; double futureValue = Prinzipal * Math.Pow (1 + i, n); double interestEaned = futureValue - Principal; return interestEaned; 

das würde dann erlauben, einen einfachen Test zu schreiben:

[TestMethod] public void CompoundInterestTest () double interest = CompoundInterestCalculation (2500, 7.55, CompoundType.Monthly, 4); Assert.AreEqual (878.21, Interesse, 0,01); 

Durch die Verwendung parametrisierter Tests wäre es zudem einfach, jeden Verbindungstyp, einen Zeitraum von Jahren sowie unterschiedliche Zins- und Hauptbeträge zu testen.

Der testgetriebene Ansatz eigentlich erleichtert ein stärker formalisierter Entwicklungsprozess, indem tatsächlich überprüfbare Einheiten entdeckt und von grenzüberschreitenden Abhängigkeiten isoliert werden.

Code First, Test Second

Das erste Codieren ist natürlicher, nur weil dies die übliche Art ist, wie Anwendungen entwickelt werden. Die Anforderung und ihre Implementierung können auf den ersten Blick auch leicht genug erscheinen, so dass das Schreiben mehrerer Komponententests wie ein Zeitmangel erscheint. Andere Faktoren wie Fristen können dazu führen, dass ein Projekt in den Entwicklungsprozess "Nur Code schreiben, damit wir ihn versenden können" erzwungen wird.

Das Problem des Code-first-Ansatzes besteht darin, dass es einfach ist, Code zu schreiben, der die Art von Test erfordert, die wir zuvor gesehen haben. Code erfordert zunächst eine aktive Disziplin, um den Code zu testen, der geschrieben wurde. Diese Disziplin ist unglaublich schwer zu erreichen, zumal es immer die nächste neue Funktion gibt, die implementiert werden muss.

Es erfordert auch Intelligenz, wenn Sie so wollen, um das Schreiben von verschränkten Codes, grenzüberschreitenden Code und die entsprechende Disziplin zu vermeiden. Wer hat nicht auf eine Schaltfläche im Visual Studio-Designer geklickt und die Berechnung des Ereignisses direkt in dem von Visual Studio für Sie erstellten Stub codiert? Es ist einfach und weil das Tool Sie in diese Richtung lenkt, wird der naive Programmierer denken, dass dies die richtige Art der Codierung ist.

Dieser Ansatz erfordert eine sorgfältige Abwägung der Fähigkeiten und der Disziplin Ihres Teams und erfordert eine engere Überwachung des Teams, insbesondere in Zeiten starker Belastung, wenn disziplinierte Ansätze zum Zerfall neigen. Zugegebenermaßen kann eine testgetriebene Disziplin auch als Terminwettbewerb verworfen werden, was jedoch eher eine bewusste Entscheidung für eine Ausnahme ist, während sie bei einem Code-First-Ansatz leicht zur Regel werden kann.

Keine Unit-Tests

Nur weil Sie keine Komponententests haben, heißt das nicht, dass Sie Tests aussortieren. Es kann einfach sein, dass das Testen Abnahmeprüfverfahren oder Integrationstests betont.

Balancing-Teststrategien

Ein kostengünstiger Unit-Test-Prozess erfordert ein ausgewogenes Verhältnis zwischen testgetriebener Entwicklung, Code First, Test Second und "Test Some Other Way". Die Kosteneffizienz von Komponententests sollte ebenso berücksichtigt werden wie Faktoren wie die Erfahrung der Entwickler im Team. Als Manager möchten Sie möglicherweise nicht hören, dass ein testgetriebener Ansatz eine gute Idee ist, wenn Ihr Team ziemlich grün ist und Sie den Prozess benötigen, um Disziplin und Herangehensweise einzubringen.