AA_cockpit_sxc_963786.jpg

© sxc.hu

Check inklusive

Python-Programme testen mit Doctest

01.12.2008
Wer mit Python programmiert, bekommt mit dem Doctest-Modul eine einfache Möglichkeit, die Programmfunktionen anschaulich zu dokumentieren und gleichzeitig zu testen.

Wie schön wäre es, wenn jede geschriebene Programmzeile automatisch fehlerfrei wäre. Ist sie aber nicht, was das Testen von Software notwendig macht. Manuelle Tests – das heißt das Sichten der Ergebnisse bei jedem Testdurchlauf durch den Programmierer – ist vor allem beim Überarbeiten von Software zeitaufwändig und fehleranfällig. Deshalb empfiehlt sich automatisiertes Testen. Dabei programmieren Sie nicht nur die eigentliche Software, sondern zusätzlich die Tests (siehe Kasten "Testgetriebene Entwicklung"). Das Testprogramm überprüft dann beim Aufruf sowohl die alten als auch die neuen Funktionen. So zahlt sich das Entwickeln speziellerer Tests aus. Unter Python unterstützt Sie das Modul Doctest beim Schreiben der Tests.

Testgetriebene Entwicklung

Eine oft empfohlene Vorgehensweise sieht vor, die Tests vor der zu testenden Software zu schreiben [6]. Was zunächst widersinnig oder zumindest nicht gerade intuitiv erscheint, hat gute Gründe: Haben Sie den Test zuerst geschrieben, machen Sie sich mehr Gedanken über eine saubere Schnittstelle des Codes und passen nicht unbewusst die Tests an die konkrete Implementation an.

Manche Programmierer finden, dass das Schreiben der Tests vor dem zu testenden Code, die so genannte testgetriebene Entwicklung, süchtig macht: Die Arbeit geht leichter von der Hand, weil die Tests wirklich nur die spezifizierte Funktionalität prüfen und klar erkennen lassen, wann die Implementation des nächsten Features ansteht: Nämlich dann, wenn alle Tests zu einer neuen Funktion fehlerfrei laufen.

Erste Schritte mit Doctest

Das Prinzip von Doctest [1] ist, die Tests in den Docstring des zu testenden Moduls beziehungsweise der Klasse, Methode oder Funktion schreiben. Die Tests bestehen aus Anweisungen und den zugehörigen Ausgaben, so wie sie im interaktiven Python-Interpreter aussehen würden. Die Testblöcke grenzen Sie vom umgebenden Text durch Leerzeilen ab. Gegenüber dieser einfachen Vorgehensweise wirkt der Umgang mit dem Unittest-Modul [2] meist recht schwerfällig.

Ein Beispiel zeigt den Einsatz von Doctest: Eine Funktion wertet eine Zeile des so genannten Common-Formats [3] einer Webserver-Zugriffslogdatei aus und gibt die Werte in Form einer Liste zurück. Den Aufbau einer solchen Log-Zeile zeigt Listing 1. Die Informationen umfassen:

  • die IP-Adresse,
  • die Identd-Information (normalerweise unbenutzt und daher nur durch ein Minuszeichen gekennzeichnet),
  • die Benutzerkennung gemäß HTTP-Authentifikation,
  • den Zeitstempel aus Datum, Zeit und Zeitzoneninformation,
  • eine Zeichenkette in Anführungszeichen, bestehend aus HTTP-Befehl, Pfad und Protokollversion,
  • den HTTP-Ergebniscode sowie
  • die Anzahl übertragener Bytes (oder alternativ ein weiteres Minuszeichen).
Listing 1
127.0.0.1 - frank [10/Oct/2000:13:55:36 -0700] "GET /index.html HTTP/1.1" 200 2326

Schafft es die Funktion nicht, eine Zeile auszuwerten, soll die Funktion eine Ausnahme ParserError erzeugen. Eine erste Implementation zeigt Listing 2. Führen Sie das Skript aus, liefert der Aufruf von doctest.testmod() in der letzten Zeile ein Ergebnis, wie in Abbildung 1.

Listing 2
#! /usr/bin/env python
# coding: latin1
import doctest
import time
class ParserError(Exception):
    pass
def parse_common_log_line(line):
    """Werte eine Zeile im Common-Log-Format aus und gib eine
    Liste mit den entsprechenden Werten für IP, Nutzerkennung,
    Datum, Zeit (beide ISO-8601-Format), Befehl, Pfad, Protokol,
    Status und Länge (0 für unbestimmte Länge) zurück.
    >>> parse_common_log_line(
    …   '127.0.0.1 - frank [15/Oct/2000:13:55:36 -0700] '
    …   '"GET /index.html HTTP/1.1" 200 2326')
    ['127.0.0.1', 'frank', '2000-10-15', '13:55:36', 'GET', '/index.html', 'HTTP/1.1', 200, 2326]
    Lässt sich die Zeile nicht auswerten, löse eine ParserError-
    Ausnahme aus.
    """
    parts = line.split()
    timestamp = time.strptime(parts[3], "[%d/%b/%Y:%H:%M:%S")
    # Identd-Kennung entfernen
    del parts[1]
    # Datum und Zeit einfügen
    parts[2:4] = (time.strftime("%Y-%m-%d", timestamp),
                  time.strftime("%H:%M:%S", timestamp))
    # Zahlen umwandeln
    parts[7] = int(parts[7])
    parts[8] = int(parts[8])
    return parts
if __name__ == "__main__":
    doctest.testmod()
Abbildung 1: Der erste Doctest-Lauf zeigt: Es gibt noch überschüssige Anführungszeichen im Ergebnis.

Vergleichen Sie die erwartete (Expected) und die tatsächliche (Got) Ausgabe, fällt in letzterer auf, dass vor dem HTTP-Befehl GET und nach der Protokoll-Angabe noch Anführungszeichen stehen. Diese entfernen Sie leicht, indem Sie im Listing 2 zwei Anweisungen ergänzen (Abbildung 2).

Abbildung 2: Mittels Slicing entfernen Sie die Anführungszeichen von den beiden Elementen der Liste parts aus dem Beispiel.

Starten Sie das noch einmal, erhalten Sie – nichts. Das geschieht immer dann, wenn Doctest keine Fehler findet. Herzlichen Glückwunsch! Möchten Sie auch eine Ausgabe zu den bestandenen Tests, erreichen Sie das mit der Option -v (für "verbose") nach dem aufgerufenen Programm.

Es wartet aber noch ein wenig Arbeit: Ein Minuszeichen als Längenangabe (siehe Funktions-Docstring) führt noch zu einem Fehler. Um das zu prüfen, fügen Sie im Docstring einen zweiten Test ein (Abbildung 3). Das Ausführen der Tests zeigt Abbildung 4. Hier erscheint nur der fehlgeschlagene Test, der bestandene nicht. Die in Abbildung 5 gezeigte Modifikation am Code behebt das Problem und akzeptiert nun auch ein Minuszeichen statt der Längenangabe.

Abbildung 3: Mit einem weiteren Test prüfen Sie, ob das Skript das Minuszeichen in eine Null umwandelt.
Abbildung 4: Das Umwandeln des Minuszeichens nach Null fehlt noch, deshalb schlägt der neue Test fehl.
Abbildung 5: Ein Minuszeichen für die Content-Länge erlauben.

Zwei weitere Tests im Docstring prüfen, ob bestimmte fehlerhafte Log-Zeilen zu einer Ausnahme ParserError führen. Zunächst geschieht dies nicht. Python löst andere Ausnahmen (IndexError, ValueError) aus. Die Endversion der Funktion (Listing 3) behandelt auch diese Probleme, sodass das Ausführen des Moduls keine Fehler mehr liefert. Beachten Sie, dass ein Test auf eine Ausnahme nicht alle Zeilen des Tracebacks enthält: Stattdessen deutet eine eingerückte Folge von Punkten die Aufrufhierarchie an.

Listing 3
def parse_common_log_line(line):
    """Werte eine Zeile im Common-Log-Format aus und gib
    eine Liste mit den Werten für IP, Nutzerkennung, Datum,
    Zeit (beide ISO-8601-Format), Befehl, Pfad, Protokol,
    Status und Länge (0 für unbestimmte Länge) zurück.
    >>> parse_common_log_line(
    …   '127.0.0.1 - frank [15/Oct/2000:13:55:36 -0700] '
    …   '"GET /index.html HTTP/1.1" 200 2326')
    ['127.0.0.1', 'frank', '2000-10-15', '13:55:36', 'GET', '/index.html', 'HTTP/1.1', 200, 2326]
    Eine unbestimmte Länge am Zeilenende wird zur Zahl 0:
    >>> parse_common_log_line(
    …   '127.0.0.1 - frank [15/Oct/2000:13:55:36 -0700] '
    …   '"GET /index.html HTTP/1.1" 200 -')[-1]
    0
    Lässt sich die Zeile nicht auswerten, löse eine
    ParserError-Ausnahme aus.
    >>> parse_common_log_line("bla")
    Traceback (most recent call last):
        …
    ParserError: kein Common-Log-Format
    >>> parse_common_log_line(
    …   '127.0.0.1 - frank [123/Oct/2000:13:55:36 -0700] '
    …   '"GET /index.html HTTP/1.1" 200 2326')
    Traceback (most recent call last):
        …
    ParserError: Datum oder Zeit ungueltig
    """
    parts = line.split()
    if len(parts) != 10:
        raise ParserError("kein Common-Log-Format")
    try:
        timestamp = time.strptime(parts[3], "[%d/%b/%Y:%H:%M:%S")
    except ValueError:
        # "time data did not match format"
        raise ParserError("Datum oder Zeit ungueltig")
    # Identd-Kennung entfernen
    del parts[1]
    # Datum und Zeit einfügen
    parts[2:4] = (time.strftime("%Y-%m-%d", timestamp),
                  time.strftime("%H:%M:%S", timestamp))
    # Anführungszeichen vor HTTP-Befehl und nach Protokoll-
    # Info entfernen
    parts[4] = parts[4][1:]
    parts[6] = parts[6][:-1]
    # Zahlen umwandeln
    parts[7] = int(parts[7])
    if parts[8] == "-":
        parts[8] = 0
    parts[8] = int(parts[8])
    return parts

Literate Testing

Das Code-Beispiel zeigt, wie einfach Sie Code durch Testfälle im Docstring prüfen. Dabei geht jedoch die Übersicht verloren, wenn der Docstring länger als der eigentliche Programm-Code gerät. Besonders häufig passiert dies bei zahlreichen Tests für subtile Sonderfälle.

Sie vermeiden das, indem Sie die Tests in eine Textdatei schreiben und diese mit der Funktion testfile des Moduls doctest ausführen. Eine solche Datei enthält im Wesentlichen den Docstring der zu testenden Funktion ohne die umschließenden Anführungszeichen, eventuell ergänzt um weitere Beschreibungen zwischen den Tests (Listing 4). Dies nennt sich auch Literate Testing [4].

Liegen mehrere solcher Testdateien mit dem Namensmuster test_*.txt in einem Verzeichnisbaum, führen Sie diese mit den Befehlen find und python aus (Listing 5).

Listing 4
Die Funktion parse_common_log_line
==================================
Die Funktion befindet sich im Modul parse_common_log_line:
>>> import parse_common_log_line as parse
Eine als Argument übergebene Zeichenkette aus einer
Log-Datei im Common-Format wird damit in eine Liste der
Bestandteile zerlegt:
>>> parse.parse_common_log_line(
…   '127.0.0.1 - frank [15/Oct/2000:13:55:36 -0700] '
…   '"GET /index.html HTTP/1.1" 200 2326')
['127.0.0.1', 'frank', '2000-10-15', '13:55:36', 'GET', '/index.html', 'HTTP/1.1', 200, 2326]
Datum und Zeit werden dabei als Zeichenketten im ISO-8601-
Format erzeugt. Die Zeitzoneninformation geht verloren.
HTTP-Status und Länge werden in Ganzzahlen umgewandelt.
Falls für die Länge nur ein Minuszeichen angegeben ist,
ist der entsprechende Listeneintrag 0:
>>> result = parse.parse_common_log_line(
…   '127.0.0.1 - frank [15/Oct/2000:13:55:36 -0700] '
…   '"GET /index.html HTTP/1.1" 200 -')
>>> result[8]
0
Hier könnten jetzt weitere Absätze und Tests folgen. …
Listing 5
$ find testverzeichnis -name 'test_*.txt' -exec python -c "import doctest; doctest.testfile('{}')" \;

LinuxCommunity kaufen

Einzelne Ausgabe
 
Abonnements
 

Ähnliche Artikel

Kommentare

Infos zur Publikation

LU 01/2015: E-Books im Griff

Digitale Ausgabe: Preis € 4,95
(inkl. 19% MwSt.)

Mit der Zeitschrift LinuxUser sind Sie als Power-User, Shell-Guru oder Administrator im kleinen Unternehmen monatlich auf dem aktuelle Stand in Sachen Linux und Open Source.

Sie sind sich nicht sicher, ob die Themen Ihnen liegen? Im Probeabo erhalten Sie drei Ausgaben zum reduzierten Preis. Einzelhefte, Abonnements sowie digitale Ausgaben erwerben Sie ganz einfach in unserem Online-Shop.

NEU: DIGITALE AUSGABEN FÜR TABLET & SMARTPHONE

HINWEIS ZU PAYPAL: Die Zahlung ist auch ohne eigenes Paypal-Konto ganz einfach per Kreditkarte oder Lastschrift möglich!       

Tipp der Woche

Ubuntu 14.10 und VirtualBox
Ubuntu 14.10 und VirtualBox
Tim Schürmann, 08.11.2014 18:45, 0 Kommentare

Wer Ubuntu 14.10 in einer virtuellen Maschine unter VirtualBox startet, der landet unter Umständen in einem Fenster mit Grafikmüll. Zu einem korrekt ...

Aktuelle Fragen

PCLinuxOS Version 2014.08 "FullMonty" Umstellung auf deutsch
Karl-Heinz Welz, 19.12.2014 09:55, 0 Antworten
Hallo, liebe Community, ich bin 63 Jahre alt und möchte jetzt nach Jahrzehnten Windows zu Linux...
ICEauthority
Thomas Mann, 17.12.2014 14:49, 2 Antworten
Fehlermeldung beim Start von Linux Mint: Could not update ICEauthority file / home/user/.ICEauth...
Linux einrichten
Sigrid Bölke, 10.12.2014 10:46, 5 Antworten
Hallo, liebe Community, bin hier ganz neu,also entschuldigt,wenn ich hier falsch bin. Mein Prob...
Externe USB-Festplatte mit Ext4 formatiert, USB-Stick wird nicht mehr eingebunden
Wimpy *, 02.12.2014 16:31, 0 Antworten
Hallo, ich habe die externe USB-FP, die nur für Daten-Backup benutzt wird, mit dem YaST-Partition...
Steuern mit Linux
Siegfried Markner, 01.12.2014 11:56, 2 Antworten
Welches Linux eignet sich am besten für Steuerungen.