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
pakein InterfaceMacher, das vom TypImpTypimplementiert wird, und sorge dann dafür, dass die übergebene Variablevvom TypMacherist:// 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 Paketpakgenausogut wie vorher. Aufrufe vonfunkbleiben unverändert; dort wird weiterhin eine Variable mit dem konkreten Typimp.ImpTypübergeben. -
Im Testkode deklariere ich einen leeren Typ
DumTypmit einer MethodeMach, die nichts macht. Damit genügt auch dieser Typ dem InterfaceMacher. Ich kann jetzt für den Test eine Variable vom TypDumTyperzeugen 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 Funktionfunkohne dass von dort Kode aus dem Paketimpaufgerufen 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
