Softwaretests Systemtests einfach strukturieren mit Python

Python bietet mit seinen »Batteries included« schon eine Menge an Werkzeugen,mit denen ohne überflüssigen Aufwand strukturierte Systemtests implementiert werden können,anstatt das Rad neu zu erfinden. Nachfolgend geben wir Ihnen einige Anregungen, um Ihnen Appetit auf eigenes Ausprobieren zu machen.

Professionelle Softwaretests finden je nach Testobjekt und -ziel auf unterschiedlichen Ebenen statt. Neu entwickelte Software wird man zunächst auf der Unitebene auf funktionale Richtigkeit und Robustheit testen, wobei eine Unit als kleinste sinnvoll zu testende Einheit zu verstehen ist. Bei fortlaufender Integration werden (Teil-) Systemtests notwendig, deren Testfokus beispielsweise auf intermodularer Kommunikation, Use Cases und anderen liegt. Automatisierung von Systemtests findet häufig mit der objektorientierten Skriptsprache Python statt. Die Sprache ist dafür sehr gut geeignet, da sie dank des Python-spezifischen Mottos »Batteries included« eine umfangreiche und plattformunabhängige Bibliothek nützlicher Module mitbringt. Zusätzlich können über den Python-Package-Index (PIP) viele weitere Module einfach nachinstalliert werden.


Häufig werden diese Systemtests aber als eine lose Sammlung von mehr oder weniger gut strukturierten Skripten implementiert. Aber auch Testsoftware ist Software, die geplant werden muss und die eine gute Architektur und ein gutes Design verdient. Unabhängig davon, ob agil oder klassisch oder eine Mischform aus beiden betrieben wird: Testsoftware muss genauso lange gewartet und angepasst werden wie das Produkt selbst. Da sich das Produkt entweder über seinen Lebenszyklus (oder im agilen Umfeld während der Entwicklung) häufig verändert, muss die Testsoftware mit möglichst wenig Aufwand angepasst werden können. Häufig werden Bedeutung und Aufwand des Testens nach wie vor unterschätzt und der Tester erfährt häufig als letzter im Team von einer Änderung des Produktes. Hier kann Python mit seiner umfangreichen Modulbibliothek seine Stärken voll ausspielen. Das Python-Modul unittest ist ein Framework für Unittests, das jedoch genauso gut für den Systemtest geeignet ist.


Objektorientierung und vorgegebene Struktur 

Unittests organisiert die Tests in Testklassen. Diese Testklassen von einer Basistestklasse abzuleiten, und dadurch gemeinsam gebrauchte Methoden austomatisch mitzuvererben bringt einen nicht zu unterschätzenden strukturellen Vorteil mit sich. 

Unittests führt anhand der Architektur seiner Testtemplates den Tester sanft an die vorbereitende Strukturierung seiner Testklassen heran: Vor jeder Testmethode der Klasse kann das System über eine gemeinsame setUp() initialisiert und über tearDown() wieder deinitialisiert werden. Ähnliche Tests hinsichtlich der Initialisierung bzw. Herstellung der Test-Vorbedingungen können somit in derselben Testklasse implementiert werden.

Nehmen wir als Beispiel ein Steuergerät, das über eine externe Schnittstelle per Protokoll ferngesteuert werden kann. Testobjekt ist das Protokoll, das über USB oder Ethernet versendet wird. Gestaltet man die Kommunikationsklassen in Python so, dass sie ein identisches Interface haben, kann die Initialisierung der jeweiligen Schnittstelle (USB oder Ethernet) vom eigentlichen Testcase abgetrennt in die setUp() ausgelagert werden. Die (oft vergessenen) Rückstellungen der in der setUp() gemachten Testvorbereitungen werden in der tearDown() ausgeführt. Die eigentlichen Tests wie auch Vor- und Nachbereitungen müssen nur einmal implementiert und bei Änderungen auch nur einmal gewartet werden. 

Die wichtigsten Tipps 

Schnittstellen können in Python nicht explizit implementiert werden, sondern werden über Duck Typing (d. h. das Vorhandensein bestimmter Attribute und/oder Methoden) bzw. abstrakte Basisklassen angelegt. Zur Gewährleistung einer fehlerfreien Ausführung müssen die Methoden setUpClass() bzw. tearDownClass() mit dem decorator @classmethod überschrieben werden: 

class MyTestingClass(unittest.TestCase) @classmethod def setUpClass(cls, argument_1, argument_2): //Init code pass 

Einfacher Testaufruf 

Nach der Implementierung der Tests können sie ausgeführt und damit ein weiteres Mal Mehrwert aus Pythons Testframework gezogen werden. Aus der Kommandozeile kann präzise gesteuert werden, welche Tests ausgeführt werden sollen:

 > python -m unittest discover 

erlaubt das automatische rekursive Auffinden von Testmodulen und Klassen in einem Verzeichnisbaum. Beispiele für für die Ausführung von einzelnen Modulen, Klassen und Methoden sind die Folgenden:


> python -m unittest test_module1 test_module2: Ausführung von test_modul1 und test_modul2 > python -m unittest test_module.TestClass: Ausführung der TestClass in test_module > python -m unittest test_module.TestClass.test_method: Ausführung der test_method in test_module.TestClass 
 
Als Ausgabe werden die Ergebnisse der positiven Tests mit einem ».« ausgegeben. Fehlgeschlagene Tests werden mit »F« markiert und Fehler bei der Testausführung mit »E«. Auch bei vielen Tests bekommt man schnell einen Überblick über die Testergebnisse. Ausnahmen werden der Reihe nach am Ende ausgegeben. So lässt sich schnell mit der Fehleranalyse beginnen.

Sollen die Testergebnisse in einer Continuous-Integration-Lösung wie Jenkins oder SonarQube angezeigt werden, ist das Ergebnisformat xUnit meist der Standard. Über PIP lässt sich der XmlTestRunner nachinstallieren, der die Ergebnisse im xUnit Format speichert.

Während der Ausführung von Tests müssen immer wieder einzelne Tests übersprungen werden. Unittest liefert dazu unterschiedlich Lösungen. Es gibt die Möglichkeit, über Decoratoren einzelne Tests oder Testklassen zu überspringen: 

 @unittest.skip(»test class skipping“) class MyTestClass(unittest.TestCase):  @unittest.skip(»Not yet implemented“) def test_not_implemented(self): pass  @unittest.skipIf(myModule.__version__ < (1, 2), »module version not supported“ def test_module(self): pass  def test_dynamic_skip(): if some_condition: self.skip(»strange condition happened“)  @unittest.expectedFailure(»This test wil fail“) def test_expected_to_fail(): self.assertEqual(1,0) 

Trennung von Testprozedur und Eingabeparameter 

Sind für ein Projekt Usescases sauber dokumentiert, so ist es für einen erfahrenen Testingenieur ohne weiteres möglich, automatische Tests zu programmieren ohne viel Domainwissen zu haben. Dieses Domainwissen über Grenzwerte und andere Testparameter müssen aber in den Test eingebracht werden. Als Austauschformat bietet sich an dieser Stelle tatsächlich Excel an. Mit dem Modul xlrd kann relativ simpel auf Exceltabellen zugegriffen werden. Gibt der Testprogrammierer ein Format vor, wie die Testparameter in der Exceltabelle abgelegt werden müssen, kann sie eine Person mit Domainwissen dort eintragen. Die Werte werden dann zur Ausführung der Tests ausgelesen. 

Werden im Excel Parameterbereiche ausgelesen, die dann abgetestet werden müssen, empfiehlt sich die Methode subTest(). Sie erlaubt die Ausführung von Tests, die sich nur anhand von Parametern unterscheiden. Jeder einzeln ausgeführte Parametertest wird als einzelnes Ergebnis gewertet. Führt ein Parametersatz zum Testergebnis FAILED, wird die Ausführung der Methode nicht abgebrochen, sondern die weiteren Parameter durchgetestet.

Wer kein Excel verwenden möchte, kann mit den Modulen csv und collections.named_tuple() die Daten aus einer CSV-Datei mit Titelzeile auslesen. Die Spaltenbezeichnung der Titelzeile können dafür verwendet werden, eine Klasse zu erzeugen, deren Eigenschaften den Spaltenbezeichnungen entsprechen. Per csv.reader kann die CSV-Datei eingelesen werden. Als Ergebnis bekommt man eine Liste von Objekten, wobei ein Objekt pro Datenzeile erzeugt wird. Auf die Werte kann bequem über die Property-Namen zugegriffen werden. 

Übertragbarkeit durch config-Dateien 

Auch Tests müssen konfiguriert werden. Wird ein Test auf eine andere Umgebung (Betriebssystem, Hardware, ... ) übertragen, muss der Test an die veränderten Rahmenbedingungen angepasst werden. Das geschieht am besten über eine Konfigurationsdatei. Python unterstützt nativ oder über Zusatzmodule XML, JSON, YAMLoder ein dem Microsoft INI-Format sehr ähnliches Format, sodass für jeden Geschmack etwas dabei sein sollte. Oder aber man nutzt einfach das Python-Format und implementiert eine Settings-Klasse, deren Eigenschaften die Werte enthalten. Folgende Vorgehensweisen haben sich dabei bewährt:


Existiert eine Umgebungsvariable mit demselben Namen wie die Einstellung in der config-Datei, wird der Wert durch die Umgebungsgvariable überschrieben. Auf diese Weise können Continuous-Integration-Lösungen ohne lästige wiederholte Anpassung der config-Datei schnell angepasst werden.

Pfade werden werden in genau einer Klasse des Tests eingetragen. Aufeinander aufbauende Pfade werden mit den Methoden von os.path zusammengebaut, z. B. mit os.path.join(). Diese Methode fügt Pfade zusammen und fügt automatisch das für das Betriebssystem notwendige Trennzeichen (os.sep) ein. Bemerkt man bei der Ausführung, dass falsche Pfade verwendet werden, gibt es dann genau eine Stelle, an der man suchen muss. Wer Tests hat, bei denen viel mit Pfaden gearbeitet wird, dem sei das Modul pathlib empfohlen. 

Notwendigkeit aussagekräftigen Loggens


Eine wirkliche und vor allem effiziente Fehleranalyse ist nur möglich, wenn während der Testcaseausführung hilfreiche Informationen gesammelt werden. Dem liegt die Idee zugrunde, dass ein Testframework ein robustes diagnostisches Werkzeug ist (oder sein sollte), das jederzeit mit dem unbekannten Fehler an unbekannter Stelle zur unpassendsten Zeit rechnet. Hat man Dutzende von FAILED Testcases zu untersuchen, ist man für ein Logfile dankbar, das klar angibt, welche Schritte vor dem Fehlersymptom ausgeführt wurden. Hier liegt das Modul logging schon gebrauchsfertig bereit. Damit wird ein globales Singletonobjekt logger erzeugt, das von überall im gesamten Framework zugreifbar ist und dafür sorgt, dass in eine einzige Logdatei geschrieben wird und zwar immer nur von einer Partei zugleich. Die Notwendigkeit, den Zugriff auf die Logdatei zu organisieren, entfällt praktischerweise. Zudem können Format und voreingestellte Ebene des Loggings festgelegt werden. Python bietet »Debug«, »Info«, »Warning«, »Error“ und »Critical«.  

(1) setup_logging()

(2) log = logging.getLogger(__name__)

(3) log.info(»Test Nachricht«) 

Mit diesen Zeilen kann man an jeder beliebigen Stelle im Code den in Zeile (1) konfigurierten Logger holen und nutzen. Zeile (1) ist eine selbstgeschriebene Funktion. In Zeile (2) wird der Logger initialisiert, der Parameter __name__ von Python automatisch auf den Modulnamen gesetzt und dann in einer Loggingzeile miteingefügt werden. Durch Zeile (3) wird diese Nachricht mit dem Level »Info« geloggt. Das Schema sieht wie folgt aus: 

 <Uhrzeit>:<Log Level> <Modul Name>:<Log Nachricht>

zum Beispiel 

19:32:42:INFO modul_name: Log Nachricht 
 
Das Logformat einer Zeile kann sehr flexibel mit einer printf-artigen Notation konfiguriert werden. Das Loglevel kann auch während der Laufzeit jederzeit verändert und an die Bedürfnisse angepasst werden. Es ist überdies möglich, gleichzeitig auf mehrere Quellen zu schreiben, wobei das Loglevel für jede Quelle separat eingestellt werden kann. Um sich in der Logdatei zurechtzufinden ist es wichtig, den Namen des Testcases in die Logdatei zu schreiben.

def setup_logging(default_level=logging.INFO, log_file=“default.log“): “““Setup file and console handler for logging“““  log_fmt = ‚%(asctime)s:%(levelname)s %(name)s: %(message)s‘ logging.basicConfig(filename=log_file, filemode=‘w‘, level=logging.DEBUG, format=log_fmt, datefmt=‘%H:%M:%S‘) logger = logging.getLogger(__name__) return logger 
 
Auch wenn es nicht die reine Lehre ist: Auch Logfiles dürfen zur besseren Lesbarkeit strukturiert, mit Leerzeilen versehen und userfreundliche Angaben enthalten, Redundanzen hingegen vermeiden. Vergleichen sie die beiden (verfremdeten) Beispiele und erweitern Sie sie im Geiste auf 10.000 Zeilen. Im unteren werden Sie sich weitaus schneller zurechtfinden: 

09:22:43,424 testframework.moduleXYZ.MethodA: Invoking insertKey ... 09:22:44,723 testframework.moduleXYZ: Key function observe has terminated. Waiting for draining stopped ... 09:22:44,723 testframework.moduleXYZ: Waiting until exit C closed ... 09:23:32,726 testframework.moduleXYZ: Exit C has stopped running. State: Idle 09:23:32,726 testframework.moduleXYZ: Key Observer has stopped. 09:23:32,740 testframework.moduleXYZ.testcase5: Running test5 09:23:32,740 testframework.moduleXYZ: Set key mode: AdminUser 09:23:33,270 testframework.moduleXYZ: Admin set. 09:23:34,270 testframework.moduleXYZ: Open drainage 09:23:35,595 testframework.moduleXYZ: Waiting until Exit B has stopped ... 09:23:36,595 testframework.moduleXYZ: Exit B has stopped running. State: Off 09:23:36,619 testframework.moduleXYZ: Waiting until drainage motor has stopped ...  09:22:43,424 .. MethodA: Invoking insertKey 09:22:44,723 ..: Key function observe has terminated. Waiting for draining stopped 09:22:44,723 ..: Waiting until exit C closed 09:23:32,726 ..: Exit C has stopped running. State: Idle 09:23:32,726 ..: Key Observer has stopped.  ************************************************************** * Start testcase 5 at 09:23:32 **************************************************************  09:23:32,740 . testcase5: Running test5 09:23:32,740 ..: Set key mode: AdminUser 09:23:33,270 ..: Admin set. 09:23:34,270 ..: Open drainage 09:23:35,595 ..: Waiting until Exit B has stopped 09:23:36,595 ..: Exit B has stopped running. State: Off 09:23:36,619 ..: Waiting until drainage motor has stopped 
 
Der Unterschied besteht darin, dass in der unteren übersichtlicheren Version redundante Angaben weggelassen sowie die Testcases durch den Sternchenkasten deutlich voneinander abgesetzt wurden. Mit Python haben wir bereits eine große Auswahl von gebrauchsfertigen Werkzeugen für den Systemtest auf dem Tisch, von denen wir hier nur einen kleinen Überblick geboten haben. Es lohnt sich, diesen Werkzeugkasten einmal genauer zu inspizieren. (fr)
 
Unsere Buchempfehlung: Softwaretests mit Python

Mit Python wird immer mehr professionelle Software entwickelt, gerade auch im industriellen Kontext, der nicht selten sicherheitsrelevanten Regulatorien unterworfen ist, zum Beispiel in den Bereichen Automotive und Medizintechnik. Programmierung und Test sind dabei zwei Paar Schuhe: Was an Professionalität bei der Entwicklung mit C/C++ häufig angekommen ist, gilt für Python eher selten. Es fehlte offenbar ein konkreter Guide. Johannes Hubertz hat genau so einen geschrieben, der zudem angenehm zu lesen ist. Mit erheblichem Wissen geht er unterschiedliche Testarten und Mocking durch. Alles ist gut motiviert. Er hat dem Thema Testen sogar einen kleinen mathematischen Abschnitt spendiert. Behandelt wird auch Test Driven Development, also die agile Softwareentwicklung. Im Vordergrund steht die praktische Anwendung und Erweiterbarkeit der Testwerkzeuge. Kurze Interviews mit Python-Entwicklern stellen deren persönliche Erfahrungen mit Softwaretests dar. 
Der Preis für dieses Werk ist mit 50 Euro sicher ambitioniert angesetzt, allerdings ist das Buch nach meiner Auffassung jeden Cent wert.

Softwaretests mit Python 254 Seiten, erschienen im Verlag Springer Vieweg ISBN-10: 3662486024 ISBN-13: 978-3662486023