
Niemand schreibt gerne Tests. Natürlich scherze ich, jeder liebt es, sie zu schreiben! Wie die Teamleiter und die Personalabteilung sagen werden, ist die richtige Antwort bei den Interviews, dass ich Tests wirklich liebe und schreibe. Aber plötzlich schreibst du gerne Tests in einer anderen Sprache. Wie fängst du an, testbezogenen Go-Code zu schreiben?
Teil 1. Testen des Handlers
In "net / http" wird der http-Server sofort unterstützt, sodass Sie ihn mühelos aufheben können. Die sich bietenden Möglichkeiten ermöglichen es uns, uns extrem mächtig zu fühlen, und daher wird unser Code den 42. Benutzer zurückgeben.
func userHandler(w http.ResponseWriter, r *http.Request) { var user User userId, err := strconv.Atoi(r.URL.Query().Get("id")) if err != nil { w.Write([]byte( "Error")) return } if userId == 42 { user = User{userId, "Jack", 2} } jsonData, _ := json.Marshal(user) w.Write(jsonData) } type User struct { Id int Name string Rating uint }
Dieser Code empfängt den Benutzer-ID-Parameter als Eingabe, emuliert dann die Anwesenheit des Benutzers in der Datenbank und gibt zurück. Jetzt müssen wir es testen ...
Es gibt eine wunderbare Sache "net / http / httptest", mit der Sie einen Anruf an unseren Handler simulieren und dann die Antwort vergleichen können.
r := httptest.NewRequest("GET", "http://127.0.0.1:80/user?id=42", nil) w := httptest.NewRecorder() userHandler(w, r) user := User{} json.Unmarshal(w.Body.Bytes(), &user) if user.Id != 42 { t.Errorf("Invalid user id %d expected %d", user.Id, 42) }
Teil 2. Schatz, wir haben hier eine externe API
Und warum müssen wir Luft holen, wenn wir uns gerade aufgewärmt haben? In unseren Diensten wird früher oder später eine externe API angezeigt. Dies ist ein seltsames, oft verstecktes Tier, das sich so verhalten kann, wie es ihm gefällt. Für Tests wünschen wir uns einen entgegenkommenderen Kollegen. Und unser kürzlich entdeckter httptest wird uns auch hier helfen. Beispielsweise ist der Anrufcode eine externe API mit Datenübertragung weiter.
func ApiCaller(user *User, url string) error { resp, err := http.Get(url) if err != nil { return err } defer resp.Body.Close() return updateUser(user, resp.Body) }
Um dies zu verhindern, können wir einen externen API-Mock erstellen. Die einfachste Option ist:
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json; charset=utf-8") w.Header().Set("Access-Control-Allow-Origin", "*") fmt.Fprintln(w, `{ "result": "ok", "data": { "user_id": 1, "rating": 42 } }`) })) defer ts.Close() user := User{id: 1} err := ApiCaller(&user, ts.URL)
ts.URL enthält eine Zeichenfolge im Format "http: //127.0.0.1: 49799", die das API-Modell ist, das unsere Implementierung aufruft
Teil 3. Lassen Sie uns mit der Basis arbeiten
Es gibt einen einfachen Weg: Den Docker mit der Basis anheben, Migrationen und Vorrichtungen rollen und unseren exzellenten Service ausführen. Versuchen wir jedoch, Tests mit einem Minimum an Abhängigkeiten von externen Diensten zu schreiben.
Durch die Implementierung der Arbeit mit der Base in Go können Sie den Treiber selbst ersetzen. Unter Umgehung von 100 Seiten Code und Reflexion empfehlen wir Ihnen, die Bibliothek
github.com/DATA-DOG/go-sqlmock zu verwendenSie können mit sql.Db auf dem Dock umgehen. Nehmen wir ein etwas interessanteres Beispiel, in dem es eine Form
geben wird .
func DbListener(db *gorm.DB) { user := User{} transaction := db.Begin() transaction.First(&user, 1) transaction.Model(&user).Update("counter", user.Counter+1) transaction.Commit() }
Ich hoffe, dieses Beispiel hat Sie zumindest dazu gebracht, darüber nachzudenken, wie Sie es testen können. In "mock.ExpectExec" können Sie einen regulären Ausdruck ersetzen, der den von Ihnen benötigten Fall abdeckt. Das einzige, woran Sie sich erinnern sollten, ist, dass die Reihenfolge, in der die Erwartung festgelegt wird, mit der Reihenfolge und Anzahl der Anrufe übereinstimmen muss.
func TestDbListener(t *testing.T) { db, mock, _ := sqlmock.New() defer db.Close() mock.ExpectBegin() result := []string{"id", "name", "counter"} mock.ExpectQuery("SELECT \\* FROM `Users`").WillReturnRows(sqlmock.NewRows(result).AddRow(1, "Jack", 2)) mock.ExpectExec("UPDATE `Users`").WithArgs(3, 1).WillReturnResult(sqlmock.NewResult(1, 1)) mock.ExpectCommit() gormDB, _ := gorm.Open("mysql", db) DbListener(gormDB.LogMode(true)) if err := mock.ExpectationsWereMet(); err != nil { t.Errorf("there were unfulfilled expectations: %s", err) } }
Ich habe hier viele Beispiele zum Testen der Basis
gefunden .
Teil 4. Arbeiten mit dem Dateisystem
Wir haben uns in verschiedenen Bereichen versucht und uns versöhnt, dass alles gut ist, um nass zu werden. Hier ist nicht alles so klar. Ich schlage zwei Ansätze vor, nass zu werden oder das Dateisystem zu verwenden.
Option 1 - wir werden alle auf
github.com/spf13/afero nass
Vorteile :
- Sie müssen nichts wiederholen, wenn Sie diese Bibliothek bereits verwenden. (aber dann langweilt man sich beim Lesen)
- Arbeiten mit einem virtuellen Dateisystem, das Ihre Tests erheblich beschleunigt.
Nachteile :
- Eine Änderung des vorhandenen Codes ist erforderlich.
- Der chmod funktioniert nicht im virtuellen Dateisystem. Aber es kann seitdem Features geben In der Dokumentation heißt es: "Vermeiden Sie Sicherheitsprobleme und Berechtigungen."
Von diesen wenigen Punkten habe ich sofort 2 Tests durchgeführt. In der Version mit dem Dateisystem habe ich eine unlesbare Datei erstellt und die Funktionsweise des Systems überprüft.
func FileRead(path string) error { path = strings.TrimRight(path, "/") + "/" // files, err := ioutil.ReadDir(path) if err != nil { return fmt.Errorf("cannot read from file, %v", err) } for _, f := range files { deleteFileName := path + f.Name() _, err := ioutil.ReadFile(deleteFileName) if err != nil { return err } err = os.Remove(deleteFileName) // } return nil }
Die Verwendung von afero.Fs erfordert nur minimale Änderungen, aber grundsätzlich ändert sich nichts am Code
func FileReadAlt(path string, fs afero.Fs) error { path = strings.TrimRight(path, "/") + "/" // files, err := afero.ReadDir(fs, path) if err != nil { return fmt.Errorf("cannot read from file, %v", err) } for _, f := range files { deleteFileName := path + f.Name() _, err := afero.ReadFile(fs, deleteFileName) if err != nil { return err } err = fs.Remove(deleteFileName) // } return nil }
Aber unser Spaß wird nicht vollständig sein, wenn wir nicht herausfinden, wie viel schneller Afero als Native ist.
Benchmark-Minute:
BenchmarkIoutil 5000 242504 ns/op 7548 B/op 27 allocs/op BenchmarkAferoOs 300000 4259 ns/op 2144 B/op 30 allocs/op BenchmarkAferoMem 300000 4169 ns/op 2144 B/op 30 allocs/op
Die Bibliothek ist dem Standard also um eine Größenordnung voraus, aber die Verwendung des virtuellen oder des realen Dateisystems liegt in Ihrem Ermessen.
Ich empfehle:
haisum.imtqy.com/2017/09/11/golang-ioutil-readallmatthias-endler.de/2018/go-io-testingNachwort
Ich mag ehrlich gesagt wirklich 100% Deckung, aber Nicht-Bibliothekscode benötigt ihn nicht. Und selbst es garantiert keinen Schutz vor Fehlern. Konzentrieren Sie sich auf die Geschäftsanforderungen und nicht auf die Fähigkeit einer Funktion, 10 verschiedene Fehler zurückzugeben.
Für diejenigen, die gerne Code stecken und Tests ausführen, ein
Repository .