Friday, April 11, 2008

mlUnit 2.0 Beta

Vor ein paar Tagen habe ich die neueste Version von mlUnit (genannt "2.0-beta1") bei SourceForge veröffentlicht. mlUnit 2 ist die Re-Implementierung von mlUnit mit der neuen objektorientierter Syntax von MATLAB R2008a. Daraus ergeben sich drei wesentliche Unterschiede zur alten Version: (1) man kann mehrere Testmethoden einer Testfallklasse nun in einer einzelnen Datei definieren; (2) man kann mehrere solcher Testfallklassen in einem Paket mit einem eigenen Namensraum zusammenfassen; (3) man benötigt zwingend MATLAB R2008a oder neuer.

Die Installation erfolgt durch den Download des bereitgestellten Archivs, das Entpacken in einen beliebigen Ordner und dem Hinzufügen des Quellverzeichnisses zum MATLAB-Pfad:

>> addpath('$MLUNIT\mlunit2\src');

Mit $MLUNIT wird hierbei das Rootverzeichnis von mlUnit 2 bezeichnet.

Ein Beispieltestfall mit mlUnit 2 sieht wie folgt aus:

>> edit test.sin.m 
    classdef test_sin < mlunit.test_case 
        methods 
            function self = test_sin(varargin) 
                self = self@mlunit.test_case(varargin{:}); 
            end 
 
            function self = test_null(self) 
                mlunit.assert_equals(0, sin(0)); 
            end 
 
            function self = test_sin_cos(self) 
                mlunit.assert_equals(cos(0), sin(pi/2)); 
            end 
        end 
    end

Es wird die Testfallklasse test_sin erstellt, die von der Basisklasse test_case abgeleitet ist, und über zwei Testmethoden verfügt: test_null und test_sin_cos. Wie man sieht, sind die Klassen und Methoden von mlUnit selbst nun in dem gleichnamigen Paket gekapselt. Dadurch werden Konflikte mit MATLAB-eigenen Methoden verhindert.

Der Testfall wird wie folgt ausgeführt:

>> runner = mlunit.text_test_runner(1, 1); 
>> loader = mlunit.test_loader; 
>> run(runner, load_tests_from_test_case(loader, 'test_sin')); 

Wie auch schon bei mlUnit 1.5 werden alle Testfälle, die während der Entwicklung von mlUnit entstanden sind, mitgeliefert. In der neuen Version sind diese im Paket mlunit_test enthalten:

>> addpath('$MLUNIT\mlunit2\test');
>> mlunit_test.run;

Zum Schluss noch ein Hinweis in eigener Sache: mlUnit 2.0-beta1 ist ein Beta-Release. Bitte melden Sie Fehler per E-Mail an thomas@dohmke.de (am besten "mlunit" im Betreff verwenden), hinterlassen Sie einen Kommentar zu diesem Artikel oder verwenden Sie den Bugtracker des Projekts. Danke.

Wednesday, March 5, 2008

Objektorientierte Programmierung mit MATLAB R2008a

Wie ich schon in meinem letzten Beitrag schrieb, wurde in der neuen MATLAB-Version R2008a die Art und Weise der objektorientierten Programmierung mit der Programmiersprache m verändert. Dazu wurde eine völlig neue Syntax definiert, die im folgenden vorgestellt werden soll.

Bis Version R2007b war es notwendig, für eine Klasse ein neues Verzeichnis zu erzeugen, dessen Name mit einem @ beginnt. Diese Vorgehensweise kann auch weiterhin verwendet werden, und zwar immer dann, wenn die Methoden der Klasse auf mehrere Dateien verteilt werden soll. Ansonsten kann eine Klasse nun auch in einer einzelnen Datei untergebracht haben, die nach dem Bezeichner der Klasse benannt wird. Als Beispiel soll eine Klasse für ein Testergebnis dienen. Der Name der Klasse ist "test_result", dementsprechend wird eine Datei mit dem Namen "test_result.m" erstellt:

classdef test_result
end

Die Anweisung "classdef" leitet die neue Klasse ein, es folgt der Name des Klasse und in üblicher MATLAB-Manier wird der Block mit einem "end" beendet. Mit dieser simplen Konstruktion ist die Klasse bereits funktionsfähig und kann auf der Kommandozeile instanziiert werden (vorausgesetzt, die Datei "test_result.m" ist entweder im aktuellen Verzeichnis oder im MATLAB-Pfad auffindbar):

>> result = test_result	

result = 

test_result with no properties.
list of methods

Wie MATLAB richtig bemerkt, hat die Klasse noch keine Eigenschaften. Das Wort "methods" wird hingegen als Link dargestellt und klickt man darauf, erscheint folgende Ausgabe mit dem Standardkonstruktor der Klasse:

Methods for class test_result:

test_result  

Zur Definition einer Eigenschaft wurde der Block "properties" eingeführt, für Methoden der Block "methods":

classdef test_result
    properties
        should_stop = 0;
    end
    
    methods
        function should_stop = get_should_stop(self)
            should_stop = self.should_stop;
        end
    end
end

Im Beispiel wird die Eigenschaft "should_stop" definiert, die innerhalb des Testergebnisses anzeigt, dass die Testausführung beendet werden soll. Auf die Eigenschaft kann mittels der Methode "get_should_stop" zugegriffen werden.

Wie schon in R2007b und allen vorherigen Versionen, müssen bei der Veränderung einer Klasse alle Objekte dieser Klasse aus dem Workspace entfernt werden. Immerhin reicht aber jetzt ein einfaches "clear" statt dem bisherigen "clear all":

>> clear
>> result = test_result

result = 

test_result

properties:
    should_stop: 0

list of methods

Im Gegensatz zur alten Programmierweise ist es nun auch erlaubt, direkt auf die Eigenschaften zuzugreifen:

>> result.should_stop

ans =

     0

Soll ein solcher Zugriff unterbunden werden, kann der "properties"-Block mit einem zusätzlichen Attribut versehen werden:

classdef test_result
    properties (GetAccess = private)
        should_stop = 0;
    end
    
    methods
        function should_stop = get_should_stop(self)
            should_stop = self.should_stop;
        end
    end
end

Damit wird der Zugriff auf "should_stop" als privat deklariert, alternativ ist auch das Schlüsselwort "protected" erlaubt, welches den Zugriff auf ein Paket beschränkt (siehe unten). Das ganze führt zu einem interessanten Effekt:

>> clear
>> result = test_result

result = 

test_result with no properties.
list of methods

Laut dieser Aussage hat test_result nun keine Eigenschaften mehr. Eine Lesezugriff auf "should_stop" ist nur noch über die definierte Methode möglich:

>> result.should_stop
??? Getting the 'should_stop' property of the 'test_result' class is not allowed.

>> result.get_should_stop

ans =

     0

Ein Schreibzugriff ist aber weiterhin möglich:

>> result.should_stop = 1;
>> result.get_should_stop

ans =

     1

Soll auch dieser verhindert werden, muss auch das Attribut "SetAccess" entsprechend auf "private" gesetzt werden.

classdef test_result
    properties (GetAccess = private, SetAccess = private)
        should_stop = 0;
    end
    
    methods
        function should_stop = get_should_stop(self)
            should_stop = self.should_stop;
        end
    end
end

Neben "GetAccess" und "SetAccess" existieren diverse weitere Attribute, u.a. um Eigenschaften als Konstanten zu definieren (sprich als statische Eigenschaften) oder um diese von der Serialisierung auszuschließen. Will man Eigenschaften mit verschiedenen Attributen auszeichnen, fügt man einfach mehrere "properties"-Blöcke ein:

classdef test_result
    properties
      errors = {};
    end
    properties (GetAccess = private, SetAccess = private)
        should_stop = 0;
    end
    
    methods
        function should_stop = get_should_stop(self)
            should_stop = self.should_stop;
        end
    end
end

Ferner ist es möglich, spezielle Methoden für den Zugriff auf eine Eigenschaft zu erstellen. Dies ist insbesondere dann sinnvoll, wenn vor dem Zuweisen oder Lesen weitere Schritte ausgeführt werden sollen, beispielsweise eine Prüfung des Wertebereichs.

Auch bei Methoden hat sich einiges getan. Oben wurde bereits auf die Methode "get_should_stop" mit der neuen Syntax verwendet, es ist aber auch weiterhin die alte Variante möglich:

>> get_should_stop(result); % alt
>> result.get_should_stop;  % neu

Ebenfalls im "method"-Block kann der Konstruktor der Klasse definiert werden, wobei die gleiche Syntax wie bei einer normalen Funktion verwendet wird. Der Name der Funktion muss dem Namen der Klasse entsprechen, der Rückgabewert muss stets eine Instanz der Klasse sein.

Will man explizit eine Methode der jeweiligen Superklasse aufrufen, d.h. einer Klasse, von der die verwendete Klasse per Vererbung abgeleitet ist, muss dem Namen der Methode ein @ und dann der Name der Superklasse folgenden. Beispiel:

classdef text_test_result < test_result
    methods
        function should_stop = get_should_stop(self)
	    fprintf(1, 'Should stop.\n');
            should_stop = get_should_stop@test_result(self);
        end
    end
end

Die Klasse "text_test_result" wird hier von der Klasse "test_result" abgeleitet. Die Methode "get_should_stop" gibt zunächst eine Meldung auf der Standardausgabe aus, und ruft dann selbige Methode der Superklasse auf.

Wie auch bei den Eigenschaften lassen sich neuerdings zusätzliche Attribute zu jedem "method"-Block hinzufügen, die den Zugriff auf die enthaltenen Methoden limitieren können sowie sie als statisch, abstrakt oder endgültig (MathWorks nennt das "sealed", bei Java würde man "final" schreiben) markieren.

Schließlich können Klassen in Paketen organisiert werden. Dazu muss die Klassendatei in ein Verzeichnis verschoben werden, dessen Name mit einem + beginnt und nachfolgend dem Namen des Paketes entspricht. Verschiebt man die Beispieldatei "test_result.m" in das Verzeichnis "+mlunit", welches außerdem dem MATLAB-Pfad hinzugefügt wird, so gehört die Klasse "test_result" nun dem Paket "mlunit" an. Entsprechend wird sie über folgenden Aufruf verwendet:

>> result = mlunit.test_result;

Ferner ist es möglich, ein oder mehrere Pakete innerhalb einer Methode (und nur dort) zu importieren:

function test_foobar()
    import mlunit.*;
    result = test_result();
end

Etwas merkwürdig ist jedoch, dass auch innerhalb eines Paketes dessen Name referenziert werden muss. Obiges Beispiel für die Klasse "text_test_result" wird demnach zu:

classdef text_test_result < mlunit.test_result
    methods
        function should_stop = get_should_stop(self)
	    fprintf(1, 'Should stop.\n');
            should_stop = get_should_stop@mlunit.test_result(self);
        end
    end
end

Die Referenz auf den Paketnamen innerhalb von "get_should_stop" lässt sich wiederum durch die "import"-Anweisung verhindern, während die erbende Klasse in der ersten Zeile stets den Paketnamen tragen muss (jedenfalls habe ich keine Möglichkeit gefunden, dies zu umgehen).

Damit endet dieser erste Einblick in die neue objektorientierten Programmierung mit MATLAB R2008a, der aber keinesfalls umfassend ist. Insbesondere fehlen noch die neuen "handle"-Klassen sowie die Möglichkeit, neben Eigenschaften und Methoden auch Ereignisse ("events") zu definieren. Sobald ich mir diese genauer angeschaut haben, wird es deshalb einen weiteren Artikel geben.

Auch kann man die Beispiele in diesem Artikel durchaus als Hinweis deuten, das mlUnit 2.0 in Arbeit ist... :)

Saturday, March 1, 2008

R2008a

Da habe ich wohl falsch getippt, als ich vor ein paar Tage mutmaßte, dass das neue MATLAB-Release am Montag erscheint. Zwar funktioniert der Download heute erst ab 18:00 Uhr (EST), sprich 00:00 Uhr (MEZ), aber die neuen Features werden schon auf der Seite What's New In Release 2008a vorgestellt. Highlight aus meiner Sicht sind die neuen Fähigkeiten für "state-of-the art object-oriented programming". Endlich entfällt das umständliche Definieren von Klassen in separaten Verzeichnissen und das Verteilen der Methoden auf einzelne Dateien, mit "handle classes" können Parameter als Referenz übergeben werden und der MATLAB Desktop unterstützt das Erzeugen von Klassen durch ein Template. Sobald ich die neue Version ein wenig ausprobieren konnte, werde ich einen ausführlichen Artikel darüber schreiben, und mich dann an die Arbeit für Version 2.0 von mlUnit machen. :)

Wednesday, February 27, 2008

Neues Simulink-Blog bei MathWorks

Nicht mehr lange, dann müsste, wenn es nach dem Release Scedule von MathWorks geht, das nächste MATLAB-Release R2008a erscheinen (ich tippe mal auf den 03.03.2008, sofern sie in Massachusetts nicht auch samstags arbeiten). Jedenfalls ist gestern erstmal ein neues Blog veröffentlicht worden, Seth on Simulink, das sich offenbar mit Simulink beschäftigen wird. In seinem ersten Post Welcome gibt er unter anderem einen Überblick über die kommenden Themen:

solvers, sample times, modeling, hacks, blocks, model reference, libraries, masking, custom code

"hacks" könnte ganz spannend werden, ansonsten fehlen mir ein wenig die Themen für Fortgeschrittene wie z.B. Codegenerierung. Und von Testen ist auch keine Rede...

Friday, February 15, 2008

slUnit: Der Testkontext

Textkontexte ermöglichen die Wiederverwendung von gemeinsamen Code mehrerer Testmethoden. Gewöhnlich werden sie im Zusammenhang mit der Klasse TestCase verwendet, die jeweils einen Textkontext mit den zugehörigen Testmethoden kapselt. Das Ziel eines Textkontextes ist die Vermeidung von doppeltem Code. Zudem stellt er sicher, dass die Ausführung einer Testmethode unabhängig von der Ausführung anderer Testmethoden ist, speziell auch dann, wenn die Tests auf externe Ressourcen zugreifen. Der wichtigste Teil des Testkontexts sind daher Methoden zur Vor- und Nachbereitung (zum "Aufräumen") eines Tests.

Konzept (xUnit)

Die xUnit-Familie sieht zwei Methoden für den Testkontext vor: set_up und tear_down. Der folgende Pseudocode zeigt zwei Beispieltestmethoden, die einen einfachen Schalter mit zwei Zuständen testen: Die Testmethode test_off prüft den ausgeschalteten Zustand (der Default-Zustand), die Testmethode test_on das Einschalten.

external ignition; 
var switch;
 
set_up() { 
  switch = new generic_switch(ignition); 
} 

test_off() { 
  assert_equals(false, switch.is_on()); 
} 

test_on() { 
  switch.on(); 
  assert_equals(true, switch.is_on()); 
} 

tear_down() { 
  switch.off(); 
}

Eine Dopplung der Variable switch wird hier verhindert, indem diese bereits in set_up angelegt wird. Als Beispiel einer externen Ressource wird diese Variable mit der Zündung eines Autos verbunden, die im Beispiel durch den Bezeichner ignition angedeutet wird. Da test_on den Schalter und damit die Zündung einschaltet, muss nach der Ausführung dieser Testmethode der Schalter wieder ausgeschaltet werden. Ansonsten schlägt die Ausführung von test_off fehl, wenn diese nach test_on ausgeführt wird. Das Ergebnis von test_off wäre also nicht mehr unabhängig von test_on. Folglich wird der Schalter in tear_down ausgeschaltet, wobei xUnit sicherstellt, dass diese Methode stets nach einer Testmethode und unabhängig von deren Ergebnis ausgeführt wird (also auch, wenn die Testmethode selbst mit einem Fehler beendet wird).

Realisierung (slUnit)

In slUnit werden Textkontexte durch die Aggregation von Testfällen in einen Testverbund realisiert. Damit ist es möglich, gemeinsamen Code der Testfälle innerhalb des Testverbundes zu kapseln. Die Ausgänge eines Testfalls sind dann nicht länger nur die Eingänge des Testobjekts, sondern können auch mit dem gemeinsam genutzten Code verbunden werden, dessen Ausgänge wiederum die Eingänge des Testobjekts und/oder der Testfälle sein können. Der Testverbund in der Abbildung im Artikel slUnit: Tests definieren und ausführen verwendet dieses Konzept.

Das Herstellen und Auflösen eines Textkontextes, wie er durch die Methoden set_up und tear_down erreicht wird, ist für modellbasierten Code, d.h. Simulink-Blöcke, nicht notwendig. Die einzige Ausnahme stellt die Interaktion mit der MATLAB-Umgebung dar, beispielsweise durch das Verwenden von MATLAB-Variablen in Konstanten. Die Integrität des Textkontextes kann dafür durch die von Simulink bereitgestellten Callback-Funktionen StartFcn und StopFcn sichergestellt werden, typischerweise indem in StartFcn die Parameter aus einer Datei geladen und in StopFcn die geladenen Werte zurückgesetzt werden.

Weitere Artikel in der Reihe

Fragen, Kommentare, Fehler

Bei Fragen, Kommentaren oder Fehlern bitte einfach eine E-Mail an thomas@dohmke.de senden oder einen Kommentar hier im Blog hinterlassen. Gewöhnlich antworte ich innerhalb von 24 Stunden. Spam und vergleichbarer Müll wird automatisch aussortiert, daher bitte einen aussagekräftigen Betreff, z.B. "Frage zu slUnit", verwenden.

Sunday, February 10, 2008

slUnit: Tests definieren und ausführen

Die Art und Weise der Erstellung und Ausführung von Testfällen ist eine der wesentlichen Eigenschaften eines Testing Frameworks, da sich aus ihr die grundlegende Architektur ergibt. Dieser Artikel beschreibt die Darstellung eines Testfalls in xUnit als generisches Konzept und anschließend die Realisierung dieses Konzepts in slUnit.

Konzept (xUnit)

Ein einzelner Testfall wird in xUnit durch eine Methode (Test Method) repräsentiert, deren Name mit "test" beginnt. Mittels eines solchem designierten Namens ist es einer Instanz der Klasse Test Runner möglich, alle auszuführenden Testmethoden zu lokalisieren. Alternativ können in modernen Programmiersprachen auch sogenannte Annotations für diese Kennzeichnung verwendet werden, beispielsweise wird mit Java 5 und JUnit 4 eine Testmethode wie folgt definiert:

@Test public void emptyStack() {
    stack = new Stack();
    assertTrue(stack.isEmpty());
}

Die Annotation @Test markiert hier die Methode emptyStack als eine Testmethode.

Im Allgemeinen kann eine Testmethode eine beliebige Anzahl von Zusicherungen enthalten, die nacheinander geprüft werden. Die Testmethode schlägt fehl, sobald eine der Zusicherungen falsch ist. Alle nachfolgenden Zusicherungen werden dann nicht mehr ausgeführt, da diese üblicherweise voneinander abhängig sind, z.B.:

assert_not_equals(0, foo(x));
assert(0 < (1 / foo(x)));

Zudem kann eine Testmethode auch die strukturellen Fähigkeiten der jeweiligen Programmiersprache verwenden, beispielsweise if-Verzweigungen und while-Schleifen, und als Teil dieser Zusicherungen bedingt oder zyklisch ausführen.

Typischerweise werden Testmethoden innerhalb von Klassen organisiert, da die meisten Testing Frameworks, die auf xUnit basieren, in einer objektorientieren Programmiersprache realisiert sind. Eine Klasse kapselt dabei solche Testmethoden, die sich denselben Testkontext teilen. Der Begriff Testkontext dient hier als Übersetzung des englischen Begriffes Test Fixture; er beschreibt den Kontext, den die Testfälle zur Ausführung und zur Überprüfung der Zusicherungen voraussetzen. Die Basisklasse für Testmethoden wird Test Case genannt. Instanzen dieser Klasse werden in Objekten der Klasse Test Suite zusammengefasst.

Für die Ausführung der Testfälle stellen die Mitglieder der xUnit-Familie verschiedene kommandozeilenbasierte oder grafische Anwendungen zur Verfügung, die alle von der Basisklasse Test Runner abgeleitet sind und dadurch eine einheitliche Schnittstelle realisieren. Die auszuführenden Tests können durch verschiedene Techniken ausgewählt werden:

Aufzählung (Enumeration)
Jede Testmethode wird manuell hinzugefügt, in dem der Konstruktor der Testklasse mit dem Namen der Testmethode aufgerufen und so die Klasse instanziiert wird. Damit ist es außerdem möglich, nur eine einzige Testmethode als Test auszuführen.
Auffindung (Discovery)
Die Testmethoden werden automatisch durch den Test Runner ermittelt, in dem die obige Namenskonvention oder Annotations verwendet werden.
Auswahl (Selection)
Als Ergänzung zur letzten Technik werden dem Test Runner zusätzliche Kriterien zur Auswahl der Testmethoden übergeben, so dass eine Untermenge der Tests ausgeführt wird. Solche Kriterien basieren üblicherweise auf spezifischen Annotations für die Testklassen oder Testmethoden.

Realisierung (slUnit)

Das Design von slUnit setzt diese klassenbasierte Architektur von xUnit um, in dem sowohl das Testobjekt als auch die Testfälle als Simulink-Subsysteme repräsentiert werden. Ein Subsystem für eine Testmethode wird mit einem spezifischen Mask-Type markiert, um es von anderen Subsystemen zu unterscheiden. Der Mask-Type ist ein Attribut, das Subsysteme in Simulink markiert. Man kann es mit den Annotations vergleichen, die in anderen xUnit-Frameworks verwendet werden. Neben Testmethoden werden auch die Klassen Test Case und Test Suite durch Subsysteme dargestellt. Tatsächlich gibt es zwischen diesen beiden Typen keine Unterschiede, so dass das zugehörige Subsystem nachfolgend als Testverbund (Test Composite) bezeichnet wird. Ein Testverbund beinhaltet mindestens ein weiteres Subsystem, das entweder eine Testmethode oder wiederrum ein Testverbund ist. Auch das Simulink-Modell selbst wird als Testverbund betrachtet, allerdings mit der Besonderheit, dass dieses auch das Testobjekt enthält. Die nachfolgende Abbildung zeigt die entsprechende Architektur eines slUnit-Modells.

Die Tests werden nun ausgeführt, indem das Simulink-Modell simuliert wird. Während der Simulation werden die Signale und Zustände des Modells für eine Anzahl von Zeitschritten berechnet werden. Eine einzelne Simulation entspricht der Ausführung eines einzelnen Testfalls. Da das Modell mehrere Testfälle enthält, muss sichergestellt werden, dass nur die Assert-Blöcke der jeweils aktiven Testmethode ihre Zusicherungen überprüfen. Alle anderen Assert-Blöcke sollen automatisch deaktiviert werden. Diese Funktionalität wird durch den Multiplexer-Block realisiert, der daher als das wichtigste Element des slUnit-Frameworks angesehen wird. Er aktiviert die Assert-Blöcke, die sich im Subsystem der aktiven Testmethode befinden, und deaktiviert alle anderen Assert-Blöcke. Zudem verbindet er den Ausgang der Testmethode mit dem Eingang des Testobjekts. Wie in der obigen Abbildung dargestellt, beinhaltet jeder Testverbund besitzt genau einen Multiplexer-Block. Dieser muss nicht zwangsläufig den Ausgang des Subsystems darstellen, sondern kann auch mit weiteren Subsystemen verbunden sein, die gemeinsame Funktionalitäten ("Common Code") aller vom Multiplexer berücksichtigen Testfälle beinhaltet.

Wie bereits erwähnt, wird ein einzelner Testfall durch Starten der Simulation ausgeführt. Sollen indes mehrere Testfälle ausgeführt werden, kann dies ähnlichen wie bei xUnit realisiert werden. Einerseits beinhaltet jeder Testverbund einen Button "Run All", der alle zugehörigen Testfälle ausführt. Andererseits können die Testfälle mit zusätzlichen Blöcken versehen werden, die über keine eigentliche Funktion verfügen, sondern den Testfall lediglich über den Namen des Blocks und/oder dessen Farbe markieren. Ein solcher Block stellt eine Art Annotation. Durch Doppelklick des Blocks können dann alle Testfälle ausgeführt werden, die einen gleichen Block enthalten. Damit ist eine Bildung von Gruppen von Testfällen möglich, die orthogonal zur Hierachie aus Testfällen und Testverbünden steht.

Das Testergebnis wird als Hintergrundfarbe des Subystems einer Testmethode dargestellt. Dieser Hintergrundfarbe wird von der Hintergrundfarbe und damit den Ergebnissen der Assert-Blöcke geerbt. D.h. die Hintergrundfarbe wird auf rot gesetzt, wenn mindestens eine Zusicherung fehlgeschlagen ist. In gleicher Art und Weise wird die Hintergrundfarbe des Subsystems eines Testverbunds aus den Ergebnissen aller enthaltenen Testfälle gebildet.

Weitere Artikel in der Reihe

Fragen, Kommentare, Fehler

Bei Fragen, Kommentaren oder Fehlern bitte einfach eine E-Mail an thomas@dohmke.de senden oder einen Kommentar hier im Blog hinterlassen. Gewöhnlich antworte ich innerhalb von 24 Stunden. Spam und vergleichbarer Müll wird automatisch aussortiert, daher bitte einen aussagekräftigen Betreff, z.B. "Frage zu slUnit", verwenden.

Thursday, December 27, 2007

Jahresabschluss 2007

Das Jahr 2007 neigt sich dem Ende zu. Es war relativ still zuletzt hier, in diesem, meinem MATLAB Blog. Dies war im Wesentlichen der Arbeit an meiner Dissertation geschuldet, die ich endlich fertigstellen wollte. Tatsächlich stehe ich kurz vor der Vollendung, die Arbeiten am zweiten Release Candidate sind so gut wie abgeschlossen. Wenn also alles klappt, werde ich im Januar 2008 hier wieder regelmäßig schreiben, insbesondere um diese beiden Artikelreihen fertigzustellen:

Geplant sind außerdem neue Versionen für mlUnit und slUnit, und wenn dann die Zeit noch reicht (und es nicht schon wieder Zeit für den Jahresabschluß 2008 ist), erblicken vielleicht auch noch ein oder zwei neue Projekte das Licht der Welt.

In diesem Sinne: Guten Rutsch. :)

Tuesday, September 11, 2007

slUnit: Mehrere Modelle automatisch testen, Teil II

Der erste und ursprüngliche Artikel zu diesem Thema stellte einen Ansatz dar, wie man mit slUnit mehrere Modelle automatisiert testen kann. Der Artikel schloss mit einer Aufzählung von Nachteilen, die in diesem zweiten Teil ausgebessert werden sollen.

Zunächst refactorn wir den erstellten function_test_case mit Name slunit_runner (siehe Teil I) in eine Klasse slunit_test_case um.

function self = slunit_test_case(varargin)

if (nargin == 1)
  self.sys_name = varargin{1};
end;
parent = test_case('run_test');
self = class(self, 'slunit_test_case', parent);

Als Argument erwartet der Konstruktor den Namen des zu testenden Simulink-Modells, d.h. slUnit-Testbetts. Die Funktionen set_up, tear_down und run_test werden direkt übernommen und als einzelne Dateien gespeichert. Zusätzlich kommmt eine Funktion str hinzu, die den Namen des Simulink-Modells als Namen des Testfalls ausgibt:

function s = str(self)

s = strrep(char(which(self.sys_name)), '\', '/');

Angenommen, wir haben ein slUnit-Testbett test_save_min.mdl mit mehreren Testfällen, von denen einer fehlschlägt, so können wir dieses Testbett nun wie folgt ausführen:

>> run(text_test_runner, slunit_test_case('test_save_min'));
======================================================================
FAIL: s:/temp/test_save_min.mdl
----------------------------------------------------------------------
Traceback (most recent call first):
In c:\projects\temp\slunit_runner.289\@slunit_test_case\run_test.m at line 5
In c:\projects\utils\mlunit\src\@test_case\run.m at line 38
In c:\projects\utils\mlunit\src\@text_test_runner\run.m at line 31
AssertionError: Tests of model test_save_min.mdl failed.
----------------------------------------------------------------------
Ran 1 test in 8.903s

FAILED (errors=0, failures=1)

Immerhin wird nun gegenüber der ersten Lösung schon mal der Name des Simulink-Modells hinter "FAIL:" ausgegeben.

Im nächsten Schritt soll statt der Textausgabe ein HTML-Bericht erzeugt werden. Dazu erstellen wir eine neue Klasse slunit_test_result, die von der mlUnit-Klasse test_result abgeleitet ist.

function self = slunit_test_result(filename)

self.filename = filename;
self.stream = fopen(filename, 'w+');
self.slunit_failed = 0;
result = test_result();
self = class(self, 'slunit_test_result', result);

fprintf(self.stream, '<!DOCTYPE html PUBLIC [..]>');
fprintf(self.stream, '<html xmlns=''http://www.w3.org/1999/xhtml''>');
fprintf(self.stream, '<head><title>slUnit Test Report</title></head>');
fprintf(self.stream, '<body>');
fprintf(self.stream, '<h1>slUnit Test Report</h1>');

Als Argument erwartet der Konstruktor den gewünschten Dateinamen des Berichts. Diese Datei wird dann initial erzeugt und das Handle in der Variable self.stream gespeichert. Als nächstes überschreiben wir die Methoden start_test,

function self = start_test(self, test)

self.test_result = start_test(self.test_result, test);
self.slunit_failed = 0;

fprintf(self.stream, '<hr />');
fprintf(self.stream, ...
  '<h2>Model: <a href="matlab:open_system(''%s'')">%s</a></h2>', ...
  str(test), ...
  str(test));
clk = clock();
fprintf(self.stream, '<p>Execution Time: %02d.%02d.%d %02d:%02d</p>', ...
  clk(3), clk(2), clk(1), clk(4), clk(5));

stop_test,

function self = stop_test(self, test)

self.test_result = stop_test(self.test_result, test);

fprintf(self.stream, '<h3>Result</h3>');
if (self.slunit_failed)
  fprintf(self.stream, '<p>At least one test failed.</p>');
else
  fprintf(self.stream, '<p>All test passed.</p>');
end;
fprintf(self.stream, '<hr />');

add_error,

function self = add_error(self, test, error)

self.test_result = add_error(self.test_result, test, error);
self.slunit_failed = 1;

und add_failure:

function self = add_failure(self, test, failure)

self.test_result = add_failure(self.test_result, test, failure);
self.slunit_failed = 1;

Diese Funktionen werden automatisch von der run-Funktion von test_case aufgerufen, und zwar immer dann, wenn ein Test gestartet oder gestoppt wird sowie ein Fehler aufgetreten ist. Zusätzlich wird in einer weiteren Funktion, finish, das Handle der Datei geschlossen:

function self = finish(self)

fprintf(self.stream, '');
fprintf(self.stream, '');
fclose(self.stream);

Mit Hilfe dieser Klasse können wir nun eine neue Funktion zum Ausführen der Tests schreiben, genannt slunit_test_runner:

function slunit_test_runner

result = slunit_test_result('report.html');

suite = test_suite();
suite = add_test(suite, slunit_test_case('test_save_min'));
suite = add_test(suite, slunit_test_case('test_save_max'));

[suite, result] = run(suite, result); %#ok
finish(result);
web('report.html');

Die Funktion kann direkt aus dem Editor mittels F5 ausgeführt werden, sie startet den Test der Modelle test_save_max.mdl sowie test_save_min.mdl und ruft anschließend den Bericht im MATLAB-eigenen Webbrowser auf. Die ausgeführten Modelle lassen sich per Link direkt in Simulink öffnen, und da diese nach der Ausführung der Tests gespeichert wurden, zeigt die Hintergrundfarbe der Tests innerhalb des Modells deren jeweiliges Ergebnis.

Will man das Aussehen des Berichts an die eigenen Bedürfnisse anpassen, kann man dies leicht in start_test und stop_test erreichen. Zum Beispiel wird durch folgende Modifikation von stop_test das Ergebnis mit farbigem Hintergrund ausgegeben:

function self = stop_test(self, test)

self.test_result = stop_test(self.test_result, test);

fprintf(self.stream, '<h3>Result</h3>');
if (self.slunit_failed)
    fprintf(self.stream, '<p style="background: #ffcccc;">At least one test failed.</p>');
else
    fprintf(self.stream, '<p style="background: #ccffcc;>All test passed.</p>');
end;
fprintf(self.stream, '<hr />');

Nicht so einfach ist es hingegen, das angezeigte Ergebnis von test_save_min.mdl zu verändern: "At least one test failed." Die Beschreibung "at least" deutet schon daraufhin, dass wir mit der derzeitigen Implementierung nicht genau wissen, wieviele der Tests innerhalb des slUnit-Testbetts fehlgeschlagen sind. Abhilfe schafft eine Schnittstelle zwischen slunit_test_case und slunit_test_result, aber dazu mehr in Teil III (mit dem auch alle Dateien zum Herunterladen veröffentlicht werden).

Saturday, July 14, 2007

slUnit: Zusicherungen mit dem Assert-Block

Die Zusicherung (engl.: assertion) ist das grundlegende Element für das automatisierte Testen, da durch ihre Auswertung bestimmt wird, ob ein Test fehlgeschlagen ist oder nicht. Die Auswertung erfolgt dabei üblicherweise durch den Vergleich von Erwartungswerten mit den Ausgangswerten des Testobjekts.

Konzept (xUnit)

Die meisten Frameworks der xUnit-Familie bieten eine Grundmenge an eingebauten Methoden für Zusicherungen, die sich in folgende Gruppen einteilen lassen:

Konstante Zusicherungen
Diese, im Englischen als Single Outcome Assertion bezeichnete Zusicherung, führt stets zum gleichen Ergebnis, d.h. sie verfügt über keine Eingänge, die ihr Verhalten beeinflussen. Ihr Ziel ist es, entweder einen unfertigen Test fehlschlagen zu lassen oder innerhalb des try-Zweiges eines try/catch-Blockes eine fehlende erwartete Ausnahme (engl.: exception) zu erkennen. Der einzige Parameter einer solchen Funktion ist - wie bei allen Zusicherungsmethoden - eine Fehlermeldung, die im Testergebnis gespeichert und nach der Ausführung aller Tests angezeigt wird. Beispiel:
fail('Test unvollständig.');
Variable Zusicherungen
Die variable Zusicherung verarbeitet im Gegensatz zur konstanten Zusicherung einen Parameter, der aussagt, ob die Zusicherung erfüllt wird oder nicht. Im Englischen wird von der Stated Outcome Assertion gesprochen. Weit verbreitet ist die Variante mit einem booleschen Parameter, z.B.

assert(y > foo(x));

wobei y hier den Erwartungswert und foo(x) die zu testende Funktion darstellt. Diese Zusicherung schlägt fehl, wenn das Ergebnis von foo(x) kleiner oder gleich dem Wert von y ist, d.h. der boolesche Ausdruck nicht wahr ist.

Vergleichende Zusicherungen
Diese Art von Zusicherungen (engl.: Equality Assertions) vergleichen zwei Eingangsparameter - den Erwartungswert und den Ausgangswert des Testobjekts - miteinander und werden erfüllt, wenn der definierte Vergleich zutrifft. Der Vergeichsoperator wird üblicherweise durch den Namen der Funktion spezifiziert, z.B. vergleicht

assert_equals(y, foo(x));

ob die Werte von y und foo(x) exakt gleich sind. Die Idee hinter einer solch spezialisierten Zusicherung ist, eine hilfreichere Fehlermeldung als bei den beiden anderen Arten von Zusicherungen auszugeben. Nehmen wir an, der Wert von y ist 2 und die Funktion foo(x) liefert als Ergebnis 3 zurück, so würde automatisch folgende Fehlermeldung erzeugt werden (mit einem xUnit-Framework, welches über eine deutsche Lokalisierung verfügt):

Erwarteter Wert ist <2>, aber das Ergebnis lautete <3>.

Weiterhin erlaubt ein optionaler dritter Parameter, eine Toleranz zu spezifizieren, z.B. wird die folgende Zusicherung erfüllt, wenn das Ergebnis von sqrt(2) größer oder gleich 1.40 und kleiner oder gleich 1.42 ist:

assert_equals(1.41, sqrt(2), 0.01);

Komplexere Zusicherungen können gewöhlich mit Hilfe der Konstrukte der jeweiligen Programmiersprache erzeugt werden. Das Ziel solcher benutzerdefinierten Zusicherungen ist die Vermeidung von Dopplungen (Duplikate) durch wiederverwendeten Quelltext und von verschachtelten Logikausdrücken.

Wird eine Zusicherung nicht erfüllt, so wird die Ausführung der jeweiligen Testmethode an der Stelle der Zusicherung abgebrochen, d.h. der nachfolgende Quelltext wird nicht mehr ausgeführt. Nicht abgebrochen wird hingegen die Ausführung der nachfolgenden Testmethoden, da diese mit xUnit stets unabhängig voneinander sind. Wenn alle Zusicherungen eines Tests erfüllt wurden, wird auch der Test als erfüllt betrachtet und üblicherweise durch einen grünen Fortschrittsbalken symbolisiert. Schlägt hingegen eine Zusicherung fehl, so schlägt auch der Test fehl und der Fortschrittsbalken wechselt zu einer roten Farbe. Diese Farbe wird beibehalten, unabhängig davon, ob weitere Tests erfüllt werden oder nicht. D.h. nach der Ausführung aller Tests lässt sich allein durch die Farbe des Fortschrittsbalkens ablesen, ob alle Tests erfolgreich waren (grün) oder nicht (rot). Zusammen mit einem roten Balken wird dann eine Liste von Fehlern ausgegeben, die je nach verwendeter Zusicherung den Erwartungswert, den Ausgangswert, die Zeile des Fehlers oder auch den optionalen Fehlertext enthält. Dadurch ist der Entwickler relativ leicht in der Lage, an die entsprechende Stelle im Code zu springen und das Problem zu analysieren.

Realisierung (slUnit)

Mit slUnit wird die Zusicherung durch den Assert-Block umgesetzt. Dieser hat einen booleschen Eingang vergleichbar zur Methode assert, der beschreibt ob die Zusicherung erfüllt wird - der boolesche Eingang ist wahr - oder nicht - der boolesche Eingang ist falsch. Der Zustand der Zusicherung wird durch die Hintergrundfarbe des Blocks dargestellt - grün für Erfolg, rot für Fehlschlag. Dies erlaubt einerseits die Auswertung des Tests, ohne das eine grafische Benutzeroberfläche oder eine Ausgabe auf der Kommandozeile notwendig ist, andererseits ist es so sehr einfach die fehlgeschlagenen Zusicherungen im grafischen Modell zu lokalisieren.

Im Gegensatz zu den Zusicherungen von xUnit soll der Assert-Block das zeitabhängige Verhalten seines Eingangssignals berücksichtigen. Eine Zusicherung durch den Assert-Block schlägt fehl, wenn dessen Eingangssignal den booleschen Wert falsch annimmt, und bleibt in diesem Zustand für die restliche Zeit des Tests, ohne jedoch die Testausführung anzuhalten. Dadurch sind die Zusicherungen eines slUnit-Tests unabhängig voneinander, was typisch für Simulink-basierte Test ist, da sie meist das Verhalten verschiedener Signale analysieren. Falls dennoch Zusicherungen benötigt werden, die nacheinander zu prüfen sind, kann dies durch zusätzliche Simulink-Blöcke wie beispielsweise Enabled-Subsysteme erfolgen.

Eine Erweiterung des Assert-Blocks ist der Assert-State-Change-Block - die Zusicherung eines Zustandswechsels. Der Block evaluiert, ob sein Eingangssignal während der Simulation von einem Wert x auf einen Wert y wechselt. Der Unterschied zwischen beiden Blöcken ist, dass der Assert-State-Change-Block solange nicht fehlschlagen kann, bis die Simulation beendet ist, da vorher nicht bewiesen ist, dass der Zustandswechsel nicht doch noch stattfinden wird. Die folgende Abbildung zeigt die Unterschiede zwischen beiden Blöcken anhand von verschiedenen statischen und dynamischen Eingangswerten.

Ein vergleichbarer Block zur Methode assert_equals ist in slUnit nicht enthalten, u.a. weil der Vergleich zweier zeitabhängiger Signale in den meisten Fällen ungleich komplizierter als der Fall a = b ist. Stattdessen wird mit dem Propagate-Block eine Möglichkeit zur Verfügung gestellt, das kumulierte Ergebnis aller Assert-Blöcke eines Subsystems auf dessen Hintergrundfarbe zu propagieren. D.h. schlägt eine oder mehrere Zusicherungen eines Subsystems fehl, wird die Hintergrundfarbe des Subsystem-Blocks durch den Propagate-Block rot, sind alle Zusicherungen erfüllt wird sie grün. Damit ist es möglich, benutzerdefinierte Zusicherungen zu erstellen, die auf den beiden Grundblöcken - Assert und Assert-State-Change - und allen anderen Simulink-Blöcken aufbauen können. Die nachfolgende Abbildung zeigt eine Beispielzusicherung, die prüft, ob das Eingangssignal y innerhalb der definierten Grenzen h und l liegt.

Weitere Artikel in der Reihe

Fragen, Kommentare, Fehler

Bei Fragen, Kommentaren oder Fehlern bitte einfach eine E-Mail an thomas@dohmke.de senden oder einen Kommentar hier im Blog hinterlassen. Gewöhnlich antworte ich innerhalb von 24 Stunden. Spam und vergleichbarer Müll wird automatisch aussortiert, daher bitte einen aussagekräftigen Betreff, z.B. "Frage zu slUnit", verwenden.

Friday, June 29, 2007

slUnit: Mehrere Modelle automatisch testen, Teil I

Unlängst erreichte mich die Frage, ob es mit slUnit auch möglich ist, mehr als ein Modell automatisiert zu testen. Die Antwort ist: nicht direkt. :) Während es relativ einfach ist, alle Tests eines Testbetts mittels dem "RunAll"-Button auszuführen, können nicht zwei Modelle mit den Bordmitteln von slUnit automatisch hintereinander gestartet werden. Abhilfe schafft der Befehl slunit_start, der drei Parameter akzeptiert:

  • den Namen des Subsystems,
  • einen Modus: 0 für alle Tests, 1 für eine Liste von Tests, 2 für das geöffnete Subsystem, und
  • eine Liste von Tests für die Modi 1 und 2.
Angenommen, man hat zwei Testbetten, "test_sample1.mdl" und "test_sample2.mdl", könnte man beide mitsamt aller Tests hintereinander mit folgender m-Funktion testen:
function slunit_runner

open_system('test_sample1.mdl');
slunit_start('test_sample1.mdl', 0);
close_system('test_sample1.mdl', 1);

open_system('test_sample2.mdl');
slunit_start('test_sample2.mdl', 0);
close_system('test_sample2.mdl', 1);
Dieser Ansatz hat den Nachteil, dass bei einem Fehler während der Tests des ersten Testbetts, die Tests des zweiten Testbetts niemals ausgeführt werden. Außerdem erinnert die Funktion stark an das, was sich bei xUnit Fixture nennt (sinngemäßg ins Deutsche übersetzt: Testkontext). Ein solcher Kontext beinhaltet alle für die Ausführung des Tests notwendigen Dinge und verhindert unter anderem Dopplungen im Code, in unserem Beispiel beispielsweise die Abfolge open_system, slunit_start und close_system, die bis auf den jeweils ersten Parameter identisch für beide Modelle ist. Diese Aufgabe kann mlUnit für uns erledigen, und zwar im einfachsten Fall mit einem function_test_case:
function suite = slunit_runner

   function set_up(sys)
      
       open_system(sys);

   end

   function run_test(sys)

       slunit_start(sys, 0);
       failed = slunit_get_failed(sys);
       assert_equals(0, failed, sprintf('Tests of model %s.mdl failed.', sys));

   end

   function tear_down(sys)

       save_system(sys);
       close_system(sys);
      
   end

   function handle = make_test(sys)
      
       handle = function_test_case(@() run_test(sys), ...
           @() set_up(sys), ...
           @() tear_down(sys));
      
   end

   suite = test_suite;
   suite = set_name(suite, 'slunit_runner');
   suite = add_test(suite, make_test('sample1'));
   suite = add_test(suite, make_test('sample2'));

end
Das Vorgehen ist eigentlich ganz einfach. Zunächst wird eine test_suite erzeugt, die für jedes slUnit-Testbett einen function_test_case aufnimmt. Diese werden wiederum durch die Funktion make_test erzeugt, welche die lokalen Funktionen set_up, run_test und tear_down dem Konstruktor von function_test_case übergibt. Da dieser jedoch Funktionen ohne Parameter erwartet, wir aber den Dateinamen des Modells als Parameter übergeben wollen (sonst müssten wir alle drei Funktionen für jedes Modell neu schreiben), erstellen wir mit dem @-Operator (quasi das Lambda-Kalkül unter MATLAB) drei anonyme Funktionen. Die Auswertung erfolgt über eine Zusicherung, die die Funktion slunit_get_failed benutzt, um festzustellen, ob einer oder mehrere Tests innerhalb des Testbetts fehlgeschlagen sind. Die Ausführung dieser Funktion erfolgt dann mit
>> run(text_test_runner(1, 2), slunit_runner);
Im Beispiel schlägt im Model "sample1.mdl" ein Test fehl, wodurch folgende Ausgabe erzeugt wird:
@()run_test(sys)(function_test_case) ... FAIL
@()run_test(sys)(function_test_case) ... OK

======================================================================
FAIL: @()run_test(sys)(function_test_case)
----------------------------------------------------------------------
Traceback (most recent call first):
 In s:\temp\slunit_runner\slunit_runner.m at line 13
 In s:\temp\slunit_runner\slunit_runner.m at line 26
 In s:\mlunit\src\@function_test_case\run_test.m at line 21
 In s:\mlunit\src\@test_case\run.m at line 38
 In s:\mlunit\src\@test_suite\run.m at line 24
 In s:\mlunit\src\@text_test_runner\run.m at line 31
AssertionError: Tests of model sample1.mdl failed.
----------------------------------------------------------------------
Ran 2 tests in 0.984s

FAILED (errors=0, failures=1)
Zwei Dinge sind hierbei unbefriedigend:
  1. Der Traceback ist im Kontext von slUnit nicht brauchbar. Zwar ist tatsächlich die Zusicherung in Zeile 13 fehlgeschlagen, allerdings ist dies nicht hilfreich bei der Analyse des Ergebnisses. Eine sinnvolle Angabe wäre, welche Tests innerhalb des Modells fehlgeschlagen sind.
  2. Gerade bei Batch-Tests, die automatisiert beispielsweise über Nacht ausgeführt werden, wird häufig ein Testreport erzeugt, der neben dem eigentlichen Testergebnis auch die verwendeten Test- und Ergebnisvektoren dokumentiert, beispielsweise als Plots. Erst kürzlich hat Alexander Duschau-Wicke in einem Blog-Eintrag auf diesen Mangel in slUnit hingewiesen und sehr treffend festgestellt:
    slunit [..] is clearly more intended for interactive testing [..].
    Der interaktive Test im Sinne der test-getriebenen Entwicklung war tatsächlich die Hauptmotivation für die Entwicklung von slUnit, insofern fehlt derzeit ein Reportgenerator in dessen Repertoire. Im Prinzip dient das jeweilige Modell selbst als Dokumentation, da beim Abspeichern auch die Ergebnisse aller Zusicherungen und Tests in Form der Hintergrundfarbe beibehalten werden. D.h. wenn wie oben realisiert, mehrere Modelle nacheinander simuliert werden, lässt sich im Nachhinein zuerst an der Fehlermeldung erkennen, ob ein Test fehlgeschlagen ist, und dann das entsprechende Modell öffnen, um den Test nachzuvollziehen bzw. zu wiederholen. Nichtsdestotrotz stimme ich Andreas zu, dass ein vollständiger Testreport sinnvoll ist, insbesondere wenn die Tests nicht dem Design, sondern der Verifikation dienen. Daher arbeite ich derzeit an einer Beispielimplementierung.