Auf die Plätze, fertig, go!

Testen und Benchmarks in Go

Andreas Schröpfer

© Shutterstock.com / fewerton

Ziel dieses Artikels ist es, die eingebaute Testfunktionalität von Go vorzustellen. Gemeinsam mit build und install ist die Anweisung test eine der wichtigsten Funktionalitäten des Go Toolings. Neben den Tests können dort auch einfach Benchmarks erzeugt werden, was unheimlich hilfreich bei der Laufzeitoptimierung von Programmen ist. Anschließend betrachten wir, wie das Testen in Visual Studio Code integriert wurde und welche weiteren Tools dem Go-Tester das Leben erleichtern.

Einen wichtigen Teil von Go stellt das integrierte Testtool dar. Die Macher der Programmiersprache haben diese Funktionalität direkt in Go integriert, sodass für Tests keine zusätzlichen Pakete oder Programme notwendig sind. Die Tests werden mit dem Befehl go test ausgeführt. Dabei ist es möglich, jeden Test einzeln oder alle Tests eines Projekts gleichzeitig auszuführen.

Für die Bezeichnung der Testfiles wird der zu testenden Datei _test angehängt. Für server.go würde man server_test.go anlegen. Diese Logik ist nicht neu und hat zur Folge, dass bei einer Sortierung nach dem Dateinamen die Tests immer bei den dazugehörigen Dateien stehen. Da Go sehr schnell kompiliert, ist die Ausführung der Tests entsprechend fix.

Gopher ante portas: Go hält Einzug auf JAXenter!

Wenige (relativ) neue Programmiersprachen erfreuen sich derzeit einer so großen Belibtheit wie Go. Googles Alternative für „C“ ist immer häufiger Zentrum interessanter Anwendungen und mittlerweile im Mainstream angekommen. Grund genug, sich ein wenig eingehender mit der Sprache zu befassen. Folgende Artikel, Kolumnen und Infografiken sind Teil unserer aktuellen Go-Aktion auf JAXenter:

Happy Gophing!

Für die Tests gibt es in der Standard-Library das Paket testing. Das Paket bietet, wie die Sprache Go, nur ein paar wichtige Funktionalitäten ohne viel Schnickschnack. Die Funktionen für Tests müssen immer die Form TestXxx(*testing.T) aufweisen. Innerhalb der Funktion werden die Tests mit einfachen if-Abfragen geprüft. Sollten die Werte ungleich der erwarteten Werte sein, so wird über t.Error() ein Fehler ausgegeben. Ein ganz einfacher Test in Go würde dabei aussehen wie in Listing 1. Das Listing zeigt die Einfachheit der Tests in Go. Die Sprachdesigner haben sich bewusst dafür entschieden, Tests nicht wesentlich von anderem Programmcode zu unterscheiden: Testcode ist auch Go-Code.

func TestFoo(t *testing.T) {
  got := foo("bar")
  want := "bar"
  if got != want {
  t.Errorf("foo() = %v, want %v", got, want)
  }
}

Wenn man innerhalb einer Testfunktion mehrere Tests durchführen möchte, so werden Subtests verwendet. Hierfür wird in der Funktion t.Run() ein String als Testname hinzugefügt. In Listing 2 ist das Beispiel aus der Dokumentation der Standard-Library zu sehen.

func TestFoo(t *testing.T) {
  // 
  t.Run("A=1", func(t *testing.T) { ... })
  t.Run("A=2", func(t *testing.T) { ... })
  t.Run("B=1", func(t *testing.T) { ... })
  // 
}

Um mehrere Testfälle abbilden zu können, haben sich sogenannte tabellarische Tests durchgesetzt. Hierfür wird eine anonyme Struktur definiert, welche die Inputwerte und die erwarteten Werte beinhaltet. Die einzelnen Testfälle werden dann innerhalb einer Schleife als Subtests abgearbeitet. Listing 3 zeigt die Testschleife, Listing 4 den Output.

func TestAdd(t *testing.T) {
  type args struct {
    a string
  }
  tests := []struct {
    name   string
    inputA int
    inputB int
    want   int
  }{
    {
      "a=0 b=1",
      0,
      1,
      1,
    },
    {
      "a=5 b=1",
      5,
      1,
      6,
    },
  }
  for _, tt := range tests {
    t.Run(tt.name, func(t *testing.T) {
      if got := add(tt.inputA, tt.inputB); got != tt.want {
        t.Errorf("add() = %v, want %v", got, tt.want)
      }
    })
  }
}
--- FAIL: TestAdd (0.00s)
  --- FAIL: TestAdd/a=5_b=1 (0.00s)
    go_testing\testing\foo_test.go:41: add() = 1, want 6
FAIL
FAIL testing 0.501s
Error: Tests failed.

Für das Erstellen von Tests gibt es gute Tools. Teile des Listing 3 wurden zum Beispiel mit gotests generiert. Dieses Tool erzeugt den Rahmen, sodass nur noch die Testfälle ergänzt werden müssen. Alle gängigen Editoren unterstützen mittlerweile das Generieren von Tests.

Neben dem Prüfen von Funktionen dienen Tests auch der Dokumentation. Gut geschriebene Tests dokumentieren den vorhandenen Code, da dort gezeigt wird, wie das API funktioniert. Bei den tabellarischen Tests leidet jedoch die Lesbarkeit ein wenig. Hier bietet das Testing-Paket die Möglichkeit, Beispiele anzulegen. Diese Beispiele bestehen nicht einfach aus leerem Beispielcode, sondern stellen auch echte Tests dar.

Die Beispiele werden innerhalb der Testfiles angelegt. Die Namenskonvention ist ExampleXxx(). Dabei wird kein extra Input benötigt, und es ist auch keine explizite Prüfung notwendig. Der erwartete Wert wird über Stdout geprüft. Im Code wird dafür am Ende ein Kommentar beginnend mit Output: angelegt:

func ExampleAdd() {
fmt.Println(add(2, 3))
// Output: 5
}

Die Beispiele aus der Testdatei werden dann innerhalb der Dokumentation ausgewiesen (Listing 5).

--- FAIL: ExampleAdd (0.00s)
got:
1
want:
5
FAIL
FAIL    go_testing/testing  0.847s
Error: Tests failed.

Test-Coverage

Ein vernünftiges Testprogramm sollte auch die Test-Coverage ausweisen. Selbstverständlich ist auch diese Funktion in Go integriert. Über den Befehl go test -cover wird ausgegeben, wie viel Prozent des Quellcodes durch die Tests abgedeckt sind:

go test -cover
PASS
coverage: 100.0% of statements
ok    testing    0.765s

Die Testabdeckung ist als reine Kennzahl sicher umstritten, da diese noch nicht viel darüber aussagt, welcher Code getestet wird. Bei einer Testabdeckung von weniger als 100 Prozent ist es wichtig zu wissen, welcher Teil des Codes getestet wird. Hierfür ist es möglich, sich die Testabdeckung visuell im Browser anzeigen zu lassen. Dafür sind zwei Befehle notwendig. Im ersten Schritt wird ein Coverprofile mit go test –coverprofile=cover.out angelegt:

go test --coverprofile=cover.out
PASS
coverage: 100.0% of statements
ok    jax_articel/testing    0.116s

Anschließend wird das Profil als .html-Datei ausgeben. Mit dem Befehl go tool cover –html=cover.out öffnet sich der Standardbrowser mit dem grafisch aufbereiteten Testprofil. Alle Dateien eines Projekts lassen sich im Browser abfragen (Abb. 1).

Abb. 1: Darstellung der Testabdeckung als html

Alle, die an dieser Stelle noch eine genauere Auswertung benötigen, können auch Heat Maps erzeugen. Diese zeigen, wie oft die einzelnen Statements in den Tests aufgerufen werden. Hierfür muss man bei der Erzeugung des Coverprofils das Flag covermode auf count setzten. Für das gezeigte Beispiel könnte der Befehl go test–covermode=count –coverprofile=count.out lauten. Alle diese Auswertungen sind ohne die Installation externer Tools möglich, alles ist Teil des Go-Standards.

Benchmarks

Ein weiterer Teil des testing-Pakets sind Benchmarks. Sie sind direkt in Go und das Go-Tooling integriert. Mit den Benchmarks hat man die Möglichkeit, einzelne Implementierungen bezüglich deren Geschwindigkeit zu vergleichen. Doch schauen wir uns zuerst die Syntax an. Analog zu den Testfunktionen haben Benchmarkfunktionen die Form BenchmarkXxx(*testing.B) und werden ebenfalls immer in den Testfiles angelegt. Über das Flag -bench werden die Benchmarks ausgeführt.

Innerhalb der Funktionen wird ein Loop angelegt, der b.N-mal läuft. Der Wert von b.N wird während des Benchmarks angepasst, bis der Lauf aussagekräftig genug ist. Wollen wir beispielsweise ausprobieren, welche Funktion ein Integer schneller zu einem String umwandelt, könnten wir Benchmarks wie in Listing 6 erstellen.

var a int = 1234

func BenchmarkSprintf(b *testing.B) {
  for i := 0; i < b.N; i++ {
    fmt.Sprintf("%d", a)
  }
}

func BenchmarkItoa(b *testing.B) {
  for i := 0; i < b.N; i++ {
    strconv.Itoa(a)
  }
}

Um die Benchmarks nun auszuführen, rufen wir go test mit der Benchmark-Flag auf.

go test -bench

Die Ausgabe zeigt Listing 7. Betrachten wir die Auswertung genauer. Dabei gibt goos das Betriebssystem und goarch die Architektur des Testsystems an. Die Auswertung des Benchmarks ist BenchmarkSprintf-4 10000000 237 ns/op.

goos: windows
goarch: amd64
pkg: go_testing/testing
BenchmarkSprintf-4    10000000      237 ns/op
BenchmarkItoa-4       20000000      68.5 ns/op
PASS
ok    go_testing/testing    7.387s

Die Zahl nach dem Namen gibt die Anzahl der Prozesse an, also auf wie vielen Kernen die Tests ausgeführt wurden. Mit dem Flag –cpu lässt sich die Anzahl der Prozesse verringern. Die nächste Zahl ist die Anzahl der Durchläufe, also der Wert von b.N. Am Ende findet sich die durchschnittliche Zeit pro Operation. Um nun kurz das Ergebnis des Beispiels auszuwerten: Man sollte für die Umwandlung eines Integers die Funktion strconv.Itoa() verwenden, da diese deutlich schneller ist.

Auf einfache Weise lassen sich so Fragen bezüglich unterschiedlicher Implementierungen beantworten. Hierfür müssen wir in Go nicht auf externe Benchmarks vertrauen, sondern können dies auf unserem eigenen System nachvollziehen.

Integration in Visual Studio Code

Der auf Electron basierende offene Editor Visual Studio Code (VS Code) bietet eine hervorragende Unterstützung für Go. Die zugehörige Erweiterung Go von lukehoban unterstützt nicht nur das Testing, sondern auch viele andere Tools. Nicht ohne Grund erfreut sich Electron in Verbindung mit der Erweiterung Go großer Beliebtheit in der Go-Community.

Am Beispiel von VS Code möchte ich die Integration des Test-Toolings in einem Editor darstellen. Andere Editors unterstützen natürlich auch das Go-Tooling, hier muss jeder seinen eigenen Favoriten finden.

VS Code erkennt anhand der Dateinamen die Testfiles und blendet in den Testfiles oberhalb der Funktionen kleine Links ein, über die gezielt einzelne Tests gestartet werden können. Dabei ist es sogar möglich, einzelne Tests über den Debugger zu starten. Dies erleichtert die Entwicklung ungemein, da so keine Testschnipsel oder Testausgaben im Code angelegt werden müssen. Voraussetzung für das Debuggen ist die Anweisung delve, die zwar noch nicht zu 100 Prozent implementiert ist, aber hierbei schon sehr gute Dienste leistet (Abb. 2).

Abb. 2: Run- und Debug-Test innerhalb von VS Code

Das bereits angesprochene Tool gotests lässt sich in VS Code per Rechtsklick ausführen und generiert dann ein Gerüst in das zugehörige Testfile der Datei. Dabei spielt es keine Rolle, ob das File bereits existiert und dort schon Tests vorhanden sind oder es noch gar kein Testfile gibt.

Das Anzeigen der Test-Coverage wird ebenfalls unterstützt. Per Rechtsklick ist es auch hier möglich, innerhalb des Editors die Testabdeckung darstellen zu lassen. Das bedeutet, dass hierfür nicht in das Terminal oder ein anderes Fenster gewechselt werden muss (Abb. 3).

Abb. 3: Darstellung der Testcoverage in VS Code

Testframeworks

Für Go gibt es mittlerweile auch eine große Anzahl an externen Frameworks. Hier sind einige der gängigen Pakete aufgelistet. Der Großteil der Tools verwendet Tests in der Form des testing-Pakets. Diese Pakete sind eher empfehlenswert als die zweite Gruppe, die eine eigene Logik implementiert haben. Denn für ein externes Test and Deploy, zum Beispiel über Travis CI, ist es notwendig, dass die Tests über den Standard ausgeführt werden können. Drei besonders interessante Tools möchte ich hier kurz vorstellen.

Testausgabe im Browser: GoConvey

GoConvey besteht aus zwei Teilen: Einerseits aus einem Server, der alle Dateien in einem Projekt überwacht und bei jeder Änderung die Tests laufen lässt. Andererseits bietet das Paket selber eine eigene Domain Specific Language, in der die Tests in einer beschreibenden Art angelegt werden können. Die dabei erzeugten Testdateien können auch über das Standardtooling per go test aufgerufen werden.

Das Komfortable an GoConvey ist der Server mit seiner übersichtlichen Browserausgabe. Auf der linken Seite ist dabei die Testabdeckung aller Pakete eines Projekts dargestellt. In der Mitte steht das Ergebnis der einzelnen Tests und rechts ein Log der letzten Aktivitäten. Über einfaches Durchklicken lassen sich hier die Testergebnisse ausgeben (Abb. 4).

Abb. 4: GoConvey im Einsatz

Durch die permanente Überwachung des Verzeichnisses ist mit GoConvey auch TDD (testgetriebene Entwicklung) möglich. So kann zuerst der Test für eine Funktion erstellt werden und dann die Funktion selbst. Insbesondere beim Schritt des Refactorings ist die Darstellung des Ergebnisses über alle Pakete eines Projekts hilfreich.

Ein kleiner Wermutstropfen ist bei GoConvey die Performance. Insbesondere bei größeren Projekten dauert es eine gewisse Zeit, bis alles Tests durchlaufen sind. Nichtsdestotrotz würde ich jedem empfehlen, sich dieses Paket näher anzusehen und auszuprobieren. Insbesondere für Go-Anfänger übernimmt GoConvey viele nützliche Aufgaben.

Testify – die fehlenden Asserts

Das Paket Testify  beinhaltet die fehlenden Assert-Funktionen. Da im Standard per if got != expected geprüft werden muss, haben viele Entwickler hierfür eigene Funktionen gebastelt. In Open-Source-Projekten wird für diese Prüfung häufig das Paket Testify verwendet. Die Testfiles sind wieder Standard-Go-Tests, doch die Syntax ist um einiges aufgeräumter. Listing 8 zeigt das Beispiel aus dem Readme des Projekts.

func TestSomething(t *testing.T) {

  // assert equality
  assert.Equal(t, 123, 123, "they should be equal")

  // assert inequality
  assert.NotEqual(t, 123, 456, "they should not be equal")

  // assert for nil (good for errors)
  assert.Nil(t, object)

  // assert for not nil (good when you expect something)
  if assert.NotNil(t, object) {

    // now we know that object isn't nil, we are safe to make
    // further assertions without causing any errors
    assert.Equal(t, "Something", object.Value)
  }
}

Mat Ryers is

Das Paket is von Mat Ryer ist relativ neu, jedoch beachtenswert, was die Syntax und die Funktionen betrifft. Inhaltlich löst es die fehlenden Asserts ähnlich wie Testify, verwendet dabei jedoch einen etwas anderen Ansatz. Anstatt assert wird is verwendet, und der Pointer auf testing.T wird nicht für jeden Test extra übergeben. Zusätzlich werden für die Beschreibung des Tests Kommentare und keine Strings verwendet. Aktuell ist das Paket zwar noch nicht sehr verbreitet. Doch ist Mat Ryer stark in der Go-Community verankert, weshalb man davon ausgehen kann, dass sein Paket auch in Zukunft unterstützt wird. Listing 9 zeigt wieder das Beispiel aus dem Readme zu dem Paket.

func Test(t *testing.T) {

  is := is.New(t)

  signedin, err := isSignedIn(ctx)
  is.NoErr(err)            // isSignedIn error
  is.Equal(signedin, true) // must be signed in

  body := readBody(r)
  is.True(strings.Contains(body, "Hi there"))
}

Fazit

Go als moderne Sprache benötigt keine externen Programme für das Testen. Die Tests werden einfach als typischer Go-Code implementiert. Testdateien werden durch den Compiler ignoriert und haben somit keinen negativen Einfluss auf den Build-Prozess. Zusätzlich zum Test unterstützt das testing-Paket auch Benchmarks, die den Entwickler ein weiteres mächtiges Werkzeug an die Hand geben, um eigene Implementierungen auf ihre Geschwindigkeit zu prüfen. Zusätzlich gibt es bereits viele Tools, die das Testen unterstützen und komfortabler machen. So werden Go-Tests auch von vielen Editoren unterstützt.

Zu Unit-Tests oder Test-driven Development gibt es viele Ansichten. Eine moderne Programmiersprache sollte das automatisierte Testen unterstützen. Go bietet hierfür ein stabiles Grundgerüst. Dadurch ist es nicht notwendig, externe Testtools zu verwenden. Die Vorteile zeigen sich sehr deutlich, wenn man unterschiedliche Open-Source-Projekte bezüglich der Tests betrachtet. Alle verwenden den Standard, und die Tests unterscheiden sich nur unwesentlich. Das Ziel, Go als Programmiersprache einfach zu halten, ist auch im Bereich des Testens gut gelungen.

Verwandte Themen:

Geschrieben von
Andreas Schröpfer
Andreas Schröpfer
Andreas Schröpfer ist seit über 10 Jahren in der IT-Beratung tätig und seit 2015 begeisterter Gopher. Er ist Contributor bei mehreren Open-Source-Projekten darunter unter anderem Go Buffalo. Er gibt Workshops zu Go und unterrichtet auch auf Udemy.
Kommentare

Hinterlasse einen Kommentar

Hinterlasse den ersten Kommentar!

avatar
400
  Subscribe  
Benachrichtige mich zu: