Mit Hilfe von Software-Tests lässt sich Code auf die Erfüllung bestimmter, zuvor definierter, Anforderungen prüfen und die Qualität einer Anwendung messen und sicherstellen.
Durch eine (zusätzliche) kontinuierliche Ausführung der Tests z. B. im Rahmen einer Kontinuierliche Integration lässt sich die Qualität somit auch nachhaltig verbessern und die Wartungskosten minimieren.
In welchem Rahmen die Tests geschrieben werden spielt, technisch gesehen, eine untergeordnete Rolle. So können die Tests im Rahmen eines Test-Driven Developments vorab oder “klassisch” parallel bzw. nachträglich implementiert werden. Wichtig sollte sein, dass Tests geschrieben und regelmäßig ausgeführt werden.
Der Artikel geht auf Beispiele in Go ein und zeigt wie man eine Go Anwendung testet.
Keep the bar green to keep the code clean!
In der Golang Standardlibrary befindet sich das package testing
mit dessen Hilfe Test in Go geschrieben werden. Die Tests können dann auf Kommandozeile mit dem Kommando go test
ausgeführt werden.
Im Artikel soll im ersten Schritt folgende Add
Funktion getestet werden:
package main
import "fmt"
func Add(first int, second int) int {
return first + second
}
func main() {
fmt.Printf("Hello World, %d \n", Add(10, 10))
}
Die Funktion ist mit Absicht einfach gehalten und soll nicht durch ihre eigene Komlexität vom Testing mit Go ablenken.
Startet man hier go test
wird das mit folgender Ausgabe quittiert:
? github.com/kkoehler/golang/testing [no test files]
Unit-Tests müssen bei Go in separate Dateien implementiert werden und die Dateiendung _test.go
besitzen. Sie können sich im gleichen Verzeichnisbaum wie die Quelldateien befinden und müssen nicht wie z. B. bei Java/Maven in einen separaten Ast gelegt werden.
Je nachdem was man testen möchte empfiehlt es sich die Test Dateien entweder im gleichen Package oder in einem zweiten Package abzulegen. Möchte man z. B. die externe Schnittstelle des Package testen, kann es Sinn machen den Test außerhalb des Packages zu halten, da man so wie ein “echter” Client des Packages agieren muss. Möchte man interne Details testen ist die Ablage im gleichen Package sinnvoll. Eine verbreitete Empfehlung ist die Unit-Tests im gleichen Package abzulegen.
Bei Go heißt es eigentlich ein Verzeichnis, ein Package. Bei Tests ist das nicht ganz der Fall. In einem Verzeichnis kann zusätzlich ein Test-Package angelegt werden. Dieses muss den Suffix _test
besitzen. Möchte man z. B. für das package add
ein Test-Package anlegen, so nennt man es im gleichen Verzeichnis package add_test
.
Tests werden nicht beim normalen Build mit übersetzt und entsprechend auch nicht ausgeliefert. Sie werden nur bei
go test
übersetzt und ausgeführt.
Einzelne Tests werden in Test-Methoden implementiert. Diese müssen folgende Aufbau besitzen:
TestXxx
entsprechen, wobei Xxx nicht mit einem Kleinbuchstaben beginnen darf.Im Beispiel kann ein Test so aussehen:
package main
import "testing"
func TestAdd(t *testing.T) {
}
Nutzt man z. B. Visual Studio Code und die Golang Extension kann man auch direkt in der IDE die Tests ausführen. Wie im Screenshot zu sehen werden über der Testfunktion Links zur Ausführung eingeblendet.
Die Macher von Go haben sich explizit gegen die Einführung von speziellen assert
Methoden, wie sie z. B. in Java’s JUnit oder Node.js Mocha vorhanden sind, entschieden. Man wollte keine “separate Sprache” für Tests entwickeln, die man sich zusätzlich erlernen muss. Es sollte möglichst einheitlich programmiert werden können.
Fehler in Tests kann man mit t.Error
bzw. t.Errorf
oder t.Fail
melden. t.Error
meldet einen Fehler, bricht den Test aber nicht ab. t.Fail
bricht die aktuelle Ausführung ab.
package main
import "testing"
func TestAdd(t *testing.T) {
result := Add(10, 10)
if result != 20 {
t.Errorf("Wrong result. Got %d but wanted 20", result)
}
}
Eine Ausführung des Test mittels go test
führt dann zu:
PASS
ok <..>/src/github.com/kkoehler/golang/testing 0.001s
Die Ausführung mittels go test -v
liefert zusätzliche Informationen zur Testausführung:
=== RUN TestAdd
--- PASS: TestAdd (0.00s)
PASS
ok github.com/kkoehler/golang/testing 0.001s
Eine Möglichkeit mehrere Testfälle mit nur geänderten Parameter durchzuführen bieten Table-Driven Tests. Hierbei werden die Parameter in einem Slice gespeichert und die Tests über eine Schleife abgearbeitet. So lassen sich recht schnell viele Szenarien mit wenig Code durchspielen. Durch die Nutzung von t.Error
laufen auch alle Tests ohne dass die Ausführung beim ersten Fehler abgebrochen wird.
func TestTableAdd(t *testing.T) {
tables := []struct {
first int
y int
n int
}{
{1, 1, 2},
{1, 2, 3},
{2, 2, 4},
{5, 2, 7},
}
for _, table := range tables {
total := Add(table.first, table.y)
if total != table.n {
t.Errorf("Sum of (%d+%d) was incorrect, got: %d, want: %d.", table.first, table.y, total, table.n)
}
}
}
Bei der Ausführung von Tests kann man mit dem run
Parameter steuern welche Tests ausgeführt werden sollen. Dem angegebenen Pattern entsprechende Tests werden ausgeführt. In folgendem Beispiel werden alle Tests ausgeführt, die dem Pattern Add
entsprechen.
go test -run Add
Möchte man die Ausführung der Table Driven Tests auch feingranularer steuern kann man die t.Run
Methode nutzen. Die t.Run
Methode benötigt zwei Parameter. Zum einen den Namen des Tests und zum Anderen die auszuführende Testmethode.
Die Erweiterung des oberen Beispiels sieht dann so aus:
func TestTableAdd(t *testing.T) {
tables := []struct {
name string
first int
y int
n int
}{
{"one and one", 1, 1, 2},
{"one and two", 1, 2, 3},
{"two and two", 2, 2, 4},
{"five and two", 5, 2, 7},
}
for _, table := range tables {
t.Run(table.name, func(t *testing.T) {
total := Add(table.first, table.y)
if total != table.n {
t.Errorf("%v: Sum of (%d+%d) was incorrect, got: %d, want: %d.", table.name, table.first, table.y, total, table.n)
}
})
}
}
Eine Ausführung mit go test -v
führt dann zu folgender Ausgabe:
=== RUN TestAdd
--- PASS: TestAdd (0.00s)
=== RUN TestTableAdd
=== RUN TestTableAdd/one_and_one
=== RUN TestTableAdd/one_and_two
=== RUN TestTableAdd/two_and_two
=== RUN TestTableAdd/five_and_two
--- PASS: TestTableAdd (0.00s)
--- PASS: TestTableAdd/one_and_one (0.00s)
--- PASS: TestTableAdd/one_and_two (0.00s)
--- PASS: TestTableAdd/two_and_two (0.00s)
--- PASS: TestTableAdd/five_and_two (0.00s)
PASS
ok github.com/kkoehler/golang/testing 0.001s
Auch hier lässt sich mit dem run
Parameter die Ausführung einschränken.
Ein wichtiger Bestandteil bei der Prüfung der eigenen Test-Qualität ist die Bestimmung der Testabdeckung. Welcher Code wird durch einen Test überprüft und welcher nicht. Das Test Kommando von Go bietet hierzu die Option -cover
mit der ein Coverage Report erstellt werden kann. Dieser kann dann zu einem HTML Bericht umformatiert werden.
go test -cover -coverprofile=c.out
go tool cover -html=c.out -o coverage.html
In obigen Beispiel liegt die Testabdeckung, wie man sieht, bei 50%. Die nichtabgedeckten Stellen sind rot markiert.
Zusätzlich zur Unit-Test Funktionalität des package testing
besteht die möglichkeit Benchmarks zu implementieren und die Performance des Go Codes zu überprüfen.
Wer tiefer in das Profiling von Go Anwendungen einsteigen möchte, sollte noch den Blog Eintrag Profiling Go Programs anschauen.
Benchmarks werden wie Tests in den _test.go
Dateien implementiert und nutzen ebenso Funktionalität des package testing
. Die Hauptunterschiede eines Benchmarks zu einem Unit-Test sind:
b.N
mal ausführen.Ein einfaches Beispiel sieht so aus:
func BenchmarkAdd(b *testing.B) {
for n := 0; n < b.N; n++ {
Add(10, 10)
}
}
Die Ausführung von go test --bench .
führt dann zu folgendem Ergebnis:
goos: linux
goarch: amd64
BenchmarkAdd-8 2000000000 0.53 ns/op
PASS
ok _/../github.com/kkoehler/golang/testing 1.120s
Im Ergebnis wird, neben der Ausgabe der Umgebung, dargestellt, dass auf der Testmaschine die Add
Funktion im Durchschnitt 0.53 Nanosekunden zur Ausführung benötigt hat.
Auch bei den Benchmark Funktionen können die gleichen Parameter wie oben beschrieben zur Einschränkung der Funktionen benutzt werden.
Wie dargestellt können mit Go recht einfach Unit-Tests erstellt und ausgeführt werden. Mit der selben Funktionalität lassen sich Benchmarks umsetzen.
Das komplette Beispiel kann auch in GitHub gefunden werden.
06.03.2019
Der praktische Soforteinstieg für Developer und Softwarearchitekten, die direkt mit Go produktiv werden wollen.
zur Buchseite beim Rheinwerk Verlag Rheinwerk Computing, ISBN 978-3-8362-7559-0 (als PDF, EPUB, MOBI und Papier)
Source Fellows GmbH
Lerchenstraße 31
72762 Reutlingen
Telefon: (0049) 07121 6969 802
E-Mail: info@source-fellows.com