Freigeben über


Testlauf

Automatisieren von Benutzeroberflächentests in WPF-Anwendungen

James McCaffrey

Codedownload verfügbar in der MSDN-Codegalerie
Code online durchsuchen

Inhalt

Die zu testende WPF-Anwendung
Die Automatisierung von Benutzeroberflächentests
Schlussbemerkung

Im Artikel dieses Monats erfahren Sie, wie Sie die Automatisierung von Benutzeroberflächentests für Windows Presentation Foundation (WPF)-Anwendungen erstellen. WPF-Anwendungen verwenden ein neues Grafiksubsystem, und die meisten traditionellen Verfahren für die Automatisierung von Benutzeroberflächentests funktionieren bei WPF-Anwendungen nicht. Glücklicherweise wurde die Bibliothek für die Microsoft-Benutzeroberflächenautomatisierung (Microsoft UI Automation, MUIA) im Hinblick auf die Benutzeroberflächenautomatisierung für WPF-Anwendungen entworfen. Sie können die Benutzeroberflächenautomatisierungs-Bibliothek von Microsoft außerdem verwenden, um klassische Win32-Anwendungen und WinForm-basierte .NET-Anwendungen auf Hostcomputern zu testen, auf denen Betriebssysteme ausgeführt werden, die Microsoft .NET Framework 3.0 unterstützen.

Verglichen mit älteren Alternativen zur Benutzeroberflächenautomatisierung ist die Benutzeroberflächenautomatisierungs-Bibliothek von Microsoft leistungsfähiger und konsistenter, und nach einem gewissen anfänglichen Lernaufwand werden Sie feststellen, dass sie viel leichter verwendet werden kann. In diesem Artikel wird davon ausgegangen, dass Sie mit WPF-Anwendungen grundsätzlich vertraut sind und gute C#-Fähigkeiten, aber keine Erfahrung mit der MUIA-Bibliothek haben.

Anhand eines Bildschirmfotos können Sie am besten sehen, worauf ich hinaus will. Das Bild in Abbildung 1 zeigt, dass hier eine einfache aber repräsentative WPF-Anwendung getestet wird. Die Anwendung heißt CryptoCalc, und sie berechnet einen kryptografischen Hash einer Eingabezeichenfolge, indem sie eines von drei Hashverfahren verwendet: MD5-Hashing, SHA1-Hashing oder DES-Verschlüsselung.

Abbildung 1 Benutzeroberflächenautomatisierung einer WPF-Anwendung

Das MD5-Hashverfahren (Message Digest 5) akzeptiert ein beliebig großes Array von Bytes und gibt einen 16-Byte-Fingerabdruck zurück, der dann für verschiedene Identifikationszwecke verwendet werden kann. Das SHA1-Verfahren (Secure Hash Algorithm 1) ist so ähnlich wie MD5, außer dass SHA1 einen anderen Algorithmus verwendet und einen 20-Byte-Fingerabdruck zurückgibt. Der Datenverschlüsselungsstandard (Data Encryption Standard, DES) ist ein symmetrisches Schlüsselverschlüsselungsverfahren, das auch für das Erstellen eines Identifizierungsbytearrays verwendet werden kann. Das DES-Kryptohashverfahren gibt ein Bytearray zurück, das mindestens so groß ist wie die Anzahl der Eingabebytes.

Die Automatisierung von Benutzeroberflächentests (siehe Abbildung 1) wird über eine Konsolenanwendung ausgeführt, die die zu testende Anwendung startet. Sie verwendet die Benutzeroberflächenautomatisierungs-Bibliothek von Microsoft, um Verweise auf die Anwendung und die Benutzersteuerelemente in der Anwendung zu erhalten, und simuliert einen Benutzer, der „Hello!“ eingibt, die Option „DES Encrypt“ auswählt und auf die Schaltfläche „Compute“ (Berechnen) klickt. Die Testautomatisierung prüft dann den sich ergebenden Status der zu testenden Anwendung durch Untersuchen des Ergebnistextfelds auf einen erwarteten Wert und meldet dann das Ergebnis (Erfolg/Misserfolg). Das Bildschirmfoto in Abbildung 1 wurde erstellt, kurz bevor die Testautomatisierung die zu testende Anwendung durch Simulieren von Benutzerklicks auf die Menüoptionen „File“ (Datei) und „Exit“ (Beenden) geschlossen hat.

In den nachfolgenden Abschnitten dieses Artikels wird die getestete CryptoCalc-WPF-Anwendung kurz beschrieben, es wird erläutert, wie die zu testende Anwendung gestartet wird, wie die Benutzeroberflächenautomatisierungs-Bibliothek von Microsoft verwendet wird, um Verweise auf die Anwendung und die Benutzersteuerelemente abzurufen, wie Benutzeraktionen simuliert werden und wie der Anwendungszustand geprüft wird. Außerdem wird beschrieben, wie Sie das hier vorgestellte Testsystem erweitern und ändern können, damit es Ihre persönlichen Anforderungen erfüllt. Die Möglichkeit, die Benutzeroberflächenautomatisierungs-Bibliothek von Microsoft zum Testen von WPF-Anwendungen zu verwenden, werden Sie sicher als eine hilfreiche Erweiterung Ihres Repertoires ansehen.

Die zu testende WPF-Anwendung

Im Folgenden soll die zu testende WPF-Anwendung kurz erläutert werden, damit das Ziel der Testautomatisierung und die Entwurfsprobleme, die mit der Automatisierung von Benutzeroberflächentests verbunden sind, klar sind. Die CryptoCalc-Anwendung ist eine einfache, Einzelfenster-Benutzeranwendung, die einen kryptografischen Hash einer Zeichenfolge berechnet. Die Anwendung akzeptiert eine benutzerdefinierte Zeichenfolge von bis zu 24 Zeichen in einem TextBox-Steuerelement, konvertiert die Eingabezeichenfolge in ein Array von Bytes, berechnet eine von drei Arten von Kryptohashes der Eingabebytes und zeigt die daraus resultierenden Hashbytes in einem zweiten TextBox-Steuerelement an.

Die hier zu testende Anwendung wurde in Visual Studio 2008 mit C# entworfen, und das Projekt wurde CryptoCalc genannt. Die WPF-Vorlage generiert eine Definition für eine grundlegende Anwendungsbenutzeroberfläche als XAML-Datei:

<Window x:Class="CryptoCalc.Window1"
  xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"
  Title="Window1" Height="300" Width="300">
  <Grid></Grid>
</Window>

Beachten Sie, dass die Definition für das Window-Steuerelement oberster Ebene kein Name-Attribut enthält. Dies ist wichtig, denn in Kürze wird sich Folgendes zeigen: Wenn Sie eine Testautomatisierung erstellen, haben Sie eine einfache Möglichkeit, mit der MUIA-Bibliothek einen Verweis auf ein Steuerelement zu erstellen, indem Sie auf die AutomationId-Eigenschaft zugreifen, die durch den Compiler aus dem Name-Attribut des Steuerelements generiert wird. Steuerelemente ohne ein XAML-Name-Attribut erhalten keine AutomationId-Eigenschaft. Diese Idee ist ein spezifisches, grundlegendes Beispiel, das verdeutlicht, wie wichtig es ist, Anwendungsentwurfsprobleme in Bezug auf Bereiche wie Sicherheit, Erweiterbarkeit und Testautomatisierung zu berücksichtigen.

Als Nächstes wurden ein Label-Steuerelement und ein TextBox-Steuerelement hinzugefügt, indem Elemente von der Visual Studio-Toolbox in die Entwurfsoberfläche gezogen wurden:

<Label Height="28" HorizontalAlignment="Left"
 Margin="10,33,0,0" Name="label1" VerticalAlignment="Top"
 Width="120">Enter string:</Label>
<TextBox MaxLength="24" Height="23" Margin="10,57,51,0"
 Name="textBox1" VerticalAlignment="Top" /> 

Beachten Sie, dass diese Steuerelemente standardmäßig normale Name-Atribute erhalten: label1 bzw. textBox1. Als Nächstes wurden drei RadioButton-Steuerelemente innerhalb eines GroupBox-Steuerelements platziert. Im Unterschied zum alten WinForms-GroupBox-Steuerelement akzeptiert ein WPF-GroupBox-Steuerelement nur ein einzelnes Element. Deshalb wurden die drei RadioButton-Steuerelemente von einem einzelnen StackPanel-Container umschlossen (der mehrere Elemente enthalten kann):

<GroupBox Header="Crypto Type" Margin="10,91,118,127"
 Name="groupBox1">
<StackPanel Height="52" Name="stackPanel1" Width="127">
 <RadioButton Height="16" Name="radioButton1" Width="120">
  MD5 Hash</RadioButton>
 <RadioButton Height="16" Name="radioButton2" Width="120">
  SHA1 Hash</RadioButton>
 <RadioButton Height="16" Name="radioButton3" Width="120">
  DES Encrypt</RadioButton>
</StackPanel>
</GroupBox>

Als Nächstes wurde im zentralen Window-Steuerelement ein Button-Steuerelement platziert, von dem die Kryptohashberechnung ausgelöst wird, sowie ein TextBox-Steuerelement, in dem das Ergebnis festgehalten wird:

<Button Height="23" Margin="10,0,0,90" Name="button1"
 VerticalAlignment="Bottom" Click="button1_Click"
 HorizontalAlignment="Left" Width="89">Compute</Button>
<TextBox Height="63" Margin="10,0,51,13" Name="textBox2"
 VerticalAlignment="Bottom" TextWrapping="Wrap" />

Eines der Features, das mir bei WPF-Anwendungen wirklich gut gefällt, ist das neue Menu-Steuerelement-Paradigma, das im Unterschied zu WinForm-Menüelementen Menüs und Untermenüs als gewöhnliche Benutzersteuerelemente behandelt. Zuerst wurde ein zentrales Menu-Containersteuerelement in den obersten Teil der CryptoCalc-Anwendung gezogen:

  <Menu Height="22" Name="menu1"
    VerticalAlignment="Top" IsMainMenu="True" >
  </Menu>

Visual Studio 2008 unterstützt keinen Drag & Drop-Entwurf für untergeordnete MenuItem-Steuerelemente. Deshalb wurden der XAML-Definitionsdatei die Menüelemente manuell durch Hinzufügen eines File-Menüelements oberster Ebene hinzugefügt:

  <MenuItem Header="_File">
    <MenuItem Header="_New" Name="fileNew" />
  <MenuItem Header="_Open" Name="fileOpen" />
  <Separator />
  <MenuItem Header="E_xit" Name="fileExit"
    InputGestureText="Alt-F4" ToolTip="Exit CryptoCalc"
    Click="OnFileExit" />
</MenuItem>

Der WPF-Entwurf unterstützt die übliche Zugriffstastensyntax, wie z. B. _File. Das <Separator />-Tag ist sauber und einfach. Durch das ToolTip-Attribut wird Code generiert, der eine kurze Hilfemeldung anzeigt, wenn der Benutzer die Maus über das zugehörige MenuItem-Steuerelement bewegt. Nachdem es kompiliert worden ist, generiert das Click OnFileExit-Attribut C#-Code, der einen Ereignishandler mit der folgenden Signatur erwartet:

public void OnFileExit(object sender, RoutedEventArgs e) {
  //...       
}

Deshalb wurde, nachdem der CryptoCalc-Anwendung der reguläre Anwendungslogikcode hinzugefügt wurde, manuell der Ereignishandler hinzugefügt und anschließend die Funktionalität durch Platzieren von „this.Close“ im Methodentext bereitgestellt. Beachten Sie, dass die Menüelement-Steuerelemente „New“ und „Open“ dieses Mal keine zugeordneten Ereignisse enthalten. Dadurch soll darauf hingewiesen werden, dass die Automatisierung von Benutzeroberflächentests oft während der Entwicklung stattfindet, wenn viele Anwendungsfeatures unvollständig sind. Der Entwurf des Help-Steuerelements oberster Ebene folgt demselben Muster wie das File-Steuerelement:

<MenuItem Header="_Help">
 <MenuItem Header="Help Topics" />
 <Separator />
 <MenuItem Header="About CryptoCalc" />
</MenuItem>

Die Definitionen für den Menu-Container und das MenuItem-Steuerelement generieren C#-Code, der wiederum die Benutzeroberfläche erstellt, die Sie im Bildschirmfoto in Abbildung 2 sehen. Nach Abschluss des Benutzeroberflächenentwurfs wurde zur Codeansicht umgeschaltet. Dann wurden den using-Anweisungen, die von Visual Studio in der Datei „Window1.xaml.cs“ generiert worden sind, zwei using-Anweisungen hinzugefügt, damit auf die Kryptoklassen und -methoden zugegriffen werden kann, ohne ihre Namen vollständig zu qualifizieren:

using System.Security.Cryptography;
using System.IO;

fig02.gif

Abbildung 2 WPF-Anwendungsmenü „File“

Die Hauptfunktionalität der einfachen CryptoCalc-Anwendung ist in der button1_Click-Methode enthalten und in Abbildung 3 aufgelistet. Der vorliegende Code greift zuerst auf den Text im textBox1-Steuerelement zu und konvertiert diesen Text in ein Bytearray. Beachten Sie Folgendes: Um den Umfang des Beispielcodes gering zu halten, wurde die Fehlerüberprüfung, die Sie normalerweise in einer Produktionsumgebung durchführen würden, weggelassen.

Abbildung 3 StatCalc-Anwendungscode

private void button1_Click(object sender, RoutedEventArgs e)
{
  string input = textBox1.Text;
  byte[] inputBytes = Encoding.UTF8.GetBytes(input);

  if (radioButton1.IsChecked == true) { 
    MD5CryptoServiceProvider md5 = new MD5CryptoServiceProvider();
    byte[] hashedBytes = md5.ComputeHash(inputBytes);
    textBox2.Text = BitConverter.ToString(hashedBytes);
  }
  else if (radioButton2.IsChecked == true) { 
    SHA1CryptoServiceProvider sha = new SHA1CryptoServiceProvider();
    byte[] hashedBytes = sha.ComputeHash(inputBytes);
    textBox2.Text = BitConverter.ToString(hashedBytes);
  }
  else if (radioButton3.IsChecked == true) { 
    DESCryptoServiceProvider des = new DESCryptoServiceProvider();
    byte[] blanks = System.Text.Encoding.UTF8.GetBytes("        "); // 8 spaces
    des.Key = blanks;
    des.IV = blanks;
    des.Padding = PaddingMode.Zeros;
    MemoryStream ms = new MemoryStream();
    CryptoStream cs =
      new CryptoStream(ms, des.CreateEncryptor(), CryptoStreamMode.Write);
    cs.Write(inputBytes, 0, inputBytes.Length);
    cs.Close();
    byte[] encryptedBytes = ms.ToArray();
    ms.Close();
    textBox2.Text = BitConverter.ToString(encryptedBytes);
  }
} 

Die Anwendungslogik verzweigt sich in Abhängigkeit vom ausgewählten RadioButton-Steuerelement. Interessanterweise musste die IsChecked-Eigenschaft auf Gleichheit mit dem Wert „true“ geprüft werden, weil sie den Typ „?bool“ (boolescher Wert, der Nullwerte zulässt) zurückgibt. Der Code für das MD5- und SHA1-Hashing sollte keiner Erläuterung bedürfen.

Der DES-Verschlüsselungsalgorithmus erfordert einen 64-Bit-Schlüssel. Weil DES für das Hashing und nicht für das Codieren und Decodieren verwendet wird, kommt hier ein Platzhalterschlüssel zum Einsatz, der durch 8 Leerzeichen generiert wird. Beim Verwenden der symmetrischen Schlüsselverschlüsselung für Hashzwecke verwende ich meist den Nullschlüssel { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, aber das DESCryptoServiceProvider-Objekt kennzeichnet dies als einen bekannten schwachen Schlüssel und löst eine Ausnahme aus. Es wird dasselbe Leerstellenbytearray verwendet, um einen Wert für den so genannten Initialisierungsvektor bereitzustellen.

Die DES-Verschlüsselung funktioniert für Blöcke von 8 Byte. Deshalb gebe ich PaddingMode.Zeros an, damit jede Eingabe mit Nullen aufgefüllt wird, um die Größe der Eingabe auf ein gerades Vielfaches von 8 Byte zu bringen. Beachten Sie, dass ich PaddingMode.Zeros für das Codieren und Decodieren nicht verwenden würde, weil das Auffüllen mit Nullen keine Entschlüsselung ermöglicht (der Entschlüsselungsalgorithmus kann nicht bestimmen, bei welchen Nullen im verschlüsselten Text es sich um eine Auffüllung handelt und welche Nullen Teil der ursprünglichen Nur-Text-Zeichenfolge sind).

Die Automatisierung von Benutzeroberflächentests

Beim Erstellen einer Testautomatisierung mit der Benutzeroberflächenautomatisierungs-Bibliothek von Microsoft müssen Sie wissen, wie Sie die Steuerelemente in der zu testenden Anwendung identifizieren. Eine gute Möglichkeit ist die Verwendung des UISpy-Tools. Für diejenigen unter Ihnen, denen es nicht bekannt ist: UISpy ist das WPF-Äquivalent zum alten Spy++-Tool und ermöglicht Ihnen, Eigenschaften der Benutzeroberflächenkomponenten einer WPF-Anwendung zu untersuchen. Das UISpy-Tool ist im Microsoft Windows SDK enthalten und als kostenloser Download unter microsoft.com/downloads verfügbar.

Das Bildschirmfoto in Abbildung 4 zeigt das Ergebnis, das Sie beim Abzielen auf das Button-Steuerelement in der CryptoCalc-Anwendung erhalten. Aus der Perspektive der Testautomatisierung sind die wichtigen Felder für das Identifizieren von Anwendungssteuerelementen die Werte für ControlType (in diesem Fall ControlType.Edit), AutomationId (button1) und Name (Compute). Das wichtige Feld für das Ändern von Anwendungssteuerelementen ist die ControlPatterns-Liste (Invoke).

Abbildung 4 Untersuchen der Steuerelemente mit UISpy

Das manuelle Testen selbst dieser winzigen CryptoCalc-Anwendung über die Benutzeroberfläche wäre langweilig, fehleranfällig, zeitaufwändig und ineffizient. Sie müssten eine Eingabe eingeben, auf die Schaltfläche „Compute“ klicken, visuell die Antwort überprüfen und manuell das Ergebnis (Erfolg/Misserfolg) aufzeichnen. Ein viel besserer Ansatz ist die Verwendung der Benutzeroberflächenautomatisierungs-Bibliothek von Microsoft, um eine Testautomatisierung zu erstellen, die einen Benutzer beim Ausführen der Anwendung simuliert, und dann bestimmt, ob die Anwendung richtig reagiert hat. Durch das Automatisieren aufwändiger, monotoner Tests können Sie Zeit für interessantere und nützlichere manuelle Testfälle gewinnen, bei denen Ihre Erfahrung und Intuition eine wichtige Rolle spielen.

Die Gesamtstruktur der Testumgebung, von der die in Abbildung 1 gezeigte Ausgabe erstellt wurde, ist in Abbildung 5 aufgelistet. Ich habe Visual Studio 2008 gestartet und eine neue Konsolenanwendung erstellt. Dabei wurde C# verwendet, aber es sollte bei Bedarf möglich sein, meinen Testautomatisierungscode problemlos in Visual Basic .NET zu konvertieren. Als Nächstes wurden der UIAutomationClient.dll- und der UIAutomationTypes.dll-Bibliothek Projektverweise hinzugefügt. Diese Bibliotheken sind in .NET Framework 3.0 enthalten, aber standardmäßig für ein Visual Studio-Projekt nicht sichtbar. Die Bibliotheken befinden sich in der Regel im Verzeichnis „C:\Programme\Reference Assemblies\Microsoft\Framework\v3.0“. Beachten Sie, dass die UIAutomationClient.dll-Bibliothek die für die Testautomatisierung erforderlichen Schlüsselklassen enthält. Die UIAutomationTypes.dll-Bibliothek enthält verschiedene Typdefinitionen, die von der MUIA-Testautomatisierung verwendet werden.

Abbildung 5 Codestruktur der Automatisierung von Benutzeroberflächentests

using System;
using System.Diagnostics; 
using System.Threading; 
using System.Windows.Automation; 

namespace Harness {
  class Program {
    static void Main(string[] args) {
      try {
        Console.WriteLine("\nBegin WPF UIAutomation test run\n");
        // launch CryptoCalc application
        // get reference to main Window control
        // get references to user controls
        // manipulate application
        // check resulting state and determine pass/fail
        Console.WriteLine("\nEnd automation\n");
      }
      catch (Exception ex) {
        Console.WriteLine("Fatal: " + ex.Message);
      }
    } // Main()
  } // class
} // ns

Der Bequemlichkeit halber werden using-Anweisungen hinzugefügt, die auf den System.Diagnostics-Namespace und den System.Threading-Namespace zeigen (damit die Process-Klasse bzw. die Thread.Sleep()-Methode problemlos verwendet werden kann). Wie bei Testautomatisierungen üblich, wird die Umgebung von einem try/catch-Block oberster Ebene umschlossen, um etwaige schwerwiegende Fehler zu behandeln. Der Testautomatisierungscode beginnt mit dem Start der zu testenden Anwendung:

Console.WriteLine("Launching CryptoCalc application");
Process p = null;
p = Process.Start("..\\..\\..\\CryptoCalc\\bin\\Debug\\CryptoCalc.exe");

Bevor jetzt überhaupt versucht wird, eine Testautomatisierung durchzuführen, muss überprüft werden, ob der Prozess, der mit der zu testenden CryptoCalc-Anwendung verknüpft ist, auf dem Hostcomputer registriert ist. Obwohl die Testumgebung einfach durch Einfügen einer Thread.Sleep-Anweisung angehalten werden kann, ist es schwierig zu wissen, wie lange die Unterbrechung dauern soll. Ein besserer Ansatz ist, eine intelligente Verzögerungsschleife zu verwenden:

int ct = 0;
do {
  Console.WriteLine("Looking for CryptoCalc process. . . ");
  ++ct;
  Thread.Sleep(100);
} while (p == null && ct < 50);

Hier halte ich bei jedem Durchlauf der Verzögerungsschleife 100 Millisekunden lang an. Die Umgebung beendet die Verzögerungsschleife, sobald das Prozessobjekt nicht null ist und demzufolge der Prozess gefunden wurde oder wenn die Schleife 50 Mal ausgeführt wurde. Abbildung 6 zeigt, wie ich bestimmen kann, ob die Verzögerungsschleife mit einem Timeout endete oder ob der AUT-Prozess gefunden wurde.

Abbildung 6 Erkennen, was geschehen ist

if (p == null)
  throw new Exception("Failed to find CryptoCalc process");
else
  Console.WriteLine("Found CryptoCalc process");

// Next I fetch a reference to the host machine's Desktop as an
// AutomationElement object:

Console.WriteLine("\nGetting Desktop");
AutomationElement aeDesktop = null;
aeDesktop = AutomationElement.RootElement;
if (aeDesktop == null)
  throw new Exception("Unable to get Desktop");
else
  Console.WriteLine("Found Desktop\n");

Alle Steuerelemente der zu testenden WPF-Anwendung können als untergeordnete Elemente des zentralen Window-Steuerelements der Anwendung betrachtet werden. Das Hauptfenster ist ein untergeordnetes Element des Gesamtdesktops. Deshalb ist ein Verweis auf den Desktop erforderlich, um einen Verweis auf die Anwendung abzurufen. Jetzt wird der zu testenden Anwendung die FindFirst-Methode hinzugefügt:

AutomationElement aeCryptoCalc = null;
int numWaits = 0;
do {
  Console.WriteLine("Looking for CryptoCalc main window. . . ");
  aeCryptoCalc = aeDesktop.FindFirst(TreeScope.Children,
    new PropertyCondition(AutomationElement.NameProperty, "CryptoCalc"));
  ++numWaits;
  Thread.Sleep(200);
} while (aeCryptoCalc == null && numWaits < 50);

Statt des intelligenten Verzögerungsschleifenverfahrens wird hier das beliebig lange Sleep-Verfahren verwendet. Die FindFirst-Methode wird verwendet, wenn das Steuerelement, das Sie suchen, ein einzelnes Steuerelement ist. FindFirst akzeptiert zwei Argumente. Das erste ist ein Bereichswert. Die drei häufigsten Bereichswerte, die in der Testautomatisierung verwendet werden, sind TreeScope.Children, TreeScope.Descendants und TreeScope.Parent. Weil die CryptoCalc-Anwendung ein direktes untergeordnetes Element des Desktops ist, wird hier der TreeScope.Children-Bereich verwendet.

Das zweite Argument zu FindFirst ist ein Objekt, das Informationen repräsentiert, durch die das gesuchte Steuerelement identifiziert wird. Hier gebe ich an, dass ich ein Steuerelement suche, das eine Name-Eigenschaft mit dem Wert „CryptoCalc“ hat. Ich hätte auch die AutomationId-Eigenschaft verwenden können, was weiter unten beschrieben wird. Jetzt überprüfe ich, ob tatsächlich ein Verweis auf das zentrale Window-Steuerelement der Anwendung vorliegt:

if (aeCryptoCalc == null)
  throw new Exception("Failed to find CryptoCalc main window");
else
  Console.WriteLine("Found CryptoCalc main window");

Sobald die Anwendung vorliegt, kann ich ihren Verweis verwenden, um Verweise auf alle Benutzersteuerelemente zu erhalten, die durch die Testautomatisierung geändert oder untersucht werden. Ich beginne mit dem Button-Steuerelement:

Console.WriteLine("\nGetting all user controls");
AutomationElement aeButton = null;
aeButton = aeCryptoCalc.FindFirst(TreeScope.Children,
  new PropertyCondition(AutomationElement.NameProperty, "Compute"));
if (aeButton == null)
  throw new Exception("No compute button");
else
  Console.WriteLine("Got Compute button");

Ich verwende dasselbe Muster, das ich abgerufen habe, um einen Verweis auf das Hauptfenster abzurufen. Hier ist das Button-Steuerelement ein direktes untergeordnetes Element des zentralen Window-Anwendungssteuerelements. Weil das Button-Steuerelement ein statisches Steuerelement ist, muss ich vor dem Zugriff auf den Steuerelementverweis das Verzögerungsschleifenverfahren verwenden. Im Fall dynamischer Steuerelemente sollten Sie das Verzögerungsschleifenverfahren verwenden.

Als Nächstes sollen Verweise auf die zwei TextBox-Steuerelemente abgerufen werden. Obwohl die zwei TextBox-Steuerelemente die Namen textBox1 und textBox2 haben, erhalten die Steuerelemente keine Name-Eigenschaften. Deshalb kann ich nicht dasselbe NameProperty-Muster verwenden, das ich für das Button-Steuerelement verwendet habe. Die TextBox-Steuerelemente erhalten jedoch eine AutomationId-Eigenschaft, die ich verwenden könnte, um einen Verweis auf die Steuerelemente abzurufen, wie z. B.:

aeTextBox1 = aeCryptoCalc.FindFirst(TreeScope.Children,
  new PropertyCondition(AutomationElement.AutomationIdProperty, "textBox1"));

Stattdessen habe ich, vor allem zu Demonstrationszwecken, beschlossen, einen anderen Ansatz zu wählen. Statt mit der Name-Eigenschaft des Steuerelements ein einzelnes Steuerelement zu identifizieren, wird hier die FindAll-Methode verwendet, um eine Auflistung der Steuerelemente nach Steuerelementtyp abzurufen. Es stellt sich heraus, dass es sich bei TextBox-Steuerelementen um ControlType.Edit-Typen handelt. Deshalb erfasst mein Code alle TextBox-Steuerelemente:

AutomationElementCollection aeAllTextBoxes = null;
aeAllTextBoxes = aeCryptoCalc.FindAll(TreeScope.Children,
  new PropertyCondition(AutomationElement.ControlTypeProperty,
  ControlType.Edit));
if (aeAllTextBoxes == null)
  throw new Exception("No textboxes collection");
else
  Console.WriteLine("Got textboxes collection");

Sobald diese Auflistung vorliegt, kann ich mithilfe der Arrayindizierung auf jedes TextBox zugreifen:

AutomationElement aeTextBox1 = null;
AutomationElement aeTextBox2 = null;
aeTextBox1 = aeAllTextBoxes[0];
aeTextBox2 = aeAllTextBoxes[1];
if (aeTextBox1 == null || aeTextBox2 == null)
  throw new Exception("TextBox1 or TextBox2 not found");
else
  Console.WriteLine("Got TextBox1 and TextBox2");

Im Allgemeinen ist das Abrufen von Verweisen mithilfe der Name-Eigenschaft oder der AutomationId-Eigenschaft ein besserer Ansatz als das Verwenden von ControlType, aber in einigen Szenarios haben Sie möglicherweise keine Wahl. Als Nächstes wird für das Testszenario ein Verweis auf das RadioButton-Steuerelement abgerufen:

AutomationElement aeRadioButton3 = null;
aeRadioButton3 = aeCryptoCalc.FindFirst(TreeScope.Descendants,
  new PropertyCondition(AutomationElement.NameProperty,
   "DES Encrypt"));
if (aeRadioButton3 == null)
  throw new Exception("No RadioButton");
else
  Console.WriteLine("Got RadioButton3");

Ich verwende ein Muster, das demjenigen ähnelt, das ich zum Abrufen eines Verweises auf das Button-Steuerelement verwendet habe. Beachten Sie jedoch, dass ich TreeScope.Descendants statt TreeScope.Children angebe. Der Grund dafür ist, dass das RadioButton-Steuerelement ein untergeordnetes Element des GroupBox-Steuerelements und deswegen kein direktes untergeordnetes Element des zentralen Window-Steuerelements ist. Alternativ hätte ich zuerst einen Verweis auf das GroupBox-Steuerelement (als untergeordnetes Element des zentralen Window-Steuerelements) abrufen und dann diesen Verweis verwenden können, um einen Verweis auf das RadioButton-Steuerelement zu erhalten. Nachdem Verweise auf die Steuerelemente vorliegen, kann ich mit der Manipulation der zu testenden Anwendung anfangen. Ich beginne durch Simulieren der Benutzereingabe im TextBox1-Steuerelement:

Console.WriteLine("\nSetting input to 'Hello1!'");
ValuePattern vpTextBox1 =
  (ValuePattern)aeTextBox1.GetCurrentPattern(ValuePattern.Pattern);
vpTextBox1.SetValue("Hello!");

Die Verwendung der SetValue-Methode ist wahrscheinlich nicht überraschend, aber beachten Sie, dass auf SetValue() nicht direkt über das aeTextBox1-Objekt zugegriffen wird. Stattdessen wird ein ValuePattern-Mittlerobjekt verwendet. Das Konzept von AutomationPattern-Objekten wie ValuePattern ist wahrscheinlich das größte konzeptionelle Hindernis für Programmierer, für die die Benutzeroberflächenautomatisierungs-Bibliothek von Microsoft noch neu ist. Sie können sich Musterobjekte als eine abstrakte Möglichkeit vorstellen, die Steuerelementfunktionalität anzuzeigen, die vom Typ oder vom Erscheinungsbild des Steuerelements unabhängig ist. Anders ausgedrückt: Sie können spezifische AutomationPattern-Instanzen wie ValuePattern verwenden, um spezifische Steuerelementfunktionalität zu ermöglichen.

Eine weitere Vereinfachung erfolgt dadurch, dass ich mir vorstelle, dass das ControlType eines Steuerelements offenlegt, welche Art von Steuerelement das Steuerelement ist, und dass das Pattern eines Steuerelements anzeigt, was das Steuerelement kann. Ich verwende einen ähnlichen Ansatz wie beim Simulieren eines Benutzers, der das RadioButton3-Steuerelement auswählt:

Console.WriteLine("Selecting 'DES Encrypt' ");
SelectionItemPattern spSelectRadioButton3 =
  (SelectionItemPattern)aeRadioButton3.GetCurrentPattern(
    SelectionItemPattern.Pattern);
spSelectRadioButton3.Select();

Dieses Mal wird SelectionItemPattern verwendet, um eine Auswahl zu ermöglichen. Der Name der GetCurrentPattern-Methode ist für diejenigen, die sich zum ersten Mal mit der MUIA-Bibliothek beschäftigen, manchmal verwirrend. Von einem Testautomatisierungsstandpunkt aus dient die Methode dem Festlegen, nicht dem Abrufen eines angegeben AutomationPattern-Elements. Aber aus der Perspektive eines Clientservers ruft der Automatisierungsclientcode eine bestimmte Eigenschaft aus dem Servercode der zu testenden Anwendung ab.

Der Code, der zum Simulieren eines Klicks auf das Schaltflächen-Steuerelement „Calculate“ verwendet wird, sollte dies veranschaulichen:

Console.WriteLine("\nClicking on Compute button");
InvokePattern ipClickButton1 =
  (InvokePattern)aeButton.GetCurrentPattern(
    InvokePattern.Pattern);
ipClickButton1.Invoke();
Thread.Sleep(1500);

Hier wird im Wesentlichen InvokePattern verwendet, um einen Schaltflächenklick zu ermöglichen und dann den Klick mithilfe der Invoke-Methode auszuführen. Beachten Sie, dass für 1,5 Sekunden angehalten wird, um der Anwendung Zeit zum Antworten zu geben. Ich könnte auch in eine Verzögerungsschleife gehen, die regelmäßig prüft, ob das textBox2-Ergebnisfeld leer ist oder nicht. An dieser Stelle im Testautomatisierungscode wurde die zu testende Anwendung gestartet, „Hello!“ in das TextBox-Eingabesteuerelement eingegeben, das RadioButton-Steuerelement „DES Encrypt“ ausgewählt und auf die Schaltfläche „Compute“ geklickt.

Untersuchen Sie jetzt das TextBox2-Steuerelement, um zu sehen, ob Sie einen korrekten erwarteten Wert erhalten haben:

Console.WriteLine("\nChecking TextBox2 for '91-1E-84-41-67-4B-FF-8F'");
TextPattern tpTextBox2 =
  (TextPattern)aeTextBox2.GetCurrentPattern(TextPattern.Pattern);
string result = tpTextBox2.DocumentRange.GetText(-1);

Hier verwende ich TextPattern, um den Aufruf einer GetText-Methode vorzubereiten. Beachten Sie, dass GetText indirekt über eine DocumentRange-Eigenschaft aufgerufen wird, die einen Textbereich zurückgibt, der den Haupttext eines Dokuments einschließt, in diesem Fall ein einzelnes Textbox-Element. Das Argument „-1“ zu GetText wird verwendet, damit es keine maximale Beschränkung für die Länge der Rückgabezeichenfolge gibt. Eine alternative Möglichkeit für das Lesen des Inhalts des TextBox2-Steuerelements besteht darin, die GetCurrentPropertyValue-Methode zu verwenden:

string result =
  (string)aeTextBox2.GetCurrentPropertyValue(ValuePattern.ValueProperty);

Ich habe die Testfalleingabe in die Testumgebung hartkodiert. Ein flexiblerer Ansatz besteht darin, die Testfalleingabe und die erwarteten Werte aus einem externen Datenspeicher zu lesen. Nun, da der eigentliche Wert der zu testenden Anwendung vorliegt, prüfe ich in Bezug auf einen erwarteten Wert, um das Ergebnis des Testszenarios (Erfolg/Misserfolg) zu bestimmen.

if (result == "91-1E-84-41-67-4B-FF-8F") {
  Console.WriteLine("Found it");
  Console.WriteLine("\nTest scenario: Pass");
} 
else {
  Console.WriteLine("Did not find it");
  Console.WriteLine("\nTest scenario: *FAIL*");
}

Dabei wird das Testszenarioergebnis einfach in der Befehlshell angezeigt. In einer Produktionsumgebung sollten Sie die Testergebnisse in der Regel in einem externen Speicher festhalten.

Wenn das Testszenario abgeschlossen ist, kann die zu testende Anwendung mithilfe des Menu-Steuerelements geschlossen werden. Zuerst rufe ich das File-MenuItem-Steuerelement oberster Ebene ab:

Console.WriteLine("\nClicking on File-Exit item in 5 seconds");
Thread.Sleep(5000);
AutomationElement aeFile = null;
aeFile = aeCryptoCalc.FindFirst(TreeScope.Descendants,
  new PropertyCondition(AutomationElement.NameProperty, "File"));
if (aeFile == null)
  throw new Exception("Could not find File menu");
else
  Console.WriteLine("Got File menu");

Beachten Sie, dass ich den TreeScope.Descendants-Bereich verwende, weil das File-MenuItem-Steuerelement ein untergeordnetes Steuerelement des Menu-Containersteuerelements ist. Nun simuliere ich einen Benutzer, der auf das File-Element klickt:

Console.WriteLine("Clicking on 'File'");
ExpandCollapsePattern expClickFile =
  (ExpandCollapsePattern)aeFile.GetCurrentPattern(ExpandCollapsePattern.Pattern);
expClickFile.Expand();

MenuItem-Steuerelemente, die Untermenüs enthalten, machen kein Invoke-Muster verfügbar, wie Sie dies vielleicht erwarten. Sie machen ein Expand-Muster verfügbar. Nachdem jetzt die Elemente des Untermenüs „File“ gerendert und sichtbar sind, kann ein Verweis auf den Exit-Befehl abgerufen werden:

AutomationElement aeFileExit = null;
aeFileExit = aeCryptoCalc.FindFirst(TreeScope.Descendants,
  new PropertyCondition(AutomationElement.NameProperty, "Exit"));
if (aeFileExit == null)
  throw new Exception("Could not find File-Exit");
else
  Console.WriteLine("Got File-Exit");

Nun kann ein InvokePattern für das Untermenü „Exit“ verwendet werden, um die zu testende CryptoCalc-Anwendung zu schließen:

InvokePattern ipFileExit =
  (InvokePattern)aeFileExit.GetCurrentPattern(InvokePattern.Pattern);
ipFileExit.Invoke();
Console.WriteLine("\nEnd automation\n");

Jetzt ist die Testautomatisierung abgeschlossen, und Sie können das Testfallergebnis aufzeichnen und ein anderes Testszenario starten.

Schlussbemerkung

Der Code, der im Artikel dieses Monats vorgestellt wird, bietet Ihnen eine gute Grundlage für die ersten Schritte beim Erstellen einer benutzerdefinierten Testautomatisierung für WPF-Anwendungen. Die MUIA-Bibliothek ist recht umfassend und kann die einfachsten Testszenarios abdecken.

Das Muster, mit dem das hier vorgeführte einfache Beispiel für das Testen Ihrer eigenen WPF-Anwendung angepasst werden kann, ist ziemlich einfach. Versuchen Sie, beim Erstellen Ihrer WPF-Anwendung sicherzustellen, dass alle Steuerelemente ein XAML-Name-Attribut besitzen, sodass ein AutomationID generiert wird. Verwenden Sie das UISpy-Tool, um zu bestimmen, wie Sie Benutzersteuerelemente identifizieren und ändern können. Bestimmen Sie das MUIA-Muster, das Ihnen ermöglicht, den Zustand und den Wert eines Benutzersteuerelements zu untersuchen. Mit den vorliegenden Informationen können Sie die meisten grundlegenden Szenarios der Automatisierung von Benutzeroberflächentests behandeln.

In allen Testsituationen müssen Sie die Anstrengungen, die für das Erstellen Ihrer Testautomatisierung erforderlich sind, und die Vorteile, die Sie durch die Automatisierung erhalten, sorgfältig gegeneinander abwägen. Meiner Erfahrung nach eignet sich die Automatisierung von Benutzeroberflächentests in WPF im Allgemeinen am besten für Regressionstests in relativ einfachen Szenarios. Dies ermöglicht Ihnen, den Schwerpunkt der manuellen Tests auf komplexe Szenarios zu legen und neue, schwer aufspürbare Fehler zu finden, ohne befürchten zu müssen, dass ein offensichtlicher Fehler übersehen wird, der sich eingeschlichen hat, als in der Entwicklungsphase eine neue Funktionalität eingeführt wurde.

Für den Typ der einfachen Testautomatisierung, der in diesem Artikel beschrieben wird, gilt nach meiner Erfahrung als allgemeine Faustregel, dass sich die Zeitinvestition lohnt, wenn das Erstellen der Testautomatisierung weniger als vier Stunden in Anspruch nimmt. Selbstverständlich wird sich Ihre Umgebung unterscheiden. Der Punkt ist, dass Sie nicht automatisch annehmen sollten, dass diese Automatisierung von Benutzeroberflächentests immer die bestmögliche Verwendung Ihrer Testressourcen darstellt. WPF ist nach wie vor eine relativ neue Technologie. Aber mit der zunehmenden Zahl von WPF-Anwendungen werden die Verfahren zur Automatisierung von Benutzeroberflächentests, die in diesem Artikel behandelt werden, beim Erstellen besserer Software wahrscheinlich immer nützlicher werden.

Dr. James McCaffrey arbeitet für Volt Information Sciences Inc. und organisiert technische Schulungen für Softwareentwickler auf dem Campus von Microsoft in Redmond, Washington. Er hat an verschiedenen Microsoft-Produkten mitgearbeitet, unter anderem an Internet Explorer und MSN Search. James McCaffrey ist der Autor von .NET Test Automation Recipes (Apress, 2006). Sie erreichen ihn unter jmccaffrey@volt.com oder v-jammc@microsoft.com.