Testen ist gut, testen lassen ist besser: Emacs Lisp Regression Test (ERT)
Wie schrieb ich im Rahmen unserer Funktion "my-format-time" zum Konvertieren eines Datums in ein anderes Datumsformat?
Wir testen die Funktion natürlich erst einmal: Dazu wechseln wir in einen Test-Buffer (z. B.
Ctrl-x b test
) und führen unsere Funktion perM-: (my-format-time "2018-05-31")
aus.
Das machen wir ab sofort nicht mehr, denn: Als "richtiger" Emacs-Benutzer haben Sie zum Testen was viel Besseres: ERT aka "Emacs Lisp Regression Testing":
ERT is a tool for automated testing in Emacs Lisp. Its main features are facilities for defining tests, running them and reporting the results, and for debugging test failures interactively.
ERT is similar to tools for other environments such as JUnit, but has unique features that take advantage of the dynamic and interactive nature of Emacs. Despite its name, it works well both for test-driven development (see http://en.wikipedia.org/wiki/Test-driven_development) and for traditional software development methods.
Testen lassen
Das Vorgeplänkel
Dazu schreiben wir unsere Funktion "my-format-time" zunächst einmal um (wir nehmen mal an, wir würden sie als richtige Funktion zum Konvertieren eines Datums innerhalb eines Programms einbauen und wollen daher keine Ausgabe an der aktuellen Cursor-Position mehr):
(defun my-format-time (time-string)
(let* ((time (parse-time-string time-string))
(day (nth 3 time))
(month (nth 4 time))
(year (nth 5 time)))
(format-time-string "%d.%m.%Y"
(encode-time 0 0 0 day month year))))
Der Auftrag
Unser Ziel: Wir wollen unsere "my-format-time"-Funktion robust machen: Sicherstellen, dass sie alle möglichen Eingaben richtig konvertiert und falsche Eingaben sie nicht aus der Bahn werfen.
Bewaffnung
Zu ERT muss man nur zwei Sachen wissen:
ert-deftest
definiert einen Test- Mit
should
legen wir fest, was rauskommen muss, damit ein Test als erfolgreich gilt
Ein erster einfacher Test könnte z. B. so aussehen (die oben aufgeführte Funktion "my-format-time" und die unten aufgeführte Test-Funktion "ert-my-format-time-test" sind in den *scratch*-Buffer einzufügen und per M-x eval-buffer
zu evaluieren):
(ert-deftest ert-my-format-time-test ()
(should (equal (my-format-time "2018-10-10") "10.10.2018")))
Ein einfaches M-x ert
gefolgt von zwei mal <ENTER>
, um per Default alle Tests auszuführen, beschert uns folgendes Ergebnis:
Selector: t
Passed: 1
Failed: 0
Skipped: 0
Total: 1/1
Started at: 2018-03-21 22:44:23+0100
Finished.
Finished at: 2018-03-21 22:44:23+0100
.
Unser Test war erfolgreich.
(Echte Wizards schreiben übrigens erst ihre Tests und dann ihre Funktionen.)
Wir können ERT übrigens auch gleich mehrere Dinge in einem Durchgang testen lassen. Hierzu fügen wir einfach weitere should
-Anweisungen hinzu:
(ert-deftest ert-my-format-time-test ()
(should (equal (my-format-time "2018-10-10") "10.10.2018"))
(should (equal (my-format-time "2018-10-15") "15.10.2018")))
Das zählt allerdings nur als ein Test:
Selector: t
Passed: 1
Failed: 0
Skipped: 0
Total: 1/1
Started at: 2018-03-21 22:50:35+0100
Finished.
Finished at: 2018-03-21 22:50:35+0100
.
Lassen wir einen Test doch mal absichtlich schiefgehen, z. B. per
(ert-deftest ert-my-format-time-test ()
(should (equal (my-format-time "2018-10-10") "10.10.2018"))
(should (equal (my-format-time "2018-10-15") "15.10.2018"))
;; Peng!
(should (equal (my-format-time "2018-10-15") "99.99.9999")))
ERT spuckt Folgendes aus:
Selector: t
Passed: 0
Failed: 1 (1 unexpected)
Skipped: 0
Total: 1/1
Started at: 2018-03-21 22:55:05+0100
Finished.
Finished at: 2018-03-21 22:55:05+0100
F
F ert-my-format-time-test
(ert-test-failed
((should
(equal
(my-format-time "2018-10-15")
"99.99.9999"))
:form
(equal "15.10.2018" "99.99.9999")
:value nil :explanation
(array-elt 0
(different-atoms
(49 "#x31" "?1")
(57 "#x39" "?9")))))
Teile und herrsche!
Um unsere Tests auseinanderhalten zu können, empfiehlt es sich, diese auf verschiedene Tests aufzuteilen. Schreiben wir doch einfach mal zwei Tests, einen, um korrekte Eingaben zu testen und einen, um falsch formatierte Eingaben zu testen (wir bestimmen, dass unsere Funktion in einem solchen Fall einen leeren String zurückgeben soll):
(ert-deftest ert-my-format-time-test-user-input ()
(should (equal (my-format-time "2018-10-10") "10.10.2018"))
(should (equal (my-format-time "2018-10-15") "15.10.2018")))
(ert-deftest ert-my-format-time-test-user-bad-format-input ()
(should (string-empty-p (my-format-time "20181010")))
(should (string-empty-p (my-format-time "2018+10+15"))))
ERT gibt aus:
Selector: t
Passed: 0
Failed: 0
Skipped: 0
Total: 0/2
Started at: 2018-03-21 23:18:22+0100
Aborted.
Aborted at: 2018-03-21 23:18:22+0100
A-
A ert-my-format-time-test-user-bad-format-input
aborted
(defun my-format-time (time-string)
(let* ((time (parse-time-string time-string))
(day (nth 3 time))
(month (nth 4 time))
(year (nth 5 time)))
(format-time-string "%d.%m.%Y"
(encode-time 0 0 0 day month year))))
Damit unsere Funktion auch mit solchen falschen Eingaben umgehen kann und wir unsere Tests erfolgreich abschließen können, fügen wir zu unserer Funktion "my-format-time" eine Prüfung hinzu, die falsche Formate abfängt (nur zu Illustrationszwecken - in der "freien Wildbahn" würden wir noch prüfen, ob die korrekte Anzahl Stellen pro Element gegeben ist, ob es sich bei den gesplitteten Elementen wirklich um Ziffern handelt etc. oder man würde gleich mit den in elisp vorhandenen Funktionen arbeiten):
(defun my-format-time (time-string)
;; Neu: Prüfung
(if (= (length (split-string time-string "-")) 3)
(progn
(let* ((time (parse-time-string time-string))
(day (nth 3 time))
(month (nth 4 time))
(year (nth 5 time)))
(format-time-string "%d.%m.%Y"
(encode-time 0 0 0 day month year))))
""))
M-x ert
spuckt aus:
Selector: t
Passed: 2
Failed: 0
Skipped: 0
Total: 2/2
Started at: 2018-03-21 23:24:38+0100
Finished.
Finished at: 2018-03-21 23:24:38+0100
..
Schritt für Schritt ins Paradies
Anschließend würden wir weitere und immer genauere Tests definieren, um unsere Funktion "my-format-time" nach und nach immer robuster zu machen. Dazu schreiben wir erst die Tests und wenn diese schiefgehen, passen wir unsere Funktion an, bis alle Tests erfolgreich abgeschlossen werden.
Vollständige Code-Listings
Der Einfachheit halber hier mal die vollständigen Code-Listings (diese sind in den *scratch
*-Buffer einzufügen und per M-x eval-buffer
zu evaluieren. Anschließend können per M-x ert <ENTER> <ENTER>
die Tests durchgeführt werden. Bitte dran denken, nach jeder Änderung im Code M-x eval-buffer
aufzurufen):
Funktion "my-format-time" ohne Prüfung (ert-my-format-time-test-user-bad-format-input
geht schief):
(defun my-format-time (time-string)
(let* ((time (parse-time-string time-string))
(day (nth 3 time))
(month (nth 4 time))
(year (nth 5 time)))
(format-time-string "%d.%m.%Y"
(encode-time 0 0 0 day month year))))
(ert-deftest ert-my-format-time-test-user-input ()
(should (equal (my-format-time "2018-10-10") "10.10.2018"))
(should (equal (my-format-time "2018-10-15") "15.10.2018")))
(ert-deftest ert-my-format-time-test-user-bad-format-input ()
(should (string-empty-p (my-format-time "20181010")))
(should (string-empty-p (my-format-time "2018+10+15"))))
Funktion "my-format-time" mit Prüfung (ert-my-format-time-test-user-bad-format-input
funktioniert):
(defun my-format-time (time-string)
(if (= (length (split-string time-string "-")) 3)
(progn
(let* ((time (parse-time-string time-string))
(day (nth 3 time))
(month (nth 4 time))
(year (nth 5 time)))
(format-time-string "%d.%m.%Y"
(encode-time 0 0 0 day month year))))
""))
(ert-deftest ert-my-format-time-test-user-input ()
(should (equal (my-format-time "2018-10-10") "10.10.2018"))
(should (equal (my-format-time "2018-10-15") "15.10.2018")))
(ert-deftest ert-my-format-time-test-user-bad-format-input ()
(should (string-empty-p (my-format-time "20181010")))
(should (string-empty-p (my-format-time "2018+10+15"))))
Frohes Testen!
Bildnachweis: Hugh Frazer (1795-1865): Battle of Clontarf (1826)
Trackbacks
Die Kommentarfunktion wurde vom Besitzer dieses Blogs in diesem Eintrag deaktiviert.
Kommentare
Ansicht der Kommentare: Linear | Verschachtelt