5 Fortgeschrittene Go-Testtechniken

Gruß an alle! Bis zum Beginn des Kurses „Golang Developer“ verbleibt weniger als eine Woche, und wir teilen weiterhin nützliches Material zu diesem Thema. Lass uns gehen!



Go verfügt über eine gute und zuverlässige integrierte Bibliothek zum Testen. Wenn Sie auf Go schreiben, wissen Sie das bereits. In diesem Artikel werden wir über verschiedene Strategien sprechen, mit denen Sie Ihre Testfähigkeiten mit Go verbessern können. Durch die Erfahrung mit dem Schreiben unserer beeindruckenden Codebasis auf Go haben wir gelernt, dass diese Strategien wirklich funktionieren und somit Zeit und Mühe bei der Arbeit mit dem Code sparen.

Verwenden Sie Testsuiten

Wenn Sie aus diesem Artikel nur eine nützliche Sache für sich selbst lernen, muss es sich um die Verwendung von Testsuiten handeln. Für diejenigen, die mit diesem Konzept nicht vertraut sind, ist das Testen mit Kits der Prozess der Entwicklung eines Tests zum Testen einer gemeinsamen Schnittstelle, die für viele Implementierungen dieser Schnittstelle verwendet werden kann. Unten sehen Sie, wie wir verschiedene Thinger Implementierungen bestehen und mit denselben Tests ausführen.

 type Thinger interface { DoThing(input string) (Result, error) } // Suite tests all the functionality that Thingers should implement func Suite(t *testing.T, impl Thinger) { res, _ := impl.DoThing("thing") if res != expected { t.Fail("unexpected result") } } // TestOne tests the first implementation of Thinger func TestOne(t *testing.T) { one := one.NewOne() Suite(t, one) } // TestOne tests another implementation of Thinger func TestTwo(t *testing.T) { two := two.NewTwo() Suite(t, two) } 

Glückliche Leser haben mit Codebasen gearbeitet, die diese Methode verwenden. Häufig in Plugin-basierten Systemtests verwendet, die zum Testen einer Schnittstelle geschrieben wurden, können alle Implementierungen dieser Schnittstelle verwendet werden, um zu verstehen, wie sie die Verhaltensanforderungen erfüllen.

Die Verwendung dieses Ansatzes kann möglicherweise dazu beitragen, Stunden, Tage und sogar genug Zeit zu sparen, um das Problem der Gleichheit der Klassen P und NP zu lösen. Wenn Sie ein Basissystem durch ein anderes ersetzen, müssen Sie nicht mehr (eine große Anzahl) zusätzlicher Tests schreiben, und es besteht auch die Gewissheit, dass dieser Ansatz den Betrieb Ihrer Anwendung nicht stört. Implizit müssen Sie eine Schnittstelle erstellen, die den Bereich des getesteten Bereichs definiert. Mithilfe der Abhängigkeitsinjektion können Sie einen Satz aus einem Paket anpassen, das an die Implementierung des gesamten Pakets übergeben wird.

Ein vollständiges Beispiel finden Sie hier . Trotz der Tatsache, dass dieses Beispiel weit hergeholt ist, kann man sich vorstellen, dass eine Datenbank remote und die andere im Speicher ist.

Ein weiteres cooles Beispiel für diese Technik befindet sich in der Standardbibliothek im Paket golang.org/x/net/nettest . Es bietet eine Möglichkeit zu überprüfen, ob net.Conn die Schnittstelle erfüllt.

Kontamination der Schnittstelle vermeiden

Sie können nicht über das Testen in Go sprechen, aber nicht über Schnittstellen.

Schnittstellen sind im Zusammenhang mit Tests wichtig, da sie das leistungsstärkste Werkzeug in unserem Testarsenal sind. Sie müssen sie daher korrekt verwenden.

Pakete exportieren häufig Schnittstellen an Entwickler, und dies führt dazu, dass:

A) Entwickler erstellen ihr eigenes Modell, um das Paket zu implementieren.
B) Das Paket exportiert sein eigenes Modell.

"Je größer die Schnittstelle, desto schwächer die Abstraktion"
- Rob Pike, Sprüche von Go

Schnittstellen müssen vor dem Export sorgfältig geprüft werden. Es ist oft verlockend, Schnittstellen zu exportieren, um Benutzern die Möglichkeit zu geben, das von ihnen benötigte Verhalten zu simulieren. Dokumentieren Sie stattdessen, welche Schnittstellen zu Ihren Strukturen passen, um keine enge Beziehung zwischen dem Verbraucherpaket und Ihrem eigenen herzustellen. Ein gutes Beispiel dafür ist das Fehlerpaket .

Wenn wir eine Schnittstelle haben, die wir nicht exportieren möchten, können wir den internen Teilbaum / Paket-Teilbaum verwenden , um sie im Paket zu speichern. Wir können daher nicht befürchten, dass der Endbenutzer von ihm abhängig ist, und können daher die Benutzeroberfläche flexibel an neue Anforderungen anpassen. Normalerweise erstellen wir Schnittstellen mit externen Abhängigkeiten, um Tests lokal ausführen zu können.

Dieser Ansatz ermöglicht es dem Benutzer, seine eigenen kleinen Schnittstellen zu implementieren, indem er einfach einen Teil der Bibliothek zum Testen umschließt. Weitere Informationen zu diesem Konzept finden Sie im Rakyl-Artikel zur Grenzflächenverschmutzung .

Exportieren Sie keine Parallelitätsprimitive

Go bietet benutzerfreundliche Parallelitätsprimitive, die aufgrund der gleichen Einfachheit manchmal auch zu einer Überbeanspruchung führen können. Zunächst sind wir besorgt über die Kanäle und das Synchronisierungspaket. Manchmal ist es verlockend, einen Kanal aus Ihrem Paket zu exportieren, damit andere ihn verwenden können. Außerdem besteht ein häufiger Fehler darin, sync.Mutex einzubetten, ohne es auf privat zu setzen. Dies ist wie üblich nicht immer schlecht, führt jedoch beim Testen Ihres Programms zu bestimmten Problemen.

Wenn Sie Kanäle exportieren, verkomplizieren Sie zusätzlich das Leben des Paketbenutzers, was sich nicht lohnt. Sobald der Kanal aus dem Paket exportiert wird, treten beim Testen desjenigen, der diesen Kanal verwendet, Schwierigkeiten auf. Für erfolgreiche Tests muss der Benutzer Folgendes wissen:

  • Wenn Daten über den Kanal gesendet werden.
  • Gab es Fehler beim Empfang von Daten?
  • Wie spült ein Paket den Kanal nach Abschluss, wenn überhaupt?
  • Wie verpacke ich eine Paket-API, damit Sie sie nicht direkt aufrufen?

Schauen Sie sich das Beispiel zum Lesen der Warteschlange an. Hier ist eine Beispielbibliothek, die aus der Warteschlange liest und dem Benutzer einen Feed zum Lesen bereitstellt.

 type Reader struct {...} func (r *Reader) ReadChan() <-chan Msg {...} 

Jetzt möchte Ihr Bibliotheksbenutzer einen Test für seinen Verbraucher implementieren:

 func TestConsumer(t testing.T) { cons := &Consumer{ r: libqueue.NewReader(), } for msg := range cons.r.ReadChan() { // Test thing. } } 


Der Benutzer kann dann entscheiden, dass die Abhängigkeitsinjektion eine gute Idee ist, und seine eigenen Nachrichten in den Kanal schreiben:

 func TestConsumer(t testing.T, q queueIface) { cons := &Consumer{ r: q, } for msg := range cons.r.ReadChan() { // Test thing. } } 


Warten Sie, was ist mit den Fehlern?

 func TestConsumer(t testing.T, q queueIface) { cons := &Consumer{ r: q, } for { select { case msg := <-cons.r.ReadChan(): // Test thing. case err := <-cons.r.ErrChan(): // What caused this again? } } } 


Jetzt müssen wir irgendwie Ereignisse generieren, um tatsächlich in diesen Stub zu schreiben, der das Verhalten der von uns verwendeten Bibliothek repliziert. Wenn die Bibliothek gerade die synchrone API geschrieben hat, können wir dem Client-Code die gesamte Parallelität hinzufügen, sodass das Testen einfacher wird.

 func TestConsumer(t testing.T, q queueIface) { cons := &Consumer{ r: q, } msg, err := cons.r.ReadMsg() // handle err, test thing } 


Wenn Sie Zweifel haben, denken Sie daran, dass es immer einfach ist, dem Verbraucherpaket (Verbrauchspaket) Parallelität hinzuzufügen, und es nach dem Export aus der Bibliothek schwierig oder unmöglich ist, es zu entfernen. Und vor allem vergessen Sie nicht, in die Paketdokumentation zu schreiben, ob die Struktur / das Paket für den gleichzeitigen Zugriff auf mehrere Goroutinen sicher ist.
Manchmal ist es immer noch wünschenswert oder notwendig, den Kanal zu exportieren. Um einige der oben genannten Probleme zu mindern, können Sie Kanäle über Accessoren anstelle des direkten Zugriffs bereitstellen und diese nur zum Lesen ( ←chan ) oder nur zum Schreiben ( chan← ) offen lassen, wenn Sie deklarieren.

Verwenden Sie net/http/httptest

Httptest können Httptest http.Handler Code ausführen, ohne einen Server zu starten oder an einen Port zu binden. Dies beschleunigt das Testen und ermöglicht es Ihnen, Tests zu geringeren Kosten parallel durchzuführen.

Hier ist ein Beispiel für denselben Test, der auf zwei Arten implementiert wurde. Hier gibt es nichts Großartiges, aber dieser Ansatz reduziert die Codemenge und spart Ressourcen.

 func TestServe(t *testing.T) { // The method to use if you want to practice typing s := &http.Server{ Handler: http.HandlerFunc(ServeHTTP), } // Pick port automatically for parallel tests and to avoid conflicts l, err := net.Listen("tcp", ":0") if err != nil { t.Fatal(err) } defer l.Close() go s.Serve(l) res, err := http.Get("http://" + l.Addr().String() + "/?sloths=arecool") if err != nil { log.Fatal(err) } greeting, err := ioutil.ReadAll(res.Body) res.Body.Close() if err != nil { log.Fatal(err) } fmt.Println(string(greeting)) } func TestServeMemory(t *testing.T) { // Less verbose and more flexible way req := httptest.NewRequest("GET", "http://example.com/?sloths=arecool", nil) w := httptest.NewRecorder() ServeHTTP(w, req) greeting, err := ioutil.ReadAll(w.Body) if err != nil { log.Fatal(err) } fmt.Println(string(greeting)) } 

Das vielleicht wichtigste Merkmal ist, dass Sie mit httptest den Test nur in die Funktion httptest können, die Sie testen möchten. Keine Router, Middleware oder andere Nebenwirkungen, die beim Einrichten von Servern, Diensten, Prozessorfabriken, Prozessorfabriken oder anderen Dingen auftreten, die Sie für eine gute Idee halten.

Um dieses Prinzip in Aktion zu sehen, lesen Sie den Artikel von Marc Berger .

Verwenden Sie das separate Paket _test

Die meisten Tests im Ökosystem werden in den Dateien pkg_test.go , verbleiben jedoch im selben Paket: package pkg . Ein separates foo_test.go ist das Paket, das Sie in der neuen Datei foo_test.go im Verzeichnis des zu foo_test.go Moduls foo/ mit dem Deklarationspaket package foo_test . Von hier aus können Sie github.com/example/foo und andere Abhängigkeiten importieren. Mit dieser Funktion können Sie viele Dinge tun. Dies ist die empfohlene Lösung für zyklische Abhängigkeiten in Tests. Sie verhindert das Auftreten von „spröden Tests“ und gibt dem Entwickler das Gefühl, wie es ist, ein eigenes Paket zu verwenden. Wenn Ihr Paket schwer zu verwenden ist, ist das Testen mit dieser Methode ebenfalls schwierig.

Diese Strategie verhindert fragile Tests, indem der Zugriff auf private Variablen eingeschränkt wird. Insbesondere wenn Ihre Tests unterbrochen werden und Sie separate Testpakete verwenden, ist fast garantiert, dass ein Client, der eine Funktion verwendet, die die Tests unterbricht, auch beim Aufruf unterbrochen wird.

Schließlich hilft es, Importzyklen in Tests zu vermeiden. Die meisten Pakete hängen eher von anderen Paketen ab, die Sie neben den Testpaketen geschrieben haben. Daher kommt es zu einer Situation, in der der Importzyklus auf natürliche Weise erfolgt. Ein externes Paket befindet sich über beiden Paketen in der Pakethierarchie. Nehmen Sie ein Beispiel aus der Programmiersprache Go (Kapitel 11, Abschnitt 2.4), in dem net/url einen URL-Parser implementiert, den net/http zur Verwendung importiert. net / url muss jedoch mit einem realen Anwendungsfall getestet werden, indem net / http importiert wird. Somit stellt sich net/url_test .

Wenn Sie jetzt ein separates Testpaket verwenden, benötigen Sie möglicherweise Zugriff auf nicht exportierte Entitäten in dem Paket, in dem sie zuvor verfügbar waren. Einige Entwickler sind zum ersten Mal damit konfrontiert, wenn sie etwas basierend auf der Zeit testen (z. B. time.Now wird mithilfe einer Funktion zu einem Stub). In diesem Fall können wir eine zusätzliche Datei verwenden, um Entitäten ausschließlich während des Testens bereitzustellen, da die _test.go Dateien von regulären Builds ausgeschlossen _test.go .

Woran müssen Sie sich erinnern?

Es ist wichtig zu bedenken, dass keine der oben beschriebenen Methoden ein Allheilmittel ist. Der beste Ansatz in jedem Unternehmen besteht darin, die Situation kritisch zu analysieren und unabhängig die beste Lösung für das Problem auszuwählen.

Möchten Sie mehr über das Testen mit Go erfahren?
Lesen Sie diese Artikel:

Dave Cheneys schreibtischgesteuerte Tests in Go
Das Kapitel Go Programming Language zum Testen.
Oder schauen Sie sich diese Videos an:
Hashimotos Advanced Testing With Go-Vortrag von Gophercon 2017
Andrew Gerrands Testtechniken sprechen von 2014

Wir hoffen, diese Übersetzung hat Ihnen geholfen. Wir warten auf Kommentare und alle, die mehr über den Kurs erfahren möchten, laden Sie zum Tag der offenen Tür ein , der am 23. Mai stattfinden wird.

Source: https://habr.com/ru/post/de452772/


All Articles