Tests auf Code und Code für Tests

In dynamischen Sprachen wie Python und Javascript ist es möglich, Methoden und Klassen in Modulen direkt während des Betriebs zu ersetzen. Dies ist sehr praktisch für Tests - Sie können einfach "Patches" einfügen, die schwere oder unnötige Logik im Kontext dieses Tests ausschließen.


Aber was tun in C ++? Geh? Java? In diesen Sprachen kann der Code nicht für Tests im laufenden Betrieb geändert werden. Für das Erstellen von Patches sind separate Tools erforderlich.


In solchen Fällen sollten Sie den Code speziell schreiben, damit er getestet wird. Dies ist nicht nur ein manischer Wunsch nach einer 100% igen Abdeckung in Ihrem Projekt. Dies ist ein Schritt zum Schreiben von unterstütztem und qualitativ hochwertigem Code.


In diesem Artikel werde ich versuchen, über die Hauptideen beim Schreiben von testbarem Code zu sprechen und zu zeigen, wie sie mit einem Beispiel für ein einfaches Go-Programm verwendet werden können.


Unkompliziertes Programm


Wir schreiben ein einfaches Programm, um eine Anfrage an die VK-API zu stellen. Dies ist ein ziemlich einfaches Programm, das eine Anfrage generiert, erstellt, die Antwort liest, die Antwort von JSON in eine Struktur dekodiert und dem Benutzer das Ergebnis anzeigt.


package main import ( "encoding/json" "fmt" "io/ioutil" "net/http" "net/url" ) const token = "token here" func main() { //     var requestURL = fmt.Sprintf( "https://api.vk.com/method/%s?&access_token=%s&v=5.95", "users.get", token, ) //   resp, err := http.PostForm(requestURL, nil) //   if err != nil { fmt.Println(err) return } //       defer resp.Body.Close() //     body, err := ioutil.ReadAll(resp.Body) //   if err != nil { fmt.Println(err) return } //      var result struct { Response []struct { ID int `json:"id"` FirstName string `json:"first_name"` LastName string `json:"last_name"` } `json:"response"` } //        err = json.Unmarshal(body, &result) //   if err != nil { fmt.Println(err) return } // ,    if len(result.Response) < 1 { fmt.Println("No values in response array") return } //    fmt.Printf( "Your id: %d\nYour full name: %s %s\n", result.Response[0].ID, result.Response[0].FirstName, result.Response[0].LastName, ) } 

Als Fachleute auf unserem Gebiet haben wir beschlossen, dass es notwendig ist, Tests für unsere Anwendung zu schreiben. Erstellen Sie eine Testdatei ...


 package main import ( "testing" ) func Test_Main(t *testing.T) { main() } 

Es sieht nicht sehr attraktiv aus. Diese Überprüfung ist ein einfacher Start einer Anwendung, die wir nicht beeinflussen können. Wir können die Arbeit mit dem Netzwerk nicht ausschließen, die Funktionsfähigkeit auf verschiedene Fehler überprüfen und sogar das Token ersetzen, damit die Überprüfung fehlschlägt. Versuchen wir herauszufinden, wie dieses Programm verbessert werden kann.


Abhängigkeitsinjektionsmuster


Zuerst müssen Sie das Muster "Abhängigkeitsinjektion" implementieren.


 type VKClient struct { Token string } func (client VKClient) ShowUserInfo() { var requestURL = fmt.Sprintf( "https://api.vk.com/method/%s?&access_token=%s&v=5.95", "users.get", client.Token, ) // ... } 

Durch Hinzufügen einer Struktur haben wir eine Abhängigkeit (Zugriffsschlüssel) für die Anwendung erstellt, die aus verschiedenen Quellen übertragen werden kann, wodurch die "verdrahteten" Werte vermieden und das Testen vereinfacht werden.


 package example import ( "testing" ) const workingToken = "workingToken" func Test_ShowUserInfo_Successful(t *testing.T) { client := VKClient{workingToken} client.ShowUserInfo() } func Test_ShowUserInfo_EmptyToken(t *testing.T) { client := VKClient{""} client.ShowUserInfo() } 

Trennung von Empfangsinformationen und deren Ausgabe


Jetzt kann nur eine Person einen Fehler machen, und dann nur, wenn sie weiß, was die Schlussfolgerung sein sollte. Um dieses Problem zu lösen, müssen Informationen nicht direkt in den Ausgabestream ausgegeben werden, sondern separate Methoden zum Abrufen von Informationen und deren Ausgabe hinzugefügt werden. Diese beiden unabhängigen Teile sind einfacher zu überprüfen und zu warten.


Erstellen wir die GetUserInfo() -Methode, die eine Struktur mit Benutzerinformationen und einem Fehler GetUserInfo() (falls dies passiert ist). Da diese Methode nichts ausgibt, werden die auftretenden Fehler ohne Ausgabe weiter übertragen, so dass der Code, der die Daten benötigt, die Situation herausfindet.


 type UserInfo struct { ID int `json:"id"` FirstName string `json:"first_name"` LastName string `json:"last_name"` } func (client VKClient) GetUserInfo() (UserInfo, error) { var requestURL = fmt.Sprintf( "https://api.vk.com/method/%s?&access_token=%s&v=5.95", "users.get", client.Token, ) resp, err := http.PostForm(requestURL, nil) if err != nil { return UserInfo{}, err } // ... var result struct { Response []UserInfo `json:"response"` } // ... return result.Response[0], nil } 

Ändern Sie ShowUserInfo() so, dass es GetUserInfo() und Fehler behandelt.


 func (client VKClient) ShowUserInfo() { userInfo, err := client.GetUserInfo() if err != nil { fmt.Println(err) return } fmt.Printf( "Your id: %d\nYour full name: %s %s\n", userInfo.ID, userInfo.FirstName, userInfo.LastName, ) } 

In Tests können Sie jetzt überprüfen, ob die richtige Antwort vom Server empfangen wurde. Wenn das Token falsch ist, wird ein Fehler zurückgegeben.


 func Test_GetUserInfo_Successful(t *testing.T) { client := VKClient{workingToken} userInfo, err := client.GetUserInfo() if err != nil { t.Fatal(err) } if userInfo.ID == 0 { t.Fatal("ID is empty") } if userInfo.FirstName == "" { t.Fatal("FirstName is empty") } if userInfo.LastName == "" { t.Fatal("LastName is empty") } } func Test_ShowUserInfo_EmptyToken(t *testing.T) { client := VKClient{""} _, err := client.GetUserInfo() if err == nil { t.Fatal("Expected error but found <nil>") } if err.Error() != "No values in response array" { t.Fatalf(`Expected "No values in response array", but found "%s"`, err) } } 

Zusätzlich zum Aktualisieren vorhandener Tests müssen Sie neue Tests für die ShowUserInfo() -Methode hinzufügen.


 func Test_ShowUserInfo(t *testing.T) { client := VKClient{workingToken} client.ShowUserInfo() } func Test_ShowUserInfo_WithError(t *testing.T) { client := VKClient{""} client.ShowUserInfo() } 

Benutzerdefinierte Alternativen


Die Tests für ShowUserInfo() ähneln denen, von denen wir ursprünglich versucht haben, wegzukommen. In diesem Fall besteht der einzige Punkt der Methode darin, Informationen an den Standardausgabestream auszugeben. Einerseits können Sie versuchen, os.Stdout neu zu definieren und die Ausgabe zu überprüfen. Es scheint eine zu redundante Lösung zu sein, wenn Sie eleganter handeln können.


Anstelle von fmt.Printf können Sie auch fmt.Fprintf , mit dem Sie auf jedem io.Writer ausgeben können. os.Stdout implementiert diese Schnittstelle, mit der wir fmt.Printf(text) durch fmt.Fprintf(os.Stdout, text) ersetzen fmt.Printf(text) . Danach können wir os.Stdout in ein separates Feld os.Stdout , das auf die gewünschten Werte gesetzt werden kann (für Tests - eine Zeichenfolge, für die Arbeit - einen Standardausgabestream).


Da die Möglichkeit, Writer für die Ausgabe zu ändern, hauptsächlich für Tests nur selten verwendet wird, ist es sinnvoll, einen Standardwert festzulegen. In go werden wir dies tun - den VKClient Typ VKClient exportierbar machen und eine Konstruktorfunktion dafür erstellen.


 type vkClient struct { Token string OutputWriter io.Writer } func CreateVKClient(token string) vkClient { return vkClient{ token, os.Stdout, } } 

In der ShowUserInfo() -Funktion ersetzen wir die Print Aufrufe durch Fprintf .


 func (client vkClient) ShowUserInfo() { userInfo, err := client.GetUserInfo() if err != nil { fmt.Fprintf(client.OutputWriter, err.Error()) return } fmt.Fprintf( client.OutputWriter, "Your id: %d\nYour full name: %s %s\n", userInfo.ID, userInfo.FirstName, userInfo.LastName, ) } 

Jetzt müssen Sie die Tests aktualisieren, damit sie den Client mithilfe des Konstruktors erstellen und bei Bedarf einen anderen Writer installieren.


 func Test_ShowUserInfo(t *testing.T) { client := CreateVKClient(workingToken) buffer := bytes.NewBufferString("") client.OutputWriter = buffer client.ShowUserInfo() result, _ := ioutil.ReadAll(buffer) matched, err := regexp.Match( `Your id: \d+\nYour full name: [^\n]+\n`, result, ) if err != nil { t.Fatal(err) } if !matched { t.Fatalf(`Expected match but failed with "%s"`, result) } } func Test_ShowUserInfo_WithError(t *testing.T) { client := CreateVKClient("") buffer := bytes.NewBufferString("") client.OutputWriter = buffer client.ShowUserInfo() result, _ := ioutil.ReadAll(buffer) if string(result) != "No values in response array" { t.Fatal("Wrong error") } } 

Für jeden Test, bei dem wir etwas ausgeben, erstellen wir einen Puffer, der die Rolle eines Standardausgabestreams spielt. Nach Ausführung der Funktion wird überprüft, ob die Ergebnisse unseren Erwartungen entsprechen - mit Hilfe von regulären Ausdrücken oder einem einfachen Vergleich.


Warum verwende ich reguläre Ausdrücke? Damit die Tests mit jedem gültigen Token funktionieren, das ich dem Programm zur Verfügung stelle, unabhängig vom Benutzernamen und der Benutzer-ID.


Abhängigkeitsinjektionsmuster - 2


Derzeit hat das Programm eine Abdeckung von 86,4%. Warum nicht 100%? Wir können keine Fehler von http.PostForm() , ioutil.ReadAll() und json.Unmarshal() provozieren, was bedeutet, dass wir nicht jedes " return UserInfo, err " überprüfen können.


Um sich noch mehr Kontrolle über die Situation zu verschaffen, müssen Sie eine Schnittstelle erstellen, unter die http.Client passt, deren Implementierung in vkClient erfolgt und für den Netzwerkbetrieb verwendet wird. Für uns ist in der Benutzeroberfläche nur eine Methode PostForm - PostForm .


 type Networker interface { PostForm(string, url.Values) (*http.Response, error) } type vkClient struct { Token string OutputWriter io.Writer Networker Networker } func CreateVKClient(token string) vkClient { return vkClient{ token, os.Stdout, &http.Client{}, } } 

Durch einen solchen Schritt entfällt die Notwendigkeit, Netzwerkoperationen im Allgemeinen durchzuführen. Jetzt können wir einfach die erwarteten Daten von VKontakte mit dem gefälschten Networker . Entfernen Sie natürlich keine Tests, die Anforderungen an den Server prüfen, aber es ist nicht erforderlich, bei jedem Test Anforderungen zu stellen.


Wir werden Implementierungen für den gefälschten Networker und Reader erstellen, damit wir die Fehler jeweils testen können - auf Anfrage, beim Lesen des Körpers und während der Deserialisierung. Wenn beim Aufrufen von PostForm ein Fehler auftreten soll, geben wir ihn einfach in dieser Methode zurück. Wenn wir einen Fehler wollen
Beim Lesen des Antwortkörpers muss ein gefälschter Reader , der einen Fehler auslöst. Und wenn wir den Fehler brauchen, um sich während der Deserialisierung zu manifestieren, geben wir die Antwort mit einer leeren Zeichenfolge im Körper zurück. Wenn wir keine Fehler wollen, geben wir einfach den Body mit dem angegebenen Inhalt zurück.


 type fakeReader struct{} func (fakeReader) Read(p []byte) (n int, err error) { return 0, errors.New("Error on read") } type fakeNetworker struct { ErrorOnPostForm bool ErrorOnBodyRead bool ErrorOnUnmarchal bool RawBody string } func (fn *fakeNetworker) PostForm(string, url.Values) (*http.Response, error) { if fn.ErrorOnPostForm { return nil, fmt.Errorf("Error on PostForm") } if fn.ErrorOnBodyRead { return &http.Response{Body: ioutil.NopCloser(fakeReader{})}, nil } if fn.ErrorOnUnmarchal { fakeBody := ioutil.NopCloser(bytes.NewBufferString("")) return &http.Response{Body: fakeBody}, nil } fakeBody := ioutil.NopCloser(bytes.NewBufferString(fn.RawBody)) return &http.Response{Body: fakeBody}, nil } 

Für jede Problemsituation fügen wir einen Test hinzu. Sie erstellen einen gefälschten Networker mit den erforderlichen Einstellungen, nach denen er an einem bestimmten Punkt einen Fehler auslöst. Danach rufen wir die zu überprüfende Funktion auf und stellen sicher, dass ein Fehler aufgetreten ist und dass wir diesen Fehler erwartet haben.


 func Test_GetUserInfo_ErrorOnPostForm(t *testing.T) { client := CreateVKClient(workingToken) client.Networker = &fakeNetworker{ErrorOnPostForm: true} _, err := client.GetUserInfo() if err == nil { t.Fatal("Expected error but none found") } if err.Error() != "Error on PostForm" { t.Fatalf(`Expected "Error on PostForm" but got "%s"`, err.Error()) } } func Test_GetUserInfo_ErrorOnBodyRead(t *testing.T) { client := CreateVKClient(workingToken) client.Networker = &fakeNetworker{ErrorOnBodyRead: true} _, err := client.GetUserInfo() if err == nil { t.Fatal("Expected error but none found") } if err.Error() != "Error on read" { t.Fatalf(`Expected "Error on read" but got "%s"`, err.Error()) } } func Test_GetUserInfo_ErrorOnUnmarchal(t *testing.T) { client := CreateVKClient(workingToken) client.Networker = &fakeNetworker{ErrorOnUnmarchal: true} _, err := client.GetUserInfo() if err == nil { t.Fatal("Expected error but none found") } const expectedError = "unexpected end of JSON input" if err.Error() != expectedError { t.Fatalf(`Expected "%s" but got "%s"`, expectedError, err.Error()) } } 

Über das Feld RawBody können RawBody Netzwerkanforderungen RawBody (geben Sie einfach das zurück, was wir von VKontakte erwarten). Dies kann erforderlich sein, um zu vermeiden, dass die Abfragegrenzen während des Tests überschritten werden, oder um Tests zu beschleunigen.


Zusammenfassung


Nach allen Vorgängen im Projekt haben wir ein Paket mit einer Länge von 91 Zeilen (+170 Testzeilen) erhalten, das die Ausgabe an einen beliebigen io.Writer unterstützt. Mit diesem io.Writer können Sie alternative Methoden für die Arbeit mit dem Netzwerk verwenden (mithilfe des Adapters für unsere Schnittstelle), in denen es eine Methode wie diese gibt Daten auszugeben und zu erhalten. Das Projekt ist zu 100% abgedeckt. Tests überprüfen jede Zeile und Anwendungsantwort vollständig auf jeden möglichen Fehler.


Jeder Schritt auf dem Weg zu einer 100% igen Abdeckung erhöhte die Modularität, Wartbarkeit und Zuverlässigkeit der Anwendung, sodass nichts falsch daran ist, dass die Tests die Struktur des Pakets vorschrieben.


Die Testbarkeit eines Codes ist eine Qualität, die nicht aus der Luft erscheint. Die Testbarkeit wird angezeigt, wenn der Entwickler Muster in geeigneten Situationen angemessen verwendet und benutzerdefinierten und modularen Code schreibt. Die Hauptaufgabe bestand darin, den Denkprozess bei der Durchführung von Refactoring-Programmen aufzuzeigen. Ähnliches Denken kann sich auf jede Anwendung und Bibliothek sowie auf andere Sprachen erstrecken.

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


All Articles