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:
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.
Zum Nachweis der Korrektheit gehört Folgendes:
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.
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.
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:
Diese bestimmen die Arten von Komponententests, die Sie möglicherweise für eine bestimmte Methode schreiben möchten.
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.
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 sollten in zwei Tests unterteilt werden:
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 ListeConcatNames (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");
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 ListeConcatNamesWithLinq (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 ListeConcatNamesWithLinq (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.
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:
Die staatliche Verifizierung deckt häufig Fehler in der Staatsverwaltung auf. Weitere Verbesserungen des vorherigen Beispielcodes finden Sie auch in den folgenden „Scheinklassen“.
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:
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.
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.
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.
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.
Natürlich möchten wir beweisen, dass ein Fehler behoben wurde. Dies ist ein "positiver" Test.
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:
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.
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.
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.