
In diesem Beitrag werde ich darüber sprechen, wie ich ein Konsolenprogramm in der Sprache Go zum Hochladen von Daten aus der Datenbank in Dateien geschrieben habe, um den gesamten Code mit 100% -Tests abzudecken. Ich beginne mit einer Beschreibung, warum ich dieses Programm brauchte. Ich werde weiterhin die ersten Schwierigkeiten beschreiben, von denen einige durch die Merkmale der Go-Sprache verursacht werden. Als nächstes werde ich ein wenig auf Travis CI aufbauen und dann darüber sprechen, wie ich Tests geschrieben habe, um den Code zu 100% abzudecken. Ich werde ein wenig auf das Testen der Arbeit mit der Datenbank und dem Dateisystem eingehen. Abschließend möchte ich sagen, wozu der Wunsch führt, den Code mit Tests abzudecken, und was dieser Indikator aussagt. Ich werde Material mit Links zu Dokumentationen und Beispielen für Commits aus meinem Projekt bereitstellen.
Programmzweck
Das Programm sollte über die Befehlszeile mit einer Angabe der Liste der Tabellen und einiger ihrer Spalten, einem Datenbereich in der ersten angegebenen Spalte, einer Aufzählung der Beziehungen der ausgewählten Tabellen zueinander und der Möglichkeit, eine Datei mit Datenbankverbindungseinstellungen anzugeben, gestartet werden. Das Ergebnis der Arbeit sollte eine Datei sein, die Anforderungen zum Erstellen der angegebenen Tabellen mit den angegebenen Spalten und Einfügeausdrücken der ausgewählten Daten beschreibt. Es wurde angenommen, dass die Verwendung eines solchen Programms das Szenario vereinfachen würde, einen Teil der Daten aus einer großen Datenbank zu extrahieren und diesen Teil lokal bereitzustellen. Außerdem sollten diese SQL-Dateien zum Entladen von einem anderen Programm verarbeitet werden, das einen Teil der Daten gemäß einer bestimmten Vorlage ersetzt.
Das gleiche Ergebnis kann mit jedem der gängigen Clients in der Datenbank und mit einem ausreichend großen manuellen Arbeitsaufwand erzielt werden. Die Anwendung sollte diesen Prozess vereinfachen und so weit wie möglich automatisieren.
Dieses Programm sollte von meinen Praktikanten zum Zweck der Ausbildung und anschließenden Verwendung in ihrer Weiterbildung entwickelt worden sein. Aber die Situation stellte sich so heraus, dass sie diese Idee ablehnten. Trotzdem habe ich mich entschlossen, in meiner Freizeit ein solches Programm zu schreiben, um meine Entwicklung in der Go-Sprache zu üben.
Die Lösung ist unvollständig und weist eine Reihe von Einschränkungen auf, die in README beschrieben sind. In jedem Fall ist dies kein Kampfprojekt.
Anwendungsbeispiele und Quellcode .
Erste Schwierigkeiten
Die Liste der Tabellen und ihrer Spalten wird als Argument in Form einer Zeichenfolge an das Programm übergeben, dh es ist nicht im Voraus bekannt. Die meisten Beispiele für die Arbeit mit einer Datenbank unter Go implizierten, dass die Datenbankstruktur im Voraus bekannt ist. Wir erstellen einfach eine struct
, die die Typen jeder Spalte angibt. In diesem Fall funktioniert dies jedoch nicht.
Die Lösung hierfür bestand darin, die MapScan
Methode von github.com/jmoiron/sqlx
, mit der ein Schnittstellenschnitt in der Größe erstellt wurde, die der Anzahl der Beispielspalten entspricht. Die nächste Frage war, wie man von diesen Schnittstellen einen echten Datentyp erhält. Die Lösung ist ein Schaltkasten nach Typ . Eine solche Lösung sieht nicht sehr schön aus, da alle Typen in die Zeichenfolge umgewandelt werden müssen: Ganzzahlen wie sie sind, Zeichenfolgen, die maskiert und in Anführungszeichen gesetzt werden sollen, aber gleichzeitig alle Typen beschreiben, die möglicherweise aus der Datenbank stammen. Ich habe keinen eleganteren Weg gefunden, um dieses Problem zu lösen.
Bei den Typen wurde auch eine Go-Sprachfunktion manifestiert - eine Variable vom Typ string kann nicht den Wert nil
annehmen, aber sowohl eine leere Zeichenfolge als auch NULL
können aus der Datenbank stammen. Um dieses Problem zu lösen, gibt es eine Lösung im database/sql
Paket - verwenden Sie eine spezielle strut
, die den Wert und das Vorzeichen speichert, unabhängig davon, ob es NULL
oder nicht.
Zusammenstellung und Berechnung des Prozentsatzes der Codeabdeckung durch Tests
Für die Montage verwende ich Travis CI, um den Prozentsatz der Codeabdeckung mit Tests zu ermitteln - Overalls. Die Datei .travis.yml
für die Assembly ist recht einfach:
language: go go: - 1.9 script: - go get -t -v ./... - go get golang.org/x/tools/cmd/cover - go get github.com/mattn/goveralls - go test -v -covermode=count -coverprofile=coverage.out ./... - $HOME/gopath/bin/goveralls -coverprofile=coverage.out -service=travis-ci -repotoken $COVERALLS_TOKEN
In den Travis CI-Einstellungen müssen Sie nur die Umgebungsvariable COVERALLS_TOKEN
, deren Wert auf der Site übernommen werden muss.
Mit Overalls können Sie bequem herausfinden, wie viel Prozent des gesamten Projekts für jede Datei eine Zeile Quellcode hervorheben, die sich als ungedeckter Test herausstellte. Zum Beispiel ist im ersten Build klar, dass ich beim Parsen einer Benutzeranforderung keine Tests für einige Fehlerfälle geschrieben habe.
Eine 100% ige Abdeckung des Codes bedeutet, dass Tests geschrieben werden, die unter anderem Code für jeden Zweig in if
ausführen. Dies ist die umfangreichste Arbeit beim Schreiben von Tests und im Allgemeinen beim Entwickeln einer Anwendung.
Sie können die Abdeckung mit Tests lokal berechnen, z. B. mit dem gleichen go test -v -covermode=count -coverprofile=coverage.out ./...
, aber Sie können dies in CI besser tun. Sie können eine Platte auf Github platzieren.
Da es sich um Würfel handelt, finde ich die Würfel von https://goreportcard.com nützlich, die die folgenden Indikatoren analysieren:
- gofmt - Code-Formatierung, einschließlich Vereinfachung von Konstruktionen
- go_vet - prüft verdächtige Konstrukte
- gocyclo - zeigt Probleme in der zyklomatischen Komplexität
- golint - für mich geht es darum, die Verfügbarkeit aller notwendigen Kommentare zu überprüfen
- Lizenz - Das Projekt muss eine Lizenz haben
- ineffassign - prüft auf ineffektive Zuordnungen
- Rechtschreibfehler - prüft auf Tippfehler
Schwierigkeiten beim Abdecken des Codes mit Tests 100%
Wenn das Parsen einer kleinen Benutzeranforderung für Komponenten hauptsächlich mit dem Konvertieren von Zeichenfolgen in einige Strukturen aus Zeichenfolgen funktioniert und durch Tests ziemlich einfach abgedeckt wird, ist die Lösung für das Testen von Code, der mit einer Datenbank funktioniert, nicht so offensichtlich.
Alternativ können Sie eine Verbindung zu einem echten Datenbankserver herstellen, die Daten in jedem Test vorab füllen, eine Auswahl treffen und löschen. Dies ist jedoch eine schwierige Lösung, weit entfernt von Unit-Tests und stellt ihre Anforderungen an die Umgebung, einschließlich des CI-Servers.
Eine andere Option könnte darin bestehen, eine Datenbank im Speicher zu verwenden, z. B. sqlite ( sqlx.Open("sqlite3", ":memory:")
). Dies impliziert jedoch, dass der Code so schwach wie möglich an das Datenbankmodul gebunden sein sollte, was das Projekt erheblich verkompliziert aber für den Integrationstest ist das ganz gut.
Für Unit-Tests ist die Verwendung von Mock für die Datenbank geeignet. Ich habe diesen gefunden. Mit diesem Paket können Sie das Verhalten sowohl bei einem normalen Ergebnis als auch bei Fehlern testen und angeben, welche Anforderung welchen Fehler zurückgeben soll.
Das Schreiben von Tests hat gezeigt, dass die Funktion, die eine Verbindung zur realen Datenbank herstellt, nach main.go verschoben werden muss, damit sie in Tests für die Funktion neu definiert werden kann, die die Scheininstanz zurückgibt.
Neben der Arbeit mit der Datenbank ist es erforderlich, die Arbeit mit dem Dateisystem zu einer separaten Abhängigkeit zu machen. Auf diese Weise kann die Aufzeichnung realer Dateien durch Schreiben in den Speicher ersetzt werden, um das Testen zu vereinfachen und die Kopplung zu verringern. So erschien die FileWriter
Oberfläche und damit die Schnittstelle der zurückgegebenen Datei. Um die filewriter_test.go
zu testen, wurden zusätzliche Implementierungen dieser Schnittstellen erstellt und in der Datei filewriter_test.go
abgelegt, damit sie nicht in den allgemeinen Build fallen, sondern in Tests verwendet werden können.
Nach einiger Zeit hatte ich eine Frage, wie man main()
Tests abdeckt. Zu dieser Zeit hatte ich dort genug Code. Wie die Suchergebnisse zeigten, wird dies in Go nicht durchgeführt . Stattdessen muss der gesamte Code, der aus main()
abgerufen werden kann, abgerufen werden. In meinem Code habe ich nur Analyseoptionen und Befehlszeilenargumente ( flag
Paket) belassen, eine Verbindung zur Datenbank hergestellt, ein Objekt instanziiert, das Dateien schreibt, und eine Methode aufgerufen, die den Rest der Arbeit erledigt. Mit diesen Leitungen erhalten Sie jedoch keine exakte Abdeckung von 100%.
Beim Testen von Go gibt es so etwas wie " Beispielfunktionen ". Dies sind Testfunktionen, die die Ausgabe mit dem vergleichen, was im Kommentar innerhalb einer solchen Funktion beschrieben ist. Beispiele für solche Tests finden Sie im Quellcode für go-Pakete . Wenn solche Dateien keine Tests und Benchmarks enthalten, werden sie mit dem Präfix example_
und enden mit _test.go
. Der Name jeder solchen Testfunktion sollte mit Example
beginnen. Dazu habe ich einen Test für ein Objekt geschrieben, das SQL in eine Datei schreibt, wobei der reale Datensatz in der Datei durch einen Mock ersetzt wurde, aus dem Sie den Inhalt abrufen und anzeigen können. Diese Schlussfolgerung wird mit dem Standard verglichen. Praktischerweise müssen Sie keinen Vergleich mit Ihren Händen schreiben, und es ist praktisch, mehrere Zeilen in Kommentare zu schreiben. Beim Test für ein Objekt, das Daten in eine CSV-Datei schreibt, traten jedoch Schwierigkeiten auf. Gemäß RFC4180 müssen Zeilen in CSV durch CRLF getrennt werden, und go fmt
ersetzt alle Zeilen durch LF, was dazu führt, dass der Standard aus dem Kommentar aufgrund unterschiedlicher Zeilentrennzeichen nicht mit der tatsächlichen Ausgabe übereinstimmt. Ich musste einen regelmäßigen Test für dieses Objekt schreiben und gleichzeitig die Datei example_
indem example_
daraus entfernte.
Die Frage bleibt, ob die Datei beispielsweise query.go
sowohl mit Beispieltests als auch mit herkömmlichen Tests getestet wird. query.go
zwei Dateien example_query_test.go
und query_test.go
? Hier gibt es zum Beispiel nur ein example_test.go
. Die Suche nach "go test example" macht immer noch Spaß.
Ich habe gelernt, Tests in Go gemäß den Anleitungen zu schreiben, die Google für "Go Writing Tests" gibt. Die meisten, auf die ich gestoßen bin ( 1 , 2 , 3 , 4 ), schlagen vor, das Ergebnis mit dem erwarteten Design des Formulars zu vergleichen
if v != 1.5 { t.Error("Expected 1.5, got ", v) }
Wenn es jedoch darum geht, Typen zu vergleichen, entwickelt sich ein bekanntes Konstrukt evolutionär zu einem Haufen von "Reflect" - oder Typ-Assertion. Oder ein anderes Beispiel, wenn Sie überprüfen müssen, ob das Slice oder die Map den erforderlichen Wert hat. Der Code wird umständlich. Also möchte ich meine Hilfsfunktionen für den Test schreiben. Obwohl eine gute Lösung hier darin besteht, eine Bibliothek zum Testen zu verwenden. Ich habe https://github.com/stretchr/testify gefunden. Sie können Vergleiche in einer einzigen Zeile durchführen . Diese Lösung reduziert die Codemenge und vereinfacht das Lesen und die Unterstützung von Tests.
Codefragmentierung und -tests
Wenn Sie einen Test für eine übergeordnete Funktion schreiben, die mit mehreren Objekten arbeitet, können Sie den Wert der Codeabdeckung durch Tests gleichzeitig erheblich erhöhen, da während dieses Tests viele Codezeilen einzelner Objekte ausgeführt werden. Wenn Sie sich das Ziel setzen, nur eine 100% ige Abdeckung zu erreichen, verschwindet die Motivation, Komponententests für kleine Komponenten des Systems zu schreiben, da dies den Wert der Codeabdeckung nicht beeinflusst.
Wenn Sie das Ergebnis in der Testfunktion nicht überprüfen, wirkt sich dies auch nicht auf den Wert der Codeabdeckung aus. Sie können einen hohen Abdeckungswert erzielen, jedoch keine schwerwiegenden Fehler in der Anwendung erkennen.
Wenn Sie dagegen Code mit vielen Zweigen haben , nach dem eine umfangreiche Funktion aufgerufen wird, ist es schwierig, ihn mit Tests abzudecken. Und hier haben Sie einen Anreiz, diesen Code zu verbessern, zum Beispiel alle Zweige in eine separate Funktion zu nehmen und einen separaten Test darauf zu schreiben. Dies wirkt sich positiv auf die Lesbarkeit des Codes aus.
Wenn der Code eine starke Kopplung aufweist, können Sie höchstwahrscheinlich keinen Test darauf schreiben. Dies bedeutet, dass Sie Änderungen daran vornehmen müssen, was sich positiv auf die Qualität des Codes auswirkt.
Fazit
Vor diesem Projekt musste ich mir kein Ziel setzen, den Code mit Tests zu 100% abzudecken. Ich konnte in 10 Stunden Entwicklungszeit eine funktionierende Anwendung erhalten, aber ich brauchte 20 bis 30 Stunden, um eine Abdeckung von 95% zu erreichen. Anhand eines kleinen Beispiels habe ich eine Vorstellung davon bekommen, wie sich der Wert der Codeabdeckung auf die Qualität auswirkt und wie viel Aufwand für die Wartung erforderlich ist.
Mein Fazit ist, dass wenn Sie ein Dashboard mit einem hohen Wert für die Codeabdeckung für jemanden sehen, es fast nichts darüber aussagt, wie gut diese Anwendung getestet wurde. Auf jeden Fall müssen Sie die Tests selbst ansehen. Aber wenn Sie selbst einen Kurs für ehrliche 100% festgelegt haben, hilft Ihnen dies, eine Bewerbung besser zu schreiben.
Weitere Informationen hierzu finden Sie in den folgenden Materialien und Kommentaren dazu:
SpoilerDas Wort "Beschichtung" wird ungefähr 20 Mal verwendet. Entschuldigung.