Komponententest prägnant Korrektheit prüfen

Dies ist ein Auszug aus dem Unit Testing Succinctly eBook von Marc Clifton, freundlicherweise von Syncfusion zur Verfügung gestellt.

Der Ausdruck "Korrektheit beweisen" wird normalerweise im Zusammenhang mit der Richtigkeit einer Berechnung verwendet. Im Hinblick auf die Prüfung von Einheiten hat der Korrektheitstest tatsächlich drei große Kategorien, von denen sich nur die zweite auf die Berechnungen selbst bezieht:

  • Sicherstellen, dass die Eingaben für eine Berechnung korrekt sind (Methodenvertrag).
  • Vergewissern Sie sich, dass ein Methodenaufruf zu dem gewünschten Berechnungsergebnis führt (als rechnerischer Aspekt bezeichnet), das in vier typische Prozesse unterteilt ist:
    • Datenumwandlung
    • Datenreduzierung
    • Zustandswechsel
    • Zustand der Richtigkeit
  • Externe Fehlerbehandlung und Wiederherstellung.

Es gibt viele Aspekte einer Anwendung, bei denen Unit-Tests zum Nachweis der Korrektheit normalerweise nicht angewendet werden können. Dazu gehören die meisten Funktionen der Benutzeroberfläche, z. B. Layout und Benutzerfreundlichkeit. In vielen Fällen ist das Testen von Einheiten nicht die geeignete Technologie, um Anforderungen und Anwendungsverhalten in Bezug auf Leistung, Last usw. zu testen.


Wie Unit-Tests die Richtigkeit belegen

Zum Nachweis der Korrektheit gehört Folgendes:

  • Überprüfung des Vertrags.
  • Überprüfung der Rechenergebnisse.
  • Überprüfen der Datentransformationsergebnisse.
  • Überprüfen, ob externe Fehler korrekt behandelt werden.

Sehen wir uns einige Beispiele für jede dieser Kategorien an, ihre Stärken, Schwächen und Probleme, auf die wir mit unserem Code stoßen könnten.

Beweisvertrag ist umgesetzt

Die grundlegendste Form der Komponententests besteht darin, zu überprüfen, ob der Entwickler eine Methode geschrieben hat, die den „Vertrag“ zwischen dem Aufrufer und der aufgerufenen Methode eindeutig angibt. Dies erfolgt normalerweise in der Form der Überprüfung, dass fehlerhafte Eingaben zu einer Methode dazu führen, dass eine Ausnahme ausgelöst wird. Zum Beispiel könnte eine "divide by" -Methode eine ArgumentOutOfRangeException wenn der Nenner 0 ist:

public static int Divide (int-Zähler, int-Nenner) if (Nenner == 0) Neue ArgumentOutOfRangeException werfen ("Nenner kann nicht 0 sein");  Rückgabe Zähler / Nenner;  [TestMethod] [ExpectedException (typeof (ArgumentOutOfRangeException))] public void BadParameterTest () Divide (5, 0); 

Die Überprüfung, dass eine Methode Vertragstests implementiert, ist jedoch einer der schwächsten Unit-Tests, die geschrieben werden können.

Rechenergebnisse belegen

Bei einem stärkeren Unit-Test muss überprüft werden, ob die Berechnung korrekt ist. Es ist nützlich, Ihre Methoden in eine der drei Berechnungsarten einzuordnen:

  • Datenreduzierung
  • Datenumwandlung
  • Zustandswechsel

Diese bestimmen die Arten von Komponententests, die Sie möglicherweise für eine bestimmte Methode schreiben möchten.

Datenreduzierung

Das Teilen Die Methode des vorherigen Beispiels kann als eine Form der Datenreduktion betrachtet werden. Es nimmt zwei Werte und gibt einen Wert zurück. Um zu zeigen:

[TestMethod] public void VerifyDivisionTest () Assert.IsTrue (Divide (6, 2) == 3, "6/2 sollte 3 sein!"); 

Dies ist ein Beispiel für das Testen einer Methode, bei der die Eingaben normalerweise auf eine resultierende Ausgabe reduziert werden. Dies ist die einfachste Form nützlicher Unit-Tests.

Datenumwandlung

Datentransformations-Einheitstests tendieren dazu, mit Wertemengen zu arbeiten. Das Folgende ist beispielsweise ein Test für eine Methode, mit der kartesische Koordinaten in Polarkoordinaten umgewandelt werden.

public static double [] ConvertToPolarCoordinates (doppeltes x, doppeltes y) double dist = Math.Sqrt (x * x + y * y); Doppelwinkel = Math.Atan2 (y, x); return new double [] dist, angle;  [TestMethod] public void ConvertToPolarCoordinatesTest () double [] pcoord = ConvertToPolarCoordinates (3, 4); Assert.IsTrue (pcoord [0] == 5, "Erwarteter Abstand gleich 5"); Assert.IsTrue (Pcoord [1] == 0,92729521800161219, "Erwarteter Winkel ist 53,130 Grad"); 

Dieser Test überprüft die Richtigkeit der mathematischen Transformation.

Listentransformationen

Listentransformationen sollten in zwei Tests unterteilt werden:

  • Stellen Sie sicher, dass die Kernumwandlung korrekt ist.
  • Stellen Sie sicher, dass der Listenvorgang korrekt ist.

Aus Sicht des Komponententests ist das folgende Beispiel beispielsweise schlecht geschrieben, da es sowohl die Datenreduktion als auch die Datentransformation beinhaltet:

public struct Name öffentlicher String Vorname get; einstellen;  public string LastName get; einstellen;  öffentliche Liste ConcatNames (Liste Namen) Liste concatenatedNames = neue Liste(); foreach (Name Name in Namen) concatenatedNames.Add (name.LastName + "," + name.FirstName);  return concatenatedNames;  [TestMethod] public void NameConcatenationTest () List Namen = neue Liste() neuer Name () Vorname = "John", Nachname = "Travolta", neuer Name () Vorname = "Allen", Nachname = "Nancy"; Liste newNames = ConcatNames (Namen); Assert.IsTrue (newNames [0] == "Travolta, John"); Assert.IsTrue (newNames [1] == "Nancy, Allen"); 

Dieser Code wird besser durch Einheiten getestet, indem die Datenreduktion von der Datentransformation getrennt wird:

öffentliche Zeichenfolge Concat (Name Name) Rückkehrname.LastName + "," + Name.FirstName;  [TestMethod] public void ContactNameTest () Name Name = Neuer Name () Vorname = "John", LastName = "Travolta"; Zeichenfolge concatenatedName = Concat (Name); Assert.IsTrue (verketteterName = "Travolta, John"); 

Lambda-Ausdrücke und Unit-Tests

Die LINQ-Syntax (Language-Integrated Query) ist eng mit Lambda-Ausdrücken gekoppelt, was zu einer leicht lesbaren Syntax führt, die das Testen von Einheiten schwierig macht. Zum Beispiel dieser Code:

öffentliche Liste ConcatNamesWithLinq (Liste names) return names.elect (t => t.LastName + "," + t.FirstName) .ToList (); 

ist wesentlich eleganter als die vorherigen Beispiele, aber es eignet sich nicht gut, um die tatsächliche "Einheit" zu testen, d. h. die Datenreduktion von einer Namensstruktur zu einer einzelnen durch Kommas getrennten Zeichenfolge, ausgedrückt in der Lambda-Funktion t => t.LastName + "," + t.FirstName. Um das Gerät von der Liste zu trennen, müssen Sie Folgendes tun:

öffentliche Liste ConcatNamesWithLinq (Liste names) return names.Select (t => Concat (t)). ToList (); 

Wir sehen, dass Unit-Tests oft ein Refactoring des Codes erfordern, um die Units von anderen Transformationen zu trennen.

Zustandsänderung

Die meisten Sprachen sind "stateful" und Klassen verwalten oft den Status. Der Zustand einer Klasse, dargestellt durch ihre Eigenschaften, ist häufig eine nützliche Sache zum Testen. Betrachten Sie diese Klasse als das Konzept einer Verbindung:

public class AlreadyConnectedToServiceException: ApplicationException public AlreadyConnectedToServiceException (Zeichenfolge msg): base (msg)  öffentliche Klasse ServiceConnection public bool Connected get; geschützter Satz;  public void Connect () if (Connected) Neue AlreadyConnectedToServiceException werfen ("Es ist jeweils nur eine Verbindung zulässig.");  // Verbinden Sie sich mit dem Dienst. Connected = true;  public void Disconnect () // Trennen Sie die Verbindung zum Dienst. Verbunden = falsch; 

Wir können Unit-Tests schreiben, um die verschiedenen zulässigen und nicht zulässigen Zustände des Objekts zu überprüfen:

[TestClass] öffentliche Klasse ServiceConnectionFixture [TestMethod] public void TestInitialState () ServiceConnection conn = neue ServiceConnection (); Assert.IsFalse (conn.Connected);  [TestMethod] public void TestConnectedState () ServiceConnection conn = neue ServiceConnection (); conn.Connect (); Assert.IsTrue (conn.Connected);  [TestMethod] public void TestDisconnectedState () ServiceConnection conn = neue ServiceConnection (); conn.Connect (); conn.Disconnect (); Assert.IsFalse (conn.Connected);  [TestMethod] [ExpectedException (typeof (AlreadyConnectedToServiceException))] public void TestAlreadyConnectedException () ServiceConnection conn = new ServiceConnection (); conn.Connect (); conn.Connect (); 

Hier wird bei jedem Test die Richtigkeit des Objektstatus überprüft:

  • Wenn es initialisiert wird.
  • Wenn Sie aufgefordert werden, eine Verbindung zum Dienst herzustellen.
  • Wenn Sie aufgefordert werden, die Verbindung zum Dienst zu trennen.
  • Wenn mehr als eine gleichzeitige Verbindung versucht wird.

Die staatliche Verifizierung deckt häufig Fehler in der Staatsverwaltung auf. Weitere Verbesserungen des vorherigen Beispielcodes finden Sie auch in den folgenden „Scheinklassen“.

Beweisen Sie, dass eine Methode eine externe Ausnahme richtig verarbeitet

Externe Fehlerbehandlung und -wiederherstellung ist oft wichtiger als das Testen, ob der eigene Code zu den richtigen Zeitpunkten Ausnahmen generiert. Dafür gibt es mehrere Gründe:

  • Sie haben keine Kontrolle über eine physisch getrennte Abhängigkeit, ob es sich um einen Webdienst, eine Datenbank oder einen anderen separaten Server handelt.
  • Sie haben keinen Beweis für die Richtigkeit des Codes einer anderen Person, normalerweise einer Drittanbieter-Bibliothek.
  • Dienste und Software von Drittanbietern können eine Ausnahme auslösen, weil Ihr Code zwar erstellt, aber nicht erkannt wird (und nicht unbedingt leicht zu erkennen ist). Ein Beispiel hierfür ist, dass beim Löschen von Datensätzen in einer Datenbank die Datenbank eine Ausnahme auslöst, weil Datensätze in anderen Tabellen auf die Datensätze verweisen, die Ihr Programm löscht, wodurch eine Fremdschlüsseleinschränkung verletzt wird.

Diese Art von Ausnahmen ist schwierig zu testen, da sie mindestens einen Fehler erzeugen müssen, der normalerweise von dem Dienst generiert wird, den Sie nicht steuern. Eine Möglichkeit, dies zu tun, besteht darin, den Dienst zu "verspotten"; Dies ist jedoch nur möglich, wenn das externe Objekt mit einer Schnittstelle, einer abstrakten Klasse oder virtuellen Methoden implementiert ist.

Spottende Klassen

Beispielsweise ist der frühere Code für die Klasse „ServiceConnection“ nicht spottbar. Wenn Sie die Statusverwaltung testen möchten, müssen Sie physisch eine Verbindung zum Dienst herstellen (was auch immer es ist), die beim Ausführen der Komponententests verfügbar ist oder nicht. Eine bessere Implementierung könnte so aussehen:

öffentliche Klasse MockableServiceConnection public bool Connected get; geschützter Satz;  protected virtual void ConnectToService () // Stellen Sie eine Verbindung zum Dienst her.  protected virtual void DisconnectFromService () // Trennen Sie die Verbindung zum Dienst.  public void Connect () if (Connected) Neue AlreadyConnectedToServiceException werfen ("Es ist jeweils nur eine Verbindung zulässig.");  ConnectToService (); Connected = true;  public void Disconnect () DisconnectFromService (); Verbunden = falsch; 

Beachten Sie, wie Sie mit diesem kleinen Refactoring jetzt eine Mock-Klasse schreiben können:

Öffentliche Klasse ServiceConnectionMock: MockableServiceConnection protected überschreibt void ConnectToService () // Keine Aktion.  protected überschreibt void DisconnectFromService () // Nichts tun. 

Damit können Sie einen Komponententest schreiben, der die Statusverwaltung unabhängig von der Verfügbarkeit des Dienstes testet. Wie dies veranschaulicht, können selbst einfache Änderungen der Architektur oder Implementierung die Testbarkeit einer Klasse erheblich verbessern.

Beweisen Sie, dass ein Fehler erneut erstellt werden kann

Ihre erste Verteidigungslinie beim Nachweis, dass das Problem behoben wurde, ist ironischerweise der Beweis, dass das Problem besteht. Vorhin haben wir ein Beispiel für das Schreiben eines Tests gesehen, der bewies, dass die Divide-Methode den Nennerwert von überprüft 0. Angenommen, ein Fehlerbericht wird abgelegt, weil ein Benutzer das Programm bei der Eingabe abgestürzt hat 0 für den Nennerwert.

Negativprüfung

Die erste Aufgabe besteht darin, einen Test zu erstellen, der diese Bedingung erfüllt:

[TestMethod] [ExpectedException (typeof (DivideByZeroException))] public void BadParameterTest () Divide (5, 0); 

Dieser Test geht vorbei weil wir beweisen, dass der Fehler existiert, indem wir überprüfen, wann der Nenner ist 0, ein DivideByZeroException wird angehoben. Diese Art von Tests werden als "negative Tests" betrachtet bestehen wenn ein Fehler auftritt Negative Tests sind ebenso wichtig wie positive Tests (im Folgenden beschrieben), da sie das Vorhandensein eines Problems überprüfen, bevor es behoben wird.

Beweisen Sie, dass ein Fehler behoben ist

Natürlich möchten wir beweisen, dass ein Fehler behoben wurde. Dies ist ein "positiver" Test.

Positives Testen

Wir können jetzt einen neuen Test einführen, einen Test, bei dem getestet wird, dass der Code den Fehler selbst erkennt, indem er einen Fehler auslöst ArgumentOutOfRangeException.

[TestMethod] [ExpectedException (typeof (ArgumentOutOfRangeException))] public void BadParameterTest () Divide (5, 0); 

Wenn wir diesen Test schreiben können Vor Bei der Behebung des Problems werden wir feststellen, dass der Test fehlschlägt. Nach der Behebung des Problems besteht unser positiver Test schließlich, und der negative Test schlägt fehl.

Dies ist zwar ein triviales Beispiel, zeigt aber zwei Konzepte:

  • Negative Tests, die belegen, dass etwas wiederholt nicht funktioniert, sind für das Verständnis des Problems und der Lösung wichtig.
  • Positive Tests, die belegen, dass das Problem behoben wurde, sind nicht nur für die Überprüfung der Lösung wichtig, sondern auch für die Wiederholung des Tests, wenn Änderungen vorgenommen werden. Unit-Tests spielen eine wichtige Rolle beim Regressionstest.

Schließlich ist es nicht immer einfach, das Vorhandensein eines Fehlers zu beweisen. Als Faustregel gilt jedoch, dass Unit-Tests, die zu viel Setup und Verspottung erfordern, ein Indikator dafür sind, dass der getestete Code nicht ausreichend von externen Abhängigkeiten isoliert ist und ein Kandidat für das Refactoring ist.

Beweisen Sie, dass beim Ändern von Code nichts kaputt ist

Es sollte offensichtlich sein, dass Regressionstests ein messbar nützliches Ergebnis von Unit-Tests sind. Wenn der Code geändert wird, werden Fehler eingeführt, die aufgedeckt werden, wenn in Ihren Gerätetests eine gute Codeabdeckung vorliegt. Dies spart effektiv viel Zeit beim Debuggen und, was noch wichtiger ist, Zeit und Geld, wenn der Programmierer den Fehler und nicht den Benutzer entdeckt.

Beweise, dass die Anforderungen erfüllt sind

Die Anwendungsentwicklung beginnt in der Regel mit einem übergeordneten Anforderungssatz, der sich in der Regel an der Benutzeroberfläche, dem Arbeitsablauf und den Berechnungen orientiert. Im Idealfall reduziert das Team die sichtbar Anforderungen bis hin zu programmatischen Anforderungen unsichtbar für den Benutzer von Natur aus.

Der Unterschied zeigt sich darin, wie das Programm getestet wird. Integrationstests finden normalerweise am statt sichtbar Ebene, während die Prüfung von Einheiten die Feinheiten von unsichtbar, programmatische Korrektheitsprüfung. Es ist wichtig zu wissen, dass Unit-Tests den Integrationstest nicht ersetzen sollen. Wie bei den übergeordneten Anwendungsanforderungen können jedoch auch programmatische Anforderungen auf niedriger Ebene definiert werden. Aufgrund dieser programmatischen Anforderungen ist es wichtig, Komponententests zu schreiben.

Nehmen wir eine Rundenmethode. Die .NET Math.Round-Methode rundet eine Zahl auf, deren Bruchkomponente größer als 0,5 ist, wird jedoch abgerundet, wenn die Bruchkomponente 0,5 oder weniger beträgt. Nehmen wir an, das ist nicht das Verhalten, das wir uns wünschen (aus welchem ​​Grund auch immer), und wir möchten abrunden, wenn die gebrochene Komponente 0,5 oder mehr beträgt. Dies ist eine rechnerische Anforderung, die aus einer übergeordneten Integrationsanforderung abgeleitet werden kann, was zu der folgenden Methode und dem folgenden Test führt:

public static int RoundUpHalf (double n) wenn (n < 0) throw new ArgumentOutOfRangeException("Value must be >= 0. "); int ret = (int) n; doppelter Bruch = n - ret; if (Bruch> = 0,5) ++ ret; ret ret; [TestMethod] public void RoundUpTest () int result1 = RoundUpHalf (1.5); int result2 = RoundUpHalf (1.499999); Assert.IsTrue (result1 == 2, "Expected 2."); Assert.IsTrue (result2 == 1, "Expected 1.");

Ein separater Test für die Ausnahme sollte ebenfalls geschrieben werden.

Die Überprüfung von Anforderungen auf Anwendungsebene, die durch Integrationstests verifiziert werden, und die Reduzierung auf niedrigere Rechenanforderungen, ist ein wichtiger Teil der Gesamtteststrategie für Unit-Tests, da klare Rechenanforderungen definiert werden, die die Anwendung erfüllen muss. Wenn bei diesem Prozess Schwierigkeiten auftreten, versuchen Sie, die Anwendungsanforderungen in eine der drei Berechnungskategorien umzuwandeln: Datenreduzierung, Datentransformation und Statusänderung.