Testlauf

Testlauf – Durchführen von Anforderung/Antwort-Tests für Webanwendungen mithilfe von JavaScript

Dr. James McCaffrey

Beispielcode herunterladen.

Im Artikel dieses Monats wird erläutert, wie Sie eine einfache und effektive browserbasierte Anforderung/Antwort-Testautomatisierung in JavaScript schreiben. Um einen besseren Eindruck davon zu bekommen, worum es in diesem Artikel genau geht, können Sie sich die Screenshots in Abbildung 1 und 2 ansehen. Abbildung 1 zeigt eine einfache, aber repräsentative ASP.NET-Webanwendung namens Product Search, die getestet wird. Der Benutzer gibt eine Suchzeichenfolge in das einzige Testfeld-Steuerelement der Anwendung ein und gibt mithilfe von zwei Optionsfeldern an, ob die Groß-/Kleinschreibung bei der Suche berücksichtigt werden soll. Die Suchergebnisse werden in einem Listenfeld-Steuerelement angezeigt.

Abbildung 1 Die Webanwendung Product Search im Test

image: Product Search Web Application Under Test

Obwohl die zu testende Beispielwebanwendung auf ASP.NET basiert, können die in diesem Artikel beschriebenen Techniken verwendet werden, um eine Testautomatisierung für Webanwendungen zu erstellen, die mithilfe von dynamischen Seitengenerierungstechnologien geschrieben werden, darunter PHP, JSP, CGI und andere.

Abbildung 2 zeigt die Anforderung/Antwort-Testautomatisierung in Aktion. Beachten Sie, dass die Testautomatisierungsumgebung browserbasiert ist. Ein Vorteil der hier vorgestellten Technik gegenüber anderen Ansätzen besteht darin, dass die Techniken mit den meisten gebräuchlichen Webbrowsern verwendet und auf Testhostcomputern mit den meisten Betriebssystemen ausgeführt werden können.

Abbildung 2 Anforderung/Antwort-Testlauf

image: Request-Response Test Run

Die Testautomatisierungsumgebung besteht aus einer einzelnen HTML-Seite, die einen relativ kurzen Satz von JavaScript-Funktionen enthält. Beachten Sie, dass die erste Zeile der Testlaufausgabe angibt, dass die jQuery-Bibliothek in der Testautomatisierung verwendet wird. Die Umgebung liest Eingabedaten für einen Testfall, die Benutzereingaben entsprechen, und sendet diese Eingabedaten programmgesteuert an die Webanwendung Product Search. Die Umgebung akzeptiert die resultierenden HTTP-Antwortdaten und überprüft, ob diese Antwort den erwarteten Wert enthält, um zu bestimmen, ob der Testfall erfolgreich war oder fehlgeschlagen ist.

In den nachfolgenden Abschnitten dieses Artikels wird zuerst kurz die zu testende Webanwendung aus Abbildung 1 beschrieben, damit Sie verstehen, welche Faktoren bei HTTP-Anforderung/Antwort-Tests relevant sind. Als Nächstes wird der Code für die Testumgebung ausführlich erklärt, dessen Ausführung in Abbildung 2 dargestellt ist, damit Sie die Umgebung an Ihre eigenen Anforderungen anpassen können. Den Abschluss bilden einige Anmerkungen dazu, wann die browserbasierte Anforderung/Antwort-Testautomatisierung mit JavaScript angemessen ist und wann alternative Ansätze möglicherweise passender sind.

In diesem Artikel wird davon ausgegangen, dass Sie mittelgroße Kenntnisse in JavaScript und ASP.NET besitzen, aber selbst wenn Sie Neuling in diesen Technologien sind, sollten Sie den Erläuterungen ohne größere Schwierigkeiten folgen können.

Erstellen der Webanwendung

Visual Studio 2008 wurde zum Erstellen der zu testenden Webanwendung Product Search verwendet. Um die Visual Studio-Funktionen zum Konfigurieren einer Website nutzen zu können, wählte ich in der Hauptmenüleiste den Befehl "Datei" | "Neu" | "Website" aus. Als Nächstes wählte ich im resultierenden Dialogfeld "Neue Website" die Option "Leere Website" aus. Dann gab ich einen HTTP-Speicherort auf meinem lokalen Computer an, um eine komplette ASP.NET-Website zu erstellen, statt einen Speicherort im Dateisystem anzugeben, um den integrierten Visual Studio-Entwicklungssserver zu verwenden. Für den Logikcode wählte ich die Sprache C# aus.

Nachdem ich auf "OK" geklickt hatte, erstellte Visual Studio die leere ProductSearch-Website. Im Fenster "Projektmappen-Explorer" klickte ich mit der rechten Maustaste auf das Projekt "ProductSearch" und wählte im Kontextmenü die Option "Neues Element hinzufügen" aus. Ich wählte das Element "Web Form" aus, akzeptierte die Standardseite namens "Default.aspx" und klickte auf "Hinzufügen", um die Seite zu erzeugen. Als Nächstes erstellte ich eine einfache Benutzeroberfläche für die zu testende Webanwendung, die in Abbildung 3 dargestellt ist.

Abbildung 3 Benutzeroberfläche der Webanwendung

Figure 3 Web App UI
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
  <title>Product Search</title>
</head>
<body bgcolor="#ccbbcc">
  <form id="form1" runat="server">
  <div>
    <asp:Label ID="Label1" runat="server" Text="Find:" 
      Font-Names="Arial" Font-Size="Small">
    </asp:Label>  
    <asp:TextBox ID="TextBox1" runat="server" Width="114px">
    </asp:TextBox>  
    <asp:Button ID="Button1" runat="server" onclick="Button1_Click" 
      Text="Go" />
    <br />

    <asp:RadioButtonList ID="RadioButtonList1" runat="server" 
      Font-Names="Arial" Font-Size="Small">
    <asp:ListItem>Case Sensitive</asp:ListItem>
    <asp:ListItem Selected="True">Not Case Sensitive</asp:ListItem>
    </asp:RadioButtonList>
  </div>
  <asp:ListBox ID="ListBox1" runat="server" Height="131px" Width="246px"
    Font-Names="Courier New" Font-Size="Small">
  </asp:ListBox>
  </form>
</body>
</html>

Wie ich in Kürze erklären werde, müssen Sie beim Erstellen einer HTTP-Anforderung/Antwort-Testautomatisierung die IDs aller Eingabesteuerelemente kennen, für die Sie Benutzereingaben simulieren möchten. Im vorliegenden Fall habe ich Zugriff auf den Quelltext der zu testenden Anwendung. Sollten Sie jedoch keinen Zugriff auf den Quelltext haben, können Sie die IDs der Eingabesteuerelemente mithilfe der Quelltextanzeigefunktion des Webbrowsers ermitteln. Beachten Sie, dass die beiden Optionsfeld-Steuerelemente eigentlich durch ein einzelnes Eingabesteuerelement mit der ID RadioButtonList1 und nicht durch zwei Steuerelemente dargestellt werden, wie Sie möglicherweise vermutet haben.

Ich fügte die Anwendungslogik direkt in die Datei "Default.aspx" ein, statt den CodeBehind-Mechanismus zu verwenden. Am Seitenanfang erstellt ich einen Skriptblock für den Logikcode der Anwendung:

<%@ Page Language="C#" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
  "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<script runat="server">
  // ...
</script>

Ich fügte eine kleine Klasse in den Skriptblock ein, die ein Product-Objekt repräsentiert:

public class Product {
  public int id;
  public string desc;
  public Product(int id, string desc) {
    this.id = id; this.desc = desc;
  }
}

Dann wird ein internes, im Anwendungsbereich geltendes ArrayList-Objekt hinzugefügt, um einen externen Datenspeicher zu simulieren:

public static ArrayList data = null;

In den meisten realistischen Webanwendungsszenarien, sind die Datenspeicher in der Regel extern, z. B. eine XML-Datei oder SQL Server-Datenbank. Bei der Durchführung von HTTP-Anforderung/Antwort-Tests ist der Speicherort des Datenspeichers der Anwendung jedoch einigermaßen irrelevant. Die HTTP-Anforderung kennt den Speicherort des Datenspeichers nicht, und die HTTP-Antwort enthält in der Regel nur HTML. Als Nächstes fügte ich etwas Quelltext hinzu, um den internen Datenspeicher mit Product-Elementen zu füllen: 

protected void Page_Load(object sender, EventArgs e) {
  if (!IsPostBack) {
    data = new ArrayList();
    Product p1 = new Product(111, "Widget");
    Product p2 = new Product(222, "Gizzmo");
    Product p3 = new Product(333, "Thingy");
    data.Add(p1); data.Add(p2); data.Add(p3);
  }
}

Schließlich platzierte ich die gesamte Anwendungslogik in einem Eregnishandler für das Onclick-Ereignis von Button1. Zuerst wird der ListBox1-Ergebnisbereich gelöscht, und die Benutzerdaten werden abgerufen:

ListBox1.Items.Clear();
string filter = TextBox1.Text.Trim();
string sensitivity = RadioButtonList1.SelectedValue;

Die Zeichenfolgenvariable sensitivity enthält entweder die Zeichenfolge "Case Sensitive" oder "Not Case Sensitive".

Danach werden die Tabellenkopfinformationen in den Ergebnisbereich von ListBox1 eingefügt, eine Zeichenfolgenvariable deklariert, in der das Ergebnis der Suche nach Product-Objekten abgelegt wird, und ein Zähler initialisiert, mit dem nachverfolgt wird, wie viele Product-Elemente dem Suchfilter entsprechen:

ListBox1.Items.Add("ID   Description");
ListBox1.Items.Add("================");
string resultRow;
int count = 0;

Jedes Product-Objekt im ArrayList-Datenspeicher wird daraufhin überprüft, ob die Suchfilterzeichenfolge mit der Beschreibung im Feld "desc" des aktuellen Objekts übereinstimmt:

foreach (Product p in data) {
  resultRow = "";
  if (sensitivity == "Not Case Sensitive" &&
    p.desc.IndexOf(filter, 
    StringComparison.CurrentCultureIgnoreCase) >= 0) {
    resultRow = p.id + " " + p.desc; ++count;
  }
  else if (sensitivity == "Case Sensitive" && 
    p.desc.IndexOf(filter) >= 0) {
    resultRow = p.id + " " + p.desc; ++count;
  }
  if (resultRow != "") ListBox1.Items.Add(resultRow);
}

Für jedes Produkt, das dem Suchfilter entspricht, wird eine Ergebniszeichenfolge erstellt und der Trefferzähler um eins erhöht. Beachten Sie, dass die IndexOf-Methode praktischerweise überladen wurde, um ein Argument zu akzeptieren, bei dem zwischen Klein-/Großschreibung unterschieden wird.

Die Anwendungslogik endet mit der Hinzufügung einer Leerzeile und einer Zusammenfassung der Anzahl zum Anzeigebereich von ListBox1:

ListBox1.Items.Add("");
ListBox1.Items.Add("Found " + count + " matching items");

Damit die Webanwendung möglichst klein und einfach bleibt, habe ich viele Vereinfachungen vorgenommen, die in einer Produktionsumgebung nicht empfehlenswert sind. Insbesondere wurde keine Fehlerüberprüfung oder –behandlung bereitgestellt.

Anforderung/Antwort-Testautomatisierung

Die Testumgebungsseite, die in Abbildung 2 während der Ausführung dargestellt wird, wurde mit Editor erstellt. Die Gesamtstruktur der Testumgebung ist in Abbildung 4 dargestellt.

Abbildung 4 Struktur der Testumgebung

<html>
<!-- RequestResponseTests.html -->
<head>
  <script src=’http://localhost/TestWithJQuery/jquery-1.3.2.js’>
  </script>
  <script type="text/javascript">
    $(document).ready(function() {
      logRemark("jQuery Library found and harness DOM is ready\n");
    } );
  
    var targetURL = ‘http://localhost/TestWithJQuery/ProductSearch/Default.aspx’;

    var testCaseData =
[ ‘001,TextBox1=T&RadioButtonList1=Case+Sensitive&Button1=clicked,333 Thingy’,
‘002,TextBox1=t&RadioButtonList1=Not+Case+Sensitive&Button1=clicked,Found 2 matching items’ ];
        
    function runTests() {
      try {
        logRemark(‘Begin Request-Response with JavaScript test run’);
        logRemark("Testing Product Search ASP.NET application\n");
        // ...
        logRemark("\nEnd test run");
      }
      catch(ex) {
        logRemark("Fatal error: " + ex);
      }
    }
    
    function getVS(target) {
      // ...  
    }

    function getEV(target) {
      // ...  
    }

    function sendAndReceive(target, rawVS, rawEV, inputData) {
      // ...  
    }

    function logRemark(comment) {
      // ...  
    }

  </script>
</head>
<body bgcolor="#66ddcc">
  <h3>Request-Response Test Harness Page</h3>
  <p><b>Actions:</b></p><p>
  <textarea id="comments" rows="24" cols=63">
  </textarea></p>
  <input type="button" value="Run Tests" onclick="runTests();" /> 
</body>
</html>

Der Benutzeroberflächencode für die Umgebung im body-Element am Seitenende besteht nur aus etwas Text, einem Textelement zur Anzeige von Informationen und einer Schaltfläche zum Starten der Testautomatisierung.

Am Anfang der Struktur der Testumgebung wird mit dem Skriptelement src auf die jQuery-Bibliothek verwiesen. Die jQuery-Bibliothek ist eine Open-Source-Sammlung von JavaScript-Funktionen, die auf der Website jquery.com verfügbar sind. Obwohl jQuery für die Webentwicklung konzipiert wurde, enthält die Bibliothek Funktionen, die sich gut für eine einfache Anforderung/Antwort-Testautomatisierung eignen. Hier wird auf eine lokale Kopie von Version 1.3.2 der Bibliothek Bezug genommen. Zu Testautomatisierungszwecken ist der Verweis auf eine lokale Kopie der Bibliothek zuverlässiger als ein Verweis auf eine Remotekopie. Danach wird der jQuery-Ausdruck $(document).ready verwendet, um sicherzustellen, dass die Umgebung auf die Bibliothek zugreifen kann und dass das zugehörige DOM in den Arbeitsspeicher geladen wird.

Nachdem die Variable targetURL festgelegt wurde, die auf die zu testende Webanwendung verweist, werden durch Kommas voneinander getrennte Testfälle in einem Zeichenfolgenarray namens testCaseData angegeben. Hier werden nur zwei Testfälle verwendet, in einer Produktionsumgebung können es jedoch Hunderte von Testfällen sein. Externe Testfalldaten sind internen Testfalldaten häufig vorzuziehen, weil sich externe Daten leichter modifizieren und gemeinsam nutzen lassen. Weil hier eine sehr einfache Technik vorgestellt wird, stellen interne Testfalldaten jedoch eine vernünftige Entwurfsentscheidung dar.

Das erste Feld der Testfälle enthält eine numerische Kennung. Das zweite Feld enthält die rohen Anforderungsdaten, die an die zu testende Anwendung gesendet werden. Das dritte Feld enthält das erwartete Ergebnis.

Wie bin ich auf das Format der Anforderungsdaten gekommen? Am einfachsten lässt sich das Format von HTTP-Anforderungsdaten durch vorläufiges Experimentieren mit der zu testenden Anwendungen bestimmen, indem die tatsächlichen Anforderungsdaten mit einem HTTP-Protokollierungstool wie Fiddler untersucht werden.

Ausführen der Tests

Die Hauptsteuerungsfunktion der Umgebung heißt runTests. In der runTests-Funktion wird durch einen try-catch-Mechanismus auf der obersten Ebene eine rudimentäre Fehlerbehandlung bereitgestellt. Es wird eine Hilfsfunktion namens logRemarks verwendet, um Informationen im textarea-Element der Umgebung anzuzeigen. Die Umgebung ruft mithilfe der Hilfsfunktionen getVS und getEV die aktuellen Werte von ViewState und EventValidation aus der zu testenden ASP.NET-Webanwendung ab Diese von der Anwendung generierten Base64-kodierten Werte fungieren primär als Zustands- und Sicherheitsmechanismen und müssen im Rahmen jeder HTTP POST-Anforderung festgelegt werden. Die sendAndReceive-Funktion führt die eigentliche HTTP-Anforderung aus und gibt die zugehörige HTTP-Antwort zurück:

The runTests function iterates through each test case:
for (i = 0; i < testCaseData.length; ++i) {
  logRemark("==========================");
  var tokens = testCaseData[i].split(‘,’);
  var caseID = tokens[0];
  var inputData = tokens[1];
  var expected = tokens[2];
  ...

Hier wird die vordefinierte split-Funktion verwendet, um die Testfallzeichenfolgen jeweils in kleinere Zeichenfolgen aufzuteilen. Anschließend werden die Hilfsfunktionen getVS und getEV aufgerufen:

logRemark(‘Case ID     : ‘ + caseID); 
logRemark(‘Fetching ViewState and EventValidation’);
var rawVS = getVS(targetURL);
var rawEV = getEV(targetURL);

Die Hauptverarbeitungsschleife wird fortgesetzt, indem die Funktion sendAndReceive aufgerufen und überprüft wird, ob die resultierende HTTP-Antwort den für den zugehörigen Testfall erwarteten Wert enthält:

var response = sendAndReceive(targetURL, rawVS, rawEV, inputData);
logRemark("Expected    : ‘" + expected + "’");
if (response.indexOf(expected) >= 0)
  logRemark("Test result : **Pass**");
else if (response.indexOf(expected) == -1)
  logRemark("Test result : **FAIL**");
} // main loop

Die Hilfsfunktion getVS stützt sich auf die jQuery-Bibliothek:

function getVS(target) {
  $.ajax({
    async: false, type: "GET", url: target,
    success: function(resp) {
      if (resp.hasOwnProperty("d")) s = resp.d;
      else s = resp;
         
      start = s.indexOf(‘id="__VIEWSTATE"’, 0) + 24;
      end = s.indexOf(‘"’, start);
    }
  });
  return s.substring(start, end);
}

Hauptzweck der Funktion getVS ist, eine vorbereitende GET-Anforderung an die zu testende Anwendung zu senden, die Antwort abzurufen und den ViewState-Wert herauszulesen. Die Funktion $.ajax akzeptiert eine anonyme Funktion. Die Parameter async, type und URL bedürfen wohl keiner weiteren Erläuterung. Die hasOwnProperty(“d”)-Methode des resp-Antwortobjekts ist im Grunde genommen ein im Microsoft .NET Framework 3.5 vorhandener Sicherheitsmechanismus und in dieser Situation nicht erforderlich.

Der ViewState-Wert wird extrahiert, indem nach dem Anfang des Attributs gesucht und dann 24 Zeichen weiter gezählt wird, um die Position zu erreichen, an der der ViewState-Wert tatsächlich beginnt. Der Code der getEV-Funktion unterscheidet sich vom Code der getVS-Funktion nur dadurch, dass der EventValidation-Wert 30 Zeichen nach dem anfänglichen id=EVENTVALIDATION-Attribut beginnt. Die Verwendung der getrennten Funktionen getVS und getEV bietet Flexibilität, macht jedoch auch zwei getrennte vorbereitende Anforderungen erforderlich. Eine Alternative hierzu wäre, die Funktionen getVS und getEV in einer Hilfsfunktion zusammenzufassen.

Die Hilfsfunktion sendAndReceive führt die eigentliche HTTP-Anforderung aus und ruft die daraus resultierende HTTP-Antwort ab. Zuerst konvertiert diese Funktion die in ViewState und EventValidation enthaltenen reinen Textzeichenfolgen in als URL codierte Zeichenfolgen. Anschließend erstellt sie die Daten, die an die Webanwendung gesendet werden sollen:

function sendAndReceive(target, rawVS, rawEV, inputData) {
  vs = encodeURIComponent(rawVS);
  ev = encodeURIComponent(rawEV);
  postData = inputData + ‘&__VIEWSTATE=’ + vs +
    ‘&__EVENTVALIDATION=’ + ev;
  ...

Die integrierte encodeURIComponent-Funktion codiert die Zeichen, die in POST-Daten nicht zulässig sind, als Escapesequenz. Beispielsweise wird das Zeichen '/' codiert als %2F. Nach einer Protokollnachricht erstellt die sendAndReceive-Funktion mithilfe der $.ajax-Methode eine HTTP POST-Anforderung:

logRemark("Posting " + inputData);
$.ajax({
  async: false,
  type: "POST",
  url: target,
  contentType: "application/x-www-form-urlencoded",
  data: postData,
  ...

Die $.ajax-Methode ist primär zum Senden asynchroner XML HTTP-Anforderungen vorgesehen. Wenn der async-Parameter auf false festgelegt wird, kann die Methode jedoch auch zum Senden synchroner Anforderungen verwendet werden. Äußerst praktisch! Sie können sich den content-type-Parameterwert als magische Zeichenfolge vorstellen, die einfach für von einem HTML-Formularelement gesendete Daten steht. In der sendAndReceive-Funktion wird das gleiche Muster wie in der getVS-Funktion verwendet, um die zugehörige HTTP-Antwort zu erfassen

success: function(resp, status) {
      if (resp.hasOwnProperty("d")) s = resp.d;
      else s = resp;
    },
    error: function(xhr, status, errObj) {
      alert(xhr.responseText);
    }
  });
  return s;
}

Zudem wurde der optionale Fehlerparameter verwendet, um in einem Warnfeld nicht behebbare Fehler anzuzeigen.

Das Hilfsprogramm logRemark ist die letzte Funktion der Testumgebung:

function logRemark(comment) {
  var currComment = $("#comments").val();
  var newComment = currComment + "\n" + comment;
  $("#comments").val(newComment);
}

Hier wird die jQuery-Syntax für Auswahl und Verkettung benutzt, um den aktuellen Textinhalt des textarea-Elements abzurufen, das die ID comments hat. Die Syntax ‘#’ wird verwendet, um ein HTML-Element nach der Kennung (ID) auszuwählen, und die val-Funktion kann sowohl zum Festlegen als auch zum Abrufen von Werten eingesetzt werden. Dem vorhandenen comment-Text wird der comment-Parameterwert und ein Zeilenvorschubzeichen hinzugefügt, und dann wird das textarea-Element unter Verwendung der jQuery-Syntax aktualisiert.

Alternativen

Die Hauptalternativen zum browserbasierten, JavaScript-Sprachansatz, der in diesem Artikel vorgestellt wurde, besteht in der Erstellung einer shellbasierten Umgebung in einer Sprache wie C#. Gegenüber einem shellbasierten Ansatz ist der browserbasierte Ansatz am nützlichsten, wenn Sie in einer äußerst dynamischen Umgebung tätig sind, in der die Testautomatisierung eine kurze Lebensdauer hat. Außerdem ist der hier dargestellte browserbasierte Ansatz ziemlich plattformunabhängig. Die Technik funktioniert mit jeder Browser- und Betriebssystemkombination, die die jQuery-Bibliothek und JavaScript unterstützt.

Dr. James McCaffrey ist für Volt Information Sciences Inc. tätig und organisiert technische Schulungen für Softwareentwickler von Microsoft. Er hat an verschiedenen Microsoft-Produkten mitgearbeitet, unter anderem an Internet Explorer und Search. James McCaffrey ist Autor von  .NET Test Automation Recipes (Apress, 2006) und kann unter jmccaffrey@volt.com or v-jammc@microsoft.com. erreicht werden.