Komponententest Was ist der Komponententest?

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.

Was ist eine Einheit??

Im Rahmen des Unit-Tests weist eine Unit mehrere Eigenschaften auf.

Reine Einheiten

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:

  • Anstatt Parameter zu übernehmen, erhält er die an der Berechnung beteiligten Werte von Objekten der Benutzeroberfläche, insbesondere einer DataGridView und einer ComboBox.
  • Es ruft mehrere Methoden auf, die möglicherweise aufgerufen werden sind Einheiten.
  • Eine der Methoden erscheint, um die Anzeige zu aktualisieren und eine Berechnung mit einer Visualisierung zu verknüpfen.

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 Einheit sollte nur eine Sache tun

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:

  • Eine erhalten SqlClient Factory-Provider-Instanz.
  • Verbindung instanziieren und öffnen.
  • Einen Befehl instanziieren und den Befehl initialisieren.
  • Erstellen und Hinzufügen von zwei Parametern zum Befehl.
  • Den Befehl ausführen und die Anzahl der betroffenen Zeilen zurückgeben.

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.

Nachweislich korrekter Code

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.

Eine Einheit sollte (idealerweise) nicht über mehrere Codepfade verfügen

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:

  • Erstellen Sie Setup-Komplexität, da für die Ausführung des inneren Codes viele Bedingungen erfüllt sein müssen.
  • Erstellen Sie Testkomplexität, da die Codepfade unterschiedliche Einstellungen erfordern, um sicherzustellen, dass jeder Codepfad getestet wird.

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.

Abhängige Einheiten

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.

Voraussetzungen

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".

Aktuelle oder simulierte Dienste

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.

Umgang mit externen Ausnahmen

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.


Was ist ein Test??

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:

  • Testen, wie sich das Gerät unter normalen Bedingungen verhält.
  • Testen, wie sich das Gerät unter anormalen Bedingungen verhält.

Test der Normalbedingungen

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.

Abnormale Bedingungen testen

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 und andere Testverfahren

Unit-Tests als Teil eines umfassenden Testansatzes

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 Testens

Wenn 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.

Abnahmetestverfahren

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.

Automatisiertes Testen der Benutzeroberfläche

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.

Usability- und User Experience-Tests

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.

Leistungs- und Lasttests

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.

  • Speicherbeschränkungen simulieren.
  • Ressourcenbeschränkungen simulieren.

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.