Modellbasiertes Testen

Eine Einführung in modellbasiertes Testen und Spec Explorer

Sergio Mera
Yiming Cao

Für die Entwicklung hochqualitativer Software ist umfassendes Testen erforderlich. Dies ist möglicherweise eine der teuersten und intensivsten Phasen der Softwareentwicklung. Wir haben schon viele Herangehensweisen zum Verbessern der Testzuverlässigkeit und -effektivität gesehen, von den einfachsten funktionalen Blackboxtests bis hin zu schwerfälligen Methoden mit Theorembeweisern und formalen Anforderungsspezifikationen. Trotz alledem wird beim Testen nicht immer mit der nötigen Gründlichkeit vorgegangen. Häufig fehlen Disziplin und Methodik vollständig.

Microsoft wendet seit über einem Jahrzehnt das modellbasierte Testen (MBT) in internen Entwicklungsprozessen erfolgreich an. MBT ist eine bewährte, erfolgreiche Technik für viele interne und externe Softwareprodukte. Der Einsatz ist in den letzten Jahren kontinuierlich gestiegen. Modellbasiertes Testen wurde von der Testcommunity relativ gut aufgenommen, besonders im Vergleich zu anderen Methoden, die im Testspektrum eher auf der „formalen“ Seite anzusiedeln sind.

Spec Explorer ist ein Microsoft MBT-Tool, das Visual Studio erweitert. Es bietet eine umfassend integrierte Entwicklungsumgebung zum Erstellen von Verhaltensmodellen. Außerdem enthält es ein grafisches Analysetool zum Überprüfen der Gültigkeit dieser Modelle und zum Erzeugen von Testfällen. Wir denken, dass dieses Tool für die Anwendung von MBT als effektive Technik in der IT-Branche entscheidend war und die natürliche Lernkurve reduzierte sowie eine moderne Entwicklungsumgebung bereitstellte.

In diesem Artikel erhalten Sie eine allgemeine Übersicht über die Hauptkonzepte von MBT und Spec Explorer. Dabei wird Spec Explorer über eine Fallstudie präsentiert, um die wesentlichen Features zu zeigen. Wir verstehen diesen Artikel auch als Sammlung praktischer Faustregeln, anhand derer Sie abwägen können, wann MBT für ein bestimmtes Testproblem als Qualitätssicherungsmethode in Betracht kommt. Sie sollten MBT nicht einfach blindlings drauflos in jedem Testszenario verwenden. Häufig ist eine andere Technik (zum Beispiel traditionelles Testen) möglicherweise die bessere Wahl.

Testfähiges Modell in Spec Explorer

Auch wenn verschiedene MBT-Tools unterschiedliche Funktionen bieten und gelegentlich leichte konzeptuelle Unterschiede aufweisen, besteht im Allgemeinen Übereinstimmung über die Bedeutung des Ausführens modellbasierter Tests. Im Grunde genommen geht es darum, automatische Testverfahren aus Modellen abzuleiten.

Modelle werden normalerweise manuell erstellt und umfassen Systemanforderungen und erwartetes Verhalten. Im Fall von Spec Explorer werden Testfälle aus einem zustandsorientierten Modell automatisch generiert. Sie enthalten Testsequenzen sowie das Testorakel. Es werden Testsequenzen aus dem Modell abgeleitet, die dafür sorgen, dass das getestete System (GS) verschiedene Zustände erreicht. Im Testorakel wird die Entwicklung des GS nachverfolgt, und es wird bestimmt, ob das System dem im Modell angegebenen Verhalten entspricht. Dabei wird ein Urteil abgegeben.

Das Modell ist einer der Hauptbestandteile eines Spec Explorer-Projekts. Es wird in einem Konstrukt namens „Modellprogramme“ angegeben. Sie können in jeder beliebigen .NET-Sprache Modellprogramme schreiben (wie in C#). Sie bestehen aus einem Satz an Regeln, die mit einem definierten Zustand interagieren. Modellprogramme werden mit einer Skriptsprache mit dem Namen „Cord“ kombiniert, dem zweiten Kernstück eines Spec Explorer-Projekts. Dadurch können Verhaltensbeschreibungen angegeben werden, die konfigurieren, wie das Modell exploriert und getestet wird. Über die Kombination des Modellprogramms und des Cord-Skripts wird für das GS ein testfähiges Modell erstellt.

Den dritten wichtigen Bestandteil im Spec Explorer-Projekt bildet natürlich das GS. Zum Generieren des Testcodes muss das GS für Spec Explorer bereitgestellt werden (dies entspricht dem Standardmodus für Spec Explorer), weil der generierte Code direkt aus dem testfähigen Modell ohne Interaktion mit dem GS abgeleitet wird. Sie können Testfälle „offline“ ausführen, die von der Modellevaluierung und den Generierungsphasen der Testfälle entkoppelt sind. Wenn das GS jedoch bereitgestellt wird, kann in Spec Explorer überprüft werden, ob die Bindungen aus dem Modell für die Implementierung eindeutig definiert sind.

Fallstudie: Ein Chatsystem

Im folgenden Beispiel wird gezeigt, wie Sie ein testfähiges Modell in Spec Explorer erstellen können. Das GS ist in diesem Fall ein einfaches Chatsystem mit einem einzigen Chatraum, in dem sich die Benutzer an- und abmelden können. Ein angemeldeter Benutzer kann die Liste aller angemeldeten Benutzer anfordern und an alle Benutzer Broadcastmeldungen senden. Die Anforderungen werden vom Chatserver immer bestätigt. Anforderungen und Antworten verhalten sich zueinander asynchron, sie können also vermischt werden. Mehrere von einem Benutzer gesendete Nachrichten werden wie erwartet in der entsprechenden Reihenfolge empfangen.

Einer der Vorteile der Verwendung von MBT besteht darin, dass Sie im Vorfeld wertvolles Feedback hinsichtlich der Anforderungen erhalten können, weil Sie gezwungen sind, das Verhaltensmodell zu formalisieren. Zweideutigkeiten, Widersprüche und fehlender Kontext können zu einem frühen Zeitpunkt erkannt werden. Folglich ist es wichtig, präzise vorzugehen und die Systemanforderungen zu formalisieren, etwa so:

R1. Users must receive a response for a logon request.
R2. Users must receive a response for a logoff request.
R3. Users must receive a response for a list request.
R4. List response must contain the list of logged-on users.
R5. All logged-on users must receive a broadcast message.
R6. Messages from one sender must be received in order.

Spec Explorer-Projekte verwenden Aktionen, um Interaktionen mit dem GS vom Standpunkt des Testsystems zu beschreiben. Es kann sich um folgende Aktionen handeln: 1) Aufrufaktionen, die einen Auslöser vom Testsystem zum GS darstellen, 2) Rückgabeaktionen, die die Antwort vom GS (sofern vorhanden) erfassen, und 3) Ereignisaktionen, die vom GS gesendete autonome Nachrichten darstellen. Aufruf-/Rückgabeaktionen sind blockierende Vorgänge und werden daher im GS mit einer einzelnen Methode dargestellt. Dies sind die Standardaktionsdeklarationen, wobei mit dem Schlüsselwort „event“ eine Event-Aktion deklariert wird. Abbildung 1 zeigt die Darstellung im Chatsystem.

Abbildung 1: Aktionsdeklarationen

// Cord code
config ChatConfig
{
  action void LogonRequest(int user);
  action event void LogonResponse(int user);
  action void LogoffRequest(int user);
  action event void LogoffResponse(int user);
  action void ListRequest(int user);
  action event void ListResponse(int user, Set<int> userList);
  action void BroadcastRequest(int senderUser, string message);
  action void BroadcastAck(int receiverUser, 
    int senderUser, string message);
  // ...
}

Nachdem die Aktionen deklariert wurden, besteht nun der nächste Schritt darin, das Systemverhalten zu definieren. In diesem Beispiel wird C# für die Modellbeschreibung verwendet. Der Systemzustand wird mit Klassenfeldern modelliert, und Zustandsübergänge werden mit Regelmethoden modelliert. Regelmethoden bestimmen die Schritte, die Sie vom aktuellen Zustand im Modellprogramm durchführen können. Außerdem legen sie fest, wie der Zustand bei jedem Schritt aktualisiert wird.

Da dieses Chatsystem in erster Linie aus der Interaktion zwischen Benutzern und dem System besteht, ist der Modellzustand lediglich eine Sammlung von Benutzern mit ihrem Zustand (siehe Abbildung 2).

Abbildung 2: Der Modellzustand

/// <summary>
/// A model of the MS-CHAT sample.
/// </summary>
public static class Model
{
  /// <summary>
  /// State of the user.
  /// </summary>
  enum UserState
  {
    WaitingForLogon,
    LoggedOn,
    WaitingForList,
    WatingForLogoff,
  }
  /// <summary>
  /// A class representing a user
  /// </summary>
  partial class User
  {
    /// <summary>
    /// The state in which the user currently is.
    /// </summary>
    internal UserState state;
    /// <summary>
    /// The broadcast messages that are waiting for delivery to this user.
    /// This is a map indexed by the user who broadcasted the message,
    /// mapping into a sequence of broadcast messages from this same user.
    /// </summary>
    internal MapContainer<int, Sequence<string>> waitingForDelivery =
      new MapContainer<int,Sequence<string>>();
  }             
    /// <summary>
    /// A mapping from logged-on users to their associated data.
    /// </summary>
    static MapContainer<int, User> users = new MapContainer<int,User>();
      // ...   
  }

Sie sehen, dass sich das Definieren eines Modellzustands nur unwesentlich vom Definieren einer normalen C#-Klasse unterscheidet. Regelmethoden sind C#-Methoden zum Beschreiben des Zustands, in dem eine Aktion aktiviert werden kann. In diesem Fall wird auch die Art des Updates beschrieben, das auf den Modellzustand angewendet wird. Hier dient „LogonRequest“ als anschauliches Beispiel dafür, wie eine Regelmethode geschrieben wird:

[Rule]
static void LogonRequest(int userId)
{
  Condition.IsTrue(!users.ContainsKey(userId));
  User user = new User();
  user.state = UserState.WaitingForLogon;
  user.waitingForDelivery = new MapContainer<int, Sequence<string>>();
  users[userId] = user;
}

Mit dieser Methode werden die Aktivierungsbedingung und die UPDATE-Regel für die Aktion „LogonRequest“ beschrieben. Dies wurde vorher im Cord-Code deklariert. Im Wesentlichen besagt die Regel Folgendes:

  • Die LogonRequest-Aktion kann ausgeführt werden, wenn die Eingangs-userId noch nicht im aktuellen Benutzersatz vorhanden ist. „Condition.Is­True“ ist eine API, die von Spec Explorer zum Angeben einer Aktivierungsbedingung bereitgestellt wird.
  • Ist die Bedingung erfüllt, wird ein neues Benutzerobjekt erstellt, dessen Zustand ordnungsgemäß initialisiert wird. Dies wird anschließend der Sammlung globaler Benutzer hinzugefügt. Dies ist der UPDATE-Teil der Regel.

Zu diesem Zeitpunkt ist die meiste Modellierarbeit abgeschlossen. Jetzt definieren wir ein paar „Maschinen“, damit wir das Verhalten das Systems explorieren können und eine visuelle Darstellung erhalten. In Spec Explorer stellen Maschinen Explorationseinheiten dar. Eine Maschine besitzt einen Namen und ein Verhalten, die in der Cord-Sprache definiert sind. Sie können eine Maschine auch mit anderen kombinieren, um ein komplexeres Verhalten zu erzielen. Betrachten wir ein paar Beispielmaschinen für das Chatmodell:

machine ModelProgram() : Actions
{
  construct model program from Actions where scope = "Chat.Model"
}

Die erste Maschine, die wir definieren, ist eine sogenannte „model program“-Maschine. Sie weist Spec Explorer mithilfe der Direktive „construct model program“ an, das gesamte Verhalten des Modells auf Grundlage der Regelmethoden zu explorieren, die im Chat.Model-Namespace gefunden werden:

machine BroadcastOrderedScenario() : Actions
{
  (LogonRequest({1..2}); LogonResponse){2};
  BroadcastRequest(1, "1a");
  BroadcastRequest(1, "1b");
  (BroadcastAck)*
}

Bei der zweiten Maschine handelt es sich um ein „scenario“, ein Aktionsmuster, das wie reguläre Ausdrücke definiert wird. Szenarien werden normalerweise mit einer „model program“-Maschine“ kombiniert, um das gesamte Verhalten folgendermaßen zu segmentieren:

machine BroadcastOrderedSlice() : Actions
{
  BroadcastOrderedScenario || ModelProgram
}

Der Operator „||“ erstellt zwischen den beiden teilnehmenden Maschinen eine „synchronisierte parallele Kombination“. Das resultierende Verhalten enthält nur die Schritte, die auf beiden Maschinen synchronisiert werden können („synchronisiert“ bedeutet hier, dass die gleiche Aktion mit der gleichen Argumentliste vorhanden ist). Die Exploration dieser Maschine ergibt das in Abbildung 3 gezeigte Diagramm.

Composing Two Machines
Abbildung 3: Kombinieren von zwei Maschinen

Wie Sie im Diagramm in Abbildung 3 sehen können, entspricht das kombinierte Verhalten der Szenariomaschine und dem Modellprogramm. Dabei handelt es sich um eine leistungsstarke Technik zum Abrufen einer einfachen Teilmenge eines komplexen Verhaltens. Wenn Ihr System zudem über unendlichen Zustandsraum verfügt (wie im Fall des Chatsystems), kann das Segmentieren des gesamten Verhaltens eine begrenzte Teilmenge generieren, die für Testzwecke besser geeignet ist.

Analysieren wir die unterschiedlichen Entitäten in diesem Diagramm. Circle-Zustände sind kontrollierbare Zustände. In diesen Zuständen werden dem GS Auslöser bereitgestellt. Diamond-Zustände sind beobachtbare Zustände. In diesen Zuständen werden ein oder mehrere Ereignisse vom GS erwartet. Das Testorakel (die erwarteten Testergebnisse) ist bereits im Diagramm mit Ereignisschritten und ihren Argumenten codiert. Zustände mit mehreren ausgehenden Ereignisschritten werden als nicht deterministische Zustände bezeichnet, da das zur Ausführungszeit vom GS bereitgestellte Ereignis zur Modellierzeit nicht „determiniert“ wird. Beachten Sie, dass das Explorationsdiagramm in Abbildung 3 mehrere nicht deterministische Zustände enthält: S19, S20, S22 usw.

Das explorierte Diagramm dient dem Verständnis des Systems. Jedoch ist es noch nicht für das Testen geeignet, weil es nicht der „Test Normal Form“ entspricht. Wir bezeichnen ein Verhalten als „Test Normal Form“, wenn es keinen Zustand enthält, der mehr als einen ausgehenden Aufrufrückgabeschritt enthält. Im Diagramm in Abbildung 3 erkennen Sie, dass S0 diese Regel offensichtlich verletzt. Um ein derartiges Verhalten in „Test Normal Form“ umzuwandeln, können Sie mithilfe der Testfallkonstruktion einfach eine neue Maschine erstellen:

machine TestSuite() : Actions where TestEnabled = true
{
  construct test cases where AllowUndeterminedCoverage = true
  for BroadcastOrderedSlice
}

Dadurch wird ein neues Verhalten generiert, indem das ursprüngliche Verhalten durchlaufen wird und Ablaufverfolgungen in „Test Normal Form“ generiert werden. Das Traversierungskriterium ist die Randabdeckung. Jeder Schritt im ursprünglichen Verhalten wird mindestens einmal abgedeckt. Im Diagramm in Abbildung 4 sehen Sie das Verhalten nach einer derartigen Traversierung.

Generating New Behavior
Abbildung 4: Generieren von neuem Verhalten

Um „Test Normal Form“ zu erreichen, werden Zustände mit mehreren Aufrufrückgabeschritten in einen pro Schritt aufgeteilt. Ereignisschritte werden niemals unterteilt und immer vollständig beibehalten, weil Ereignisse vom GS zur Laufzeit ausgewählt werden können. Testfälle müssen jede mögliche Wahl verarbeiten können.

Spec Explorer kann Testsammlungscode aus einem „Test Normal Form“-Verhalten generieren. Die Standardform von generiertem Testcode ist ein Visual Studio-Komponententest. Sie können eine solche Testsammlung direkt mit den Testtools in Visual Studio oder mit dem Befehlszeilentool „mstest.exe“ ausführen. Der generierte Testcode ist für Menschen lesbar und kann leicht debuggt werden:

#region Test Starting in S0
[Microsoft.VisualStudio.TestTools.UnitTesting.TestMethodAttribute()]
public void TestSuiteS0() {
  this.Manager.BeginTest("TestSuiteS0");
  this.Manager.Comment("reaching state \'S0\'");
  this.Manager.Comment("executing step \'call LogonRequest(2)\'");
  Chat.Adapter.ChatAdapter.LogonRequest(2);
  this.Manager.Comment("reaching state \'S1\'");
  this.Manager.Comment("checking step \'return LogonRequest\'");
  this.Manager.Comment("reaching state \'S4\'");
  // ...
    }

Der Testcode-Generator kann umfassend angepasst und konfiguriert werden, um Testfälle für andere Testframeworks wie NUnit zu generieren.

Das vollständige Chatmodell ist im Spec Explorer-Installer enthalten.

Wann lohnt sich MBT?

Das modellbasierte Testen hat seine Vor- und Nachteile. Offensichtlich von Vorteil ist die Möglichkeit, Testfälle durch einen Tastendruck zu generieren, nachdem das testfähige Modell fertig gestellt wurde. Darüber hinaus entstehen durch die Vorabformalisierung des Modells weitere Vorteile: Anforderungsinkonsistenzen werden früh erkannt, und das Team kann sich auf das erwartete Verhalten einstellen. Beim Schreiben manueller Testfälle gibt es auch ein „Modell“, es ist jedoch nicht formalisiert und besteht nur im Kopf des Testers. MBT zwingt das Testteam, seine Erwartungen an das Testsystem deutlich zu kommunizieren und in einer klar strukturierten Form aufzuschreiben.

Ein weiterer deutlicher Vorteil besteht darin, dass die Projektverwaltung weniger aufwendig ist. Änderungen am Systemverhalten oder neu hinzugefügte Features werden durch eine Aktualisierung des Modells wiedergegeben. Dies ist normalerweise einfacher als das individuelle Ändern manueller Testfälle. Das Identifizieren derjenigen Testfälle, die geändert werden müssen, kann zeitaufwendig sein. Beachten Sie auch, dass die Modellerstellung unabhängig von der Implementierung oder dem tatsächlichen Testen stattfindet. Das bedeutet, dass verschiedene Mitglieder eines Teams zur gleichen Zeit an unterschiedlichen Aufgaben arbeiten können.

Ein Nachteil besteht jedoch darin, dass die Denkweise häufig geändert werden muss. Dies ist wahrscheinlich eine der Hauptherausforderungen dieser Technik. Neben dem bekannten Problem, dass IT-Mitarbeiter keine Zeit haben, neue Tools auszuprobieren, ist die Lernkurve zum Verwenden dieser Technik nicht zu vernachlässigen. Je nach Team können durch die Umsetzung von MBT auch einige Prozessveränderungen erforderlich sein, wodurch möglicherweise auch ein Pushback generiert wird.

Von Nachteil ist es weiterhin, dass Sie mehr Arbeit vorab leisten müssen. Folglich dauert es im Vergleich zu traditionellen, manuell geschriebenen Testfällen länger, bis der erste Testfall generiert ist. Zusätzlich muss das Testprojekt komplex genug sein, um die Investition zu rechtfertigen.

Glücklicherweise gibt es einige Faustregeln, mit denen ermittelt werden kann, wann sich MBT wirklich lohnt. Ein erstes Kriterium ist das Vorhandensein eines unendlichen Satzes an Systemzuständen mit Anforderungen, die unterschiedlich erfüllt werden können. Ein reaktives oder verteiltes System oder ein System mit asynchronen oder nicht deterministischen Interaktionen sind ein weiteres Kriterium. Letztlich können Methoden mit vielen komplexen Parametern auf einen MBT-Einsatz hinweisen.

Wenn diese Bedingungen erfüllt sind, kann MBT einen großen Unterschied ausmachen und den Testaufwand erheblich reduzieren. Microsoft Blueline ist ein Beispiel hierfür. In diesem Projekt wurden Hunderte von Protokollen als Teil der Commplianceinitiative für Windows-Protokolle überprüft. Dabei haben wir Spec Explorer verwendet, um die technische Richtigkeit der Protokolldokumentation hinsichtlich des tatsächlichen Protokollverhaltens zu überprüfen. Der Aufwand war immens, und Microsoft hat etwa 250 Personenjahre an Tests aufgewendet. Microsoft Research hat eine statistische Studie überprüft, nach der Microsoft durch die Verwendung von MBT im Vergleich mit einer traditionellen Testmethode 50 Personenjahre an Tests bzw. etwa 40 Prozent des Aufwands gespart hat.  

Beim modellbasierten Testen handelt es sich um eine leistungsfähige Technik, die traditionelle Techniken mit einer systematischen Methode bereichert. Spec Explorer ist ein ausgereiftes Tool, das die MBT-Konzepte in einer umfassend integrierten, topaktuellen Entwicklungsumgebung als kostenloses Visual Studio Power Tool nutzt.

Yiming Cao arbeitet als Senior Development Lead für das Microsoft Interop and Tools-Team. Er beschäftigt sich mit dem Protocol Engineering Framework (einschließlich Microsoft Message Analyzer) und Spec Explorer. Bevor er zu Microsoft kam, arbeitete er für IBM Corp. an der Suite für Unternehmenszusammenarbeit und schloss sich dann einem Startupunternehmen an, das im Bereich Medienstreaming tätig war.

Sergio Mera arbeitet als Senior Program Manager für das Microsoft Interop and Tools-Team. Er beschäftigt sich mit dem Protocol Engineering Framework (einschließlich Microsoft Message Analyzer) und Spec Explorer. Vor seiner Zeit bei Microsoft war er als Wissenschaftler und Dozent am Fachbereich Informatik der University of Buenos Aires tätig und befasste sich mit Modallogik und automatisierten Theorembeweisern.

Unser Dank gilt dem folgenden technischen Experten für die Durchsicht dieses Artikels: Nico Kicillof (Microsoft)
Nico Kicillof (nicok@microsoft.com) ist Lead Program Manager für Windows Phone-Buildarchitektur und Entwicklungstools. Er erstellt Tools und Methoden, mit denen Techniker Softwareprodukte erstellen, testen und pflegen können. Vor seiner Tätigkeit für Microsoft war er Professor und stellvertretender Leiter des Fachbereichs Informatik der University of Buenos Aires.