Dies ist ein Auszug aus dem Unit Testing Succinctly eBook von Marc Clifton, freundlicherweise von Syncfusion zur Verfügung gestellt.
Bei Unit-Tests geht es um den Nachweis der Korrektheit. Um zu beweisen, dass etwas richtig funktioniert, müssen Sie zuerst verstehen, was sowohl ein Einheit und ein Prüfung Bevor Sie herausfinden können, was innerhalb der Fähigkeiten des Komponententests nachweisbar ist.
Im Rahmen des Unit-Tests weist eine Unit mehrere Eigenschaften auf.
Eine reine Einheit ist die einfachste und idealste Methode zum Schreiben eines Komponententests. Eine reine Einheit verfügt über mehrere Eigenschaften, die das Testen erleichtern.
Eine Einheit sollte (idealerweise) keine anderen Methoden aufrufen
In Bezug auf Unit-Tests sollte eine Unit in erster Linie eine Methode sein, die etwas tut, ohne andere Methoden aufzurufen. Beispiele für diese reinen Einheiten finden Sie im String
und Mathematik
Die meisten der durchgeführten Klassen sind von keiner anderen Methode abhängig. Zum Beispiel der folgende Code (entnommen aus etwas, das der Autor geschrieben hat)
public void SelectedMasters () string currentEntity = dgvModel.DataMember; String navToEntity = cbMasterTables.SelectedItem.ToString (); DataGridViewSelectedRowCollection selectedRows = dgvModel.SelectedRows; StringBuilder qualifier = BuildQualifier (selectedRows); UpdateGrid (navToEntity); SetRowFilter (navToEntity, qualifier.ToString ()); ShowNavigateToMaster (navToEntity, qualifier.ToString ());
sollte aus drei Gründen nicht als Einheit betrachtet werden:
Der erste Grund weist darauf hin, dass subtile Problem-Eigenschaften als Methodenaufrufe betrachtet werden sollten. Sie befinden sich tatsächlich in der zugrunde liegenden Implementierung. Wenn Ihre Methode Eigenschaften anderer Klassen verwendet, handelt es sich hierbei um eine Art Methodenaufruf, der beim Schreiben einer geeigneten Einheit sorgfältig berücksichtigt werden sollte.
Dies ist realistisch nicht immer möglich. Oft genug ist ein Aufruf des Frameworks oder einer anderen API erforderlich, damit das Gerät seine Arbeit erfolgreich ausführen kann. Diese Aufrufe sollten jedoch untersucht werden, um zu bestimmen, ob die Methode verbessert werden könnte, um eine reinere Einheit zu erstellen, indem zum Beispiel die Aufrufe in eine höhere Methode extrahiert werden und die Ergebnisse der Aufrufe als Parameter an die Einheit übergeben werden.
Eine Folgerung zu "Eine Einheit sollte keine anderen Methoden aufrufen" ist, dass eine Einheit eine Methode ist macht eine Sache und nur eine Sache. Oft werden dazu andere Methoden aufgerufen mehr als eine Sache-Eine wertvolle Fähigkeit, zu wissen, wann etwas tatsächlich aus mehreren Teilaufgaben besteht - selbst wenn es sich um eine übergeordnete Aufgabe handelt, die wie eine einzelne Aufgabe klingt!
Der folgende Code könnte wie eine sinnvolle Einheit aussehen, die eines tut: Sie fügt einen Namen in die Datenbank ein.
public int Einfügen (Person Person) DbProviderFactory factory = SqlClientFactory.Instance; using (DbConnection connection = factory.CreateConnection ()) connection.ConnectionString = "Server = localhost; Database = myDataBase; Trusted_Connection = True;"; Verbindung.Öffnen (); using (DbCommand command = Verbindung.CreateCommand ()) command.CommandText = "Werte in PERSON (ID, NAME) einfügen (@Id, @Name)"; command.CommandType = CommandType.Text; DbParameter id = command.CreateParameter (); id.ParameterName = "@Id"; id.DbType = DbType.Int32; id.Value = person.Id; DbParameter name = command.CreateParameter (); name.ParameterName = "@Name"; name.DbType = DbType.String; name.Size = 50; name.Value = person.Name; command.Parameters.AddRange (neuer DbParameter [] id, name); int rowsAffected = command.ExecuteNonQuery (); return rowsAffected;
Dieser Code führt jedoch mehrere Aufgaben aus:
SqlClient
Factory-Provider-Instanz.Es gibt eine Vielzahl von Problemen mit diesem Code, die es ausschließen, eine Einheit zu sein und es schwierig machen, sie in Basiseinheiten zu reduzieren. Eine bessere Möglichkeit, diesen Code zu schreiben, könnte folgendermaßen aussehen:
public int RefactoredInsert (person person) DbProviderFactory factory = SqlClientFactory.Instance; using (DbConnection conn = OpenConnection (Factory, "Server = localhost; Database = myDataBase; Trusted_Connection = True;";))) using (DbCommand cmd = CreateTextCommand (conn, ") in PERSON (ID, NAME) -Werte (@Id, @) Name) ")) AddParameter (cmd," @Id ", person.Id); AddParameter (cmd, "@Name", 50, person.Name); int rowsAffected = cmd.ExecuteNonQuery (); return rowsAffected; protected DbConnection OpenConnection (DbProviderFactory-Factory, Zeichenfolge connectString) DbConnection conn = factory.CreateConnection (); conn.ConnectionString = connectString; conn.Open (); zurückkehren conn; protected DbCommand CreateTextCommand (DbConnection-Verbindung, Zeichenfolge cmdText) DbCommand cmd = conn.CreateCommand (); cmd.CommandText = cmdText; cmd.CommandType = CommandType.Text; return cmd; protected void AddParameter (DbCommand cmd, Zeichenfolge paramName, int paramValue) DbParameter param = cmd.CreateParameter (); param.ParameterName = paramName; param.DbType = DbType.Int32; param.Value = paramValue; cmd.Parameters.Add (param); protected void AddParameter (DbCommand-Cmd, Zeichenfolge paramName, int-Größe, Zeichenfolge paramValue) DbParameter param = cmd.CreateParameter (); param.ParameterName = paramName; param.DbType = DbType.String; param.Size = Größe; param.Value = paramValue; cmd.Parameters.Add (param);
Beachten Sie, wie die Methoden nicht nur sauberer aussehen OpenConnection
, CreateTextCommand
, und AddParameter
sind eher für Unit-Tests geeignet (ignorieren die Tatsache, dass es sich um geschützte Methoden handelt). Diese Methoden machen nur eine Sache und können als Einheiten getestet werden, um sicherzustellen, dass sie diese eine Sache richtig machen. Aus diesem Grund wird es wenig Sinn, das zu testen RefactoredInsert
Methode, da sie vollständig auf anderen Funktionen basiert, die Unit-Tests haben. Am besten möchten Sie vielleicht einige Ausnahmebehandlungstestfälle und möglicherweise eine Überprüfung der Felder in der Person
Tabelle.
Was ist, wenn die übergeordnete Methode mehr als nur andere Methoden aufruft, für die Unit-Tests vorhanden sind, beispielsweise eine Art zusätzlicher Berechnung? In diesem Fall sollte der Code, der die Berechnung durchführt, in eine eigene Methode verschoben werden, Tests sollten dafür geschrieben werden, und die übergeordnete Methode kann sich wiederum auf die Richtigkeit des aufgerufenen Codes verlassen. Dies ist der Prozess, bei dem nachweislich korrekter Code erstellt wird. Die Korrektheit von Methoden auf höherer Ebene verbessert sich, wenn sie lediglich Methoden auf niedrigerer Ebene aufrufen, die Beweise (Komponententests) der Korrektheit enthalten.
Die zyklomatische Komplexität ist im Allgemeinen die Flucht von Komponententests und Anwendungstests, da die Prüfung aller Codepfade schwieriger wird. Im Idealfall hat eine Einheit keine ob
oder Schalter
Aussagen. Der Körper dieser Aussagen sollte als die Einheiten betrachtet werden (vorausgesetzt, dass sie die anderen Kriterien einer Einheit erfüllen) und um prüfbar gemacht zu werden, sollten sie in ihre eigenen Methoden extrahiert werden.
Hier ist ein weiteres Beispiel aus dem MyXaml-Projekt des Autors (Teil des Parsers):
if (tagName == "*") foreach (XmlNode-Knoten in topElement.ChildNodes) if (! (Knoten ist XmlComment)) objectNode = node; brechen; foreach (XmlAttribute attr in objectNode.Attributes) if (attr.LocalName == "Name") nameAttr = attr; brechen; else … etc…
Hier haben wir mehrere Codepfade, an denen beteiligt ist ob
, sonst
, und für jeden
Aussagen, die:
Natürlich können bedingte Verzweigungen, Schleifen, Case-Anweisungen usw. nicht vermieden werden. Es kann jedoch sinnvoll sein, den Code zu überarbeiten, sodass die internen Komponenten der Bedingungen und Schleifen separate Methoden sind, die unabhängig getestet werden können. Dann können die Tests für die übergeordnete Methode einfach sicherstellen, dass die Zustände (dargestellt durch Bedingungen, Schleifen, Schalter usw.) unabhängig von den von ihnen durchgeführten Berechnungen ordnungsgemäß behandelt werden.
Methoden, die Abhängigkeiten von anderen Klassen, Daten und Statusinformationen aufweisen, sind komplexer zu testen, da diese Abhängigkeiten in Anforderungen für instanziierte Objekte, das Vorhandensein von Daten und einen vorbestimmten Zustand umgesetzt werden.
In ihrer einfachsten Form haben abhängige Einheiten Vorbedingungen, die erfüllt sein müssen. Unit-Test-Engines bieten Mechanismen zum Instanziieren von Testabhängigkeiten, sowohl für einzelne Tests als auch für alle Tests innerhalb einer Testgruppe oder "Vorrichtung".
Komplizierte abhängige Einheiten erfordern, dass Dienste wie Datenbankverbindungen instanziiert oder simuliert werden. In dem vorherigen Codebeispiel ist das Einfügen
Die Methode kann nicht als Einheit getestet werden, ohne dass eine Verbindung zu einer tatsächlichen Datenbank hergestellt werden kann. Dieser Code kann besser getestet werden, wenn die Datenbankinteraktion simuliert werden kann, normalerweise durch Verwendung von Schnittstellen oder Basisklassen (abstrakt oder nicht)..
Die überarbeiteten Methoden in der Einfügen
Code, der zuvor beschrieben wurde, ist ein gutes Beispiel dafür DbProviderFactory
ist eine abstrakte Basisklasse, daher kann man leicht eine Klasse erstellen, von der abgeleitet wird DbProviderFactory
um die Datenbankverbindung zu simulieren.
Abhängige Einheiten sind, da sie Aufrufe an andere APIs oder Methoden ausführen, auch anfälliger. Sie müssen möglicherweise explizit Fehler behandeln, die möglicherweise von den aufgerufenen Methoden generiert werden. In dem früheren Codebeispiel ist das Einfügen
Der Code der Methode könnte in einen try-catch-Block eingeschlossen werden, da die Datenbankverbindung möglicherweise nicht vorhanden ist. Der Ausnahmehandler wird möglicherweise zurückgegeben 0
Für die Anzahl der betroffenen Zeilen wird der Fehler durch einen anderen Mechanismus gemeldet. In einem solchen Szenario müssen die Komponententests diese Ausnahme simulieren können, um sicherzustellen, dass alle Codepfade ordnungsgemäß ausgeführt werden, einschließlich Fang
und endlich
Blöcke.
Ein Test liefert eine nützliche Aussage über die Richtigkeit der Einheit. Bei Tests, die die Richtigkeit einer Einheit bestätigen, wird die Einheit normalerweise auf zwei Arten trainiert:
Das Testen des Verhaltens des Geräts unter normalen Bedingungen ist bei weitem der einfachste Test. Wenn wir eine Funktion schreiben, schreiben wir sie entweder, um eine explizite oder implizite Anforderung zu erfüllen. Die Implementierung spiegelt ein Verständnis dieser Anforderung wider, die zum Teil umfasst, was wir als Eingaben für die Funktion erwarten und wie wir davon ausgehen, dass sich die Funktion mit diesen Eingaben verhält. Daher testen wir das Ergebnis der Funktion bei erwarteten Eingaben, ob das Ergebnis der Funktion ein Rückgabewert oder eine Zustandsänderung ist. Wenn das Gerät von anderen Funktionen oder Diensten abhängig ist, erwarten wir außerdem, dass sich diese korrekt verhalten und einen Test mit dieser implizierten Annahme schreiben.
Das Testen, wie sich das Gerät unter anormalen Bedingungen verhält, ist viel schwieriger. Es muss festgestellt werden, was ein anormaler Zustand ist, was normalerweise nicht offensichtlich ist, wenn der Code überprüft wird. Dies wird komplizierter, wenn eine abhängige Einheit getestet wird - eine Einheit, die erwartet, dass sich eine andere Funktion oder ein anderer Dienst korrekt verhält. Außerdem wissen wir nicht, wie ein anderer Programmierer oder Benutzer das Gerät trainieren könnte.
Unit-Tests ersetzen andere Testpraktiken nicht; Es sollte andere Testverfahren ergänzen und zusätzliche Dokumentation und Vertrauen bieten. Abbildung 1 zeigt ein Konzept des "Anwendungsentwicklungsablaufs", wie andere Tests in Unit-Tests integriert sind. Beachten Sie, dass der Kunde in jede Phase involviert sein kann, normalerweise jedoch in das Akzeptanztestverfahren (ATP), die Systemintegration und die Verwendbarkeit.
Vergleichen Sie dies mit dem V-Modell des Softwareentwicklungs- und Testprozesses. Während es sich auf das Wasserfallmodell der Softwareentwicklung bezieht (wobei letztendlich alle anderen Softwareentwicklungsmodelle entweder eine Untermenge oder eine Erweiterung davon sind), liefert das V-Modell ein gutes Bild davon, welche Art von Prüfung für jede Schicht erforderlich ist der Software-Entwicklungsprozess:
Das V-Modell des TestensWenn ein Testpunkt in einer anderen Testpraxis fehlschlägt, kann darüber hinaus normalerweise ein bestimmter Code identifiziert werden, der für den Fehler verantwortlich ist. Wenn dies der Fall ist, wird es möglich, diesen Code als eine Einheit zu behandeln und einen Komponententest zu schreiben, um zuerst den Fehler zu erstellen und, wenn der Code geändert wurde, den Fix zu überprüfen.
Ein Akzeptanztestverfahren (ATP) wird häufig als vertragliche Anforderung zum Nachweis der Implementierung bestimmter Funktionen verwendet. ATPs sind oft mit Meilensteinen verbunden, und Meilensteine sind oft mit Zahlungen oder weiterer Projektfinanzierung verbunden. Ein ATP unterscheidet sich von einem Komponententest, weil das ATP zeigt, dass die Funktionalität in Bezug auf die gesamte Einzelpostenanforderung implementiert wurde. Zum Beispiel kann ein Komponententest feststellen, ob die Berechnung korrekt ist. Die ATP kann jedoch überprüfen, dass die Benutzerelemente in der Benutzeroberfläche bereitgestellt werden und dass die Benutzeroberfläche das Ergebnis der Berechnung entsprechend der Anforderung anzeigt. Diese Anforderungen werden durch den Gerätetest nicht abgedeckt.
Ein ATP kann anfänglich als eine Reihe von Benutzeroberflächeninteraktionen (UI) geschrieben werden, um zu überprüfen, ob die Anforderungen erfüllt wurden. Regressionstests der Anwendung, die sich ständig weiterentwickelt, sind sowohl für Unit-Tests als auch für Abnahme-Tests anwendbar. Automatisierte Benutzeroberflächen-Tests sind ein weiteres Tool, das völlig unabhängig von Unit-Tests ist. Das spart Zeit und Arbeitskraft und verringert gleichzeitig Fehler beim Testen. Wie bei ATPs ersetzen Unit-Tests keinesfalls den Wert automatisierter Benutzeroberflächentests.
Unit-Tests, ATPs und automatisierte UI-Tests ersetzen in keiner Weise Usability-Tests. Sie stellen die Anwendung vor den Benutzern und erhalten ihr Feedback zur Benutzererfahrung. Bei Usability-Tests sollte es sich nicht um das Auffinden von Berechnungsfehlern (Bugs) handeln, und daher liegt dies völlig außerhalb des Bereichs von Unit-Tests.
Einige Gerätetestwerkzeuge bieten eine Möglichkeit, die Leistung einer Methode zu messen. Die Test-Engine von Visual Studio berichtet beispielsweise über die Ausführungszeit und NUnit verfügt über Attribute, mit denen überprüft werden kann, ob eine Methode innerhalb einer festgelegten Zeit ausgeführt wird.
Idealerweise sollte ein Komponententest-Tool für .NET-Sprachen explizit Leistungstests implementieren, um die Kompilierung von Just-In-Time-Code (JIT) bei der ersten Ausführung des Codes zu kompensieren.
Die meisten Belastungstests (und die zugehörigen Leistungstests) sind nicht für Komponententests geeignet. Bestimmte Formen von Belastungstests können auch mit Komponententests durchgeführt werden, zumindest mit den Einschränkungen der Hardware und des Betriebssystems, z.
Für diese Art von Tests ist jedoch im Idealfall die Unterstützung des Frameworks oder der Betriebssystem-API erforderlich, um diese Art von Lasten für die getestete Anwendung zu simulieren. Wenn das gesamte Betriebssystem dazu gezwungen wird, eine große Menge Speicher, Ressourcen oder beides zu verbrauchen, wirkt sich dies auf alle Anwendungen aus, einschließlich der Anwendung zum Komponententest. Dies ist kein wünschenswerter Ansatz.
Andere Arten von Belastungstests, z. B. das Simulieren mehrerer Instanzen einer gleichzeitigen Ausführung einer Operation, sind keine Kandidaten für den Komponententest. Zum Beispiel ist das Testen der Leistung eines Web-Services mit einer Last von einer Million Transaktionen pro Minute möglicherweise nicht mit einem einzigen Computer möglich. Während diese Art von Tests leicht als Einheit geschrieben werden kann, würde der eigentliche Test eine Reihe von Testmaschinen umfassen. Und am Ende haben Sie nur ein sehr enges Verhalten des Web-Service unter sehr spezifischen Netzwerkbedingungen getestet, die in keiner Weise die reale Welt repräsentieren.
Aus diesem Grund haben Leistungs- und Belastungstests die Anwendung von Komponententests eingeschränkt.