Die Nutzung dieses Werks ist unter den Bedingungen der "Creative Commons Namensnennung - Keine Bearbeitungen 4.0 International"-Lizenz erlaubt.
Testen mit Interfaces
(Updates: 12.11.2014, 24.11.2014)
Dieser Aufsatz macht auf einen hilfreichen Aspekt von Interfaces aufmerksam, und hoffentlich das Testen leichter.
Testen ist wichtig ... gehört aber zu den weniger beliebten Tätigkeiten eines Programmierers. Umso interessierter war ich als ich vor kurzem auf John Graham-Cummings Aufsatz " Go interfaces make test stubbing easy" stieß.
Und in der Tat, Interfaces machen das Testen leichter, indem nämlich das zu testende Paket während des Tests von Funktionen importierter Pakete isoliert wird. Los geht's!
// funktion.go enthält die Funktion funk. package pak import "pfad/zu/imp" func funk(v imp.ImpTyp) { // ... v.Mach() // ... }
// funktion_test.go enthält einen Test für die Funktion funk. package pak import "testing" func TestFunk(t *testing.T) { // ... funk(v) // ... }
// imptyp.go enthält den Typ ImpTyp mit seiner Methode Mach. package imp type ImpTyp struct{ ... } func (ImpTyp) Mach() { ... }
Ein Funktion funk
im Paket pak
arbeitet mit einer
Variablen v
vom Typ ImpTyp
und ruft dessen Methode
Mach
.
ImpTyp
und Mach
sind im Paket imp
deklariert. So weit, so gut!
Problem und Lösung
Wenn ich nun die Testroutine TestFunk
starte, um die Funktion
funk
zu testen, so möchte ich das tun ohne mittelbar auch
die Methode Mach
zu testen.
Erstens weil das an anderer Stelle passiert, nämlich bei den Tests im Paket
imp
.
Und zweitens weil Mach
eine Vielzahl von Dingen voraussetzen
kann, z.B. eine geöffnete Datenbank, die nun überhaupt nichts mit dem zu
testenden Kode in funk
zu tun haben, und das Testen unnötig
kompliziert machen.
So geht's:
-
Ich deklariere im Paket
pak
ein InterfaceMacher
, das vom TypImpTyp
implementiert wird, und sorge dann dafür, dass die übergebene Variablev
vom TypMacher
ist:// funktion.go enthält die Funktion funk. package pak import "pfad/zu/imp" type Macher interface { Mach() } func funk(v Macher) { // ... v.Mach() // ... }
Damit funktioniert das Paketpak
genausogut wie vorher. Aufrufe vonfunk
bleiben unverändert; dort wird weiterhin eine Variable mit dem konkreten Typimp.ImpTyp
übergeben. -
Im Testkode deklariere ich einen leeren Typ
DumTyp
mit einer MethodeMach
, die nichts macht. Damit genügt auch dieser Typ dem InterfaceMacher
. Ich kann jetzt für den Test eine Variable vom TypDumTyp
erzeugen und an die zu testende FunktionMach
übergeben:// funktion_test.go enthält einen Test für die Funktion funk. package pak import "testing" type DumTyp struct{} func (DumTyp) Mach() {} func TestFunk(t *testing.T) { // ... v := DumTyp{} funk(v) // ... }
Damit testet TestFunk die Funktionfunk
ohne dass von dort Kode aus dem Paketimp
aufgerufen werden müsste ... Applaus!
Eine unerwartete Schwierigkeit
Was tun, wenn Mach
im Paket imp
keine Methode,
sondern eine einfache Funktion ist?
Nun,
wenn imp
ein externes Paket ist, dann habe ich verloren.
(Update: 12.11.2014)
Ein eigenes Paket imp
könnte ich z.B. ändern, indem ich dort
einen leeren Typ Imp
deklariere und aus jeder exportierten
Funktion, in unserem Beispiel also aus Mach
, eine Methode
mit einem Empfänger vom Typ Imp
machte.
(Update: 12.11.2014)
Eine bessere Methode, die auch mit Funktionen aus fremden Paketen
funktioniert, beschreibe ich in
"Testen
mit Funktionszeigern".
Grundlegendes und Weiterführendes
Grundsätzliches zum Testen in Go findet man in " Wie man mit Go arbeitet" und natürlich in der Doku zum Paket testing. Zu tabellengesteuerten Tests wird man fündig beim go-wiki.
(Update: 24.11.2014)
Für Leser, die des Englischen mächtig sind, hier noch der Verweis auf einen Vortrag
von Andrew Gerrand:
Testing Techniques,
und auf die
dazugehörenden Dias.
Wie Interfaces in Go intern arbeiten beschreibt Russ Cox in " Go Data Structures: Interfaces". Aus der Sicht eines Python-Programmierers schreibt Jordan Orelli über Go-Interfaces in " How to use interfaces in Go".
Oktober 2014