Skip to content

Testen ist gut, testen lassen ist besser: Emacs Lisp Regression Test (ERT)

Hugh Frazer: Battle of Clontarf

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 per M-: (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

Keine Trackbacks

Kommentare

Ansicht der Kommentare: Linear | Verschachtelt

Noch keine Kommentare

Kommentar schreiben

Die angegebene E-Mail-Adresse wird nicht dargestellt, sondern nur für eventuelle Benachrichtigungen verwendet.
Gravatar, Twitter, Pavatar, Identica, Favatar Autoren-Bilder werden unterstützt.
Umschließende Sterne heben ein Wort hervor (*wort*), per _wort_ kann ein Wort unterstrichen werden.
Standard-Text Smilies wie :-) und ;-) werden zu Bildern konvertiert.
Markdown-Formatierung erlaubt
Formular-Optionen