Wir schreiben eine Lernanwendung in Go und Javascript, um die tatsächlichen Aktienrenditen zu bewerten. Teil 2 - Testen des Backends

Im ersten Teil des Artikels haben wir einen kleinen Webserver geschrieben, der das Backend für unser Informationssystem darstellt. Dieser Teil war nicht besonders interessant, obwohl er die Verwendung der Schnittstelle und eine der Methoden für die Arbeit mit Goroutinen demonstrierte. Sowohl das als auch ein anderes können für Anfänger interessant sein.

Der zweite Teil ist viel interessanter und nützlicher, da darin Unit-Tests sowohl für den Server selbst als auch für das Bibliothekspaket geschrieben werden, das das Data Warehouse implementiert. Fangen wir an.


Bild von hier

Ich möchte Sie daran erinnern, dass unsere Anwendung aus einem ausführbaren Modul (Webserver, API), einem Speichermodul (Entitätsdatenstrukturen, Vertragsschnittstelle für Speicheranbieter) und Speicheranbietermodulen besteht (in unserem Beispiel gibt es nur ein Modul, das die Speicherschnittstelle ausführt Daten im Speicher).

Wir werden das ausführbare Modul und die Speicherimplementierung testen. Das Vertragsmodul enthält keinen Code, der getestet werden könnte. Es gibt nur Typdeklarationen.
Zum Testen verwenden wir nur die Funktionen der Standardbibliothek - Test- und httptest-Pakete. Meiner Meinung nach sind sie völlig ausreichend, obwohl es viele verschiedene Test-Frameworks gibt. Schau sie dir an, vielleicht wirst du sie mögen. Aus meiner Sicht benötigen Programme auf Go nicht wirklich die Frameworks (verschiedener Art), die derzeit existieren. Dies ist kein Javascript für Sie, das im dritten Teil des Artikels behandelt wird.

Zunächst ein paar Worte zur Testmethode, die ich für Go-Programme verwende.

Erstens muss ich sagen, dass ich Go wirklich mag, nur weil es den Programmierer nicht in ein starres Framework treibt. Obwohl einige Entwickler fairerweise gerne in das Framework des vorherigen PL einsteigen. Sagen wir, der gleiche Rob Pike sagte, dass er kein Problem beim Kopieren des Codes gesehen habe, wenn das einfacher wäre. Ein solches Kopieren und Einfügen ist sogar in der Standardbibliothek enthalten. Anstatt das Paket zu importieren, kopierte einer der Autoren der Sprache einfach den Text einer Funktion (Unicode-Überprüfung). In diesem Test wird das Unicode-Paket importiert, sodass alles in Ordnung ist.

Übrigens kann in diesem Sinne (im Sinne der Sprachflexibilität) beim Schreiben von Tests eine interessante Technik verwendet werden. Das Fazit lautet: Wir wissen, dass Schnittstellenverträge in Go implizit ausgeführt werden. Das heißt, wir können einen Typ deklarieren, Methoden dafür schreiben und eine Art Vertrag ausführen. Vielleicht sogar ohne es zu wissen. Dies ist bekannt und verstanden. Dies funktioniert jedoch auch in die entgegengesetzte Richtung. Wenn der Autor eines Moduls keine Schnittstelle geschrieben hat, die uns helfen würde, einen Stub zum Testen unseres Pakets zu erstellen, können wir die Schnittstelle in unserem Test deklarieren, die in einem Paket eines Drittanbieters ausgeführt wird. Eine fruchtbare Idee, obwohl sie in unserer Trainingsanwendung nicht nützlich ist.

Zweitens ein paar Worte zum Zeitpunkt des Schreibens der Tests. Wie jeder weiß, gibt es unterschiedliche Meinungen darüber, wann Unit-Tests zu schreiben sind. Die Hauptideen sind wie folgt:

  • Wir schreiben Tests, bevor wir Code (TDD) schreiben. So verstehen wir die Aufgabe besser und legen Qualitätskriterien fest.
  • Wir schreiben Tests beim Schreiben von Code oder sogar etwas später (wir werden dieses inkrementelle Prototyping berücksichtigen).
  • Wir werden später Tests schreiben, wenn Zeit ist. Und das ist kein Scherz. Manchmal sind die Bedingungen so, dass physisch keine Zeit bleibt.

Ich glaube nicht, dass es zu diesem Thema die einzig richtige Meinung gibt. Ich werde meine teilen und die Leser bitten, in den Kommentaren zu kommentieren. Meine Meinung ist folgende:

  • Die Entwicklung freistehender Pakete auf TDD vereinfacht die Angelegenheit erheblich, insbesondere wenn das Starten der Anwendung zur Überprüfung ein ressourcenintensiver Prozess ist. Zum Beispiel habe ich kürzlich ein GPS / GLONASS-Fahrzeugüberwachungssystem entwickelt. Treiberpakete für Protokolle können nur durch Tests entwickelt werden, da das Starten und manuelle Überprüfen einer Anwendung das Warten auf Daten von Trackern erfordert, was äußerst unpraktisch ist. Für Tests nahm ich Proben von Datenpaketen, zeichnete sie in Tabellentests auf und startete den Server erst, wenn die Treiber bereit waren.
  • Wenn die Codestruktur nicht klar ist, versuche ich zuerst, einen minimal funktionierenden Prototyp zu erstellen. Dann schreibe ich Tests oder poliere zuerst den Code ein wenig und dann nur die Tests.
  • Für ausführbare Module schreibe ich zuerst einen Prototyp. Tests später. Ich teste den offensichtlichen ausführbaren Code überhaupt nicht (Sie können den Start des http-Servers von main in eine separate Funktion eingeben und im Test aufrufen, aber warum die Standardbibliothek testen?)

Auf dieser Grundlage wurde in unserer Schulungsanwendung der RAM-Speicheranbieter durch Tests geschrieben. Die ausführbare Serverdatei wurde über einen Prototyp erstellt.

Beginnen wir mit den Tests zur Implementierung des Repositorys.

Im Repository haben wir die Factory-Methode New (), die einen Zeiger auf eine Instanz des Speichertyps zurückgibt. Es gibt auch Methoden zum Abrufen von Securities () - Anführungszeichen, zum Hinzufügen von Papier zur Add () - Liste und zum Initialisieren des Speichers mit Daten vom Mosbirzh InitData () - Server.

Testen des Konstruktors (OOP-Begriffe werden frei und informell verwendet. In voller Übereinstimmung mit der Position von OOP in Go).

//    func TestNew(t *testing.T) { //   - memoryStorage := New() //     var s *Storage //         .   if reflect.TypeOf(memoryStorage) != reflect.TypeOf(s) { t.Errorf(" :  %v,   %v", reflect.TypeOf(memoryStorage), reflect.TypeOf(s)) } //     t.Logf("\n%+v\n\n", memoryStorage) } 

In diesem Test wurde ohne besonderen Bedarf gezeigt, dass der einzige Weg in Go, um den Typ einer Variablen zu überprüfen, die Reflexion ist (Reflect.TypeOf (memoryStorage)). Eine Überbeanspruchung dieses Moduls wird nicht empfohlen. Die Herausforderungen sind schwer und überhaupt nicht wert. Was ist in diesem Test außer dem Fehlen eines Fehlers noch zu überprüfen?

Als nächstes testen wir den Eingang von Angeboten und das Hinzufügen von Papier. Diese Tests duplizieren sich teilweise gegenseitig, dies ist jedoch nicht kritisch (im Test zum Hinzufügen von Papier wird die Methode zum Einholen von Angeboten zur Überprüfung aufgerufen). Im Allgemeinen schreibe ich manchmal einen Test für alle CRUD-Operationen für eine bestimmte Entität. Das heißt, im Test erstelle ich eine Entität, lese sie, ändere sie, lese sie erneut, lösche sie, lese sie erneut. Nicht sehr elegant, aber offensichtliche Mängel sind nicht sichtbar.

Angebotstest.

 //    func TestSecurities(t *testing.T) { //     var s *Storage //    ss, err := s.Securities() if err != nil { t.Error(err) } //     t.Logf("\n%+v\n\n", ss) } } 

Hier ist alles ziemlich offensichtlich.

Testen Sie nun, um Papier hinzuzufügen. In diesem Test verwenden wir für Bildungszwecke (ohne wirklichen Bedarf) eine sehr praktische Tischtesttechnik. Das Wesentliche ist wie folgt: Wir erstellen ein Array unbenannter Strukturen, von denen jede die Eingabedaten für den Test und das erwartete Ergebnis enthält. In unserem Fall legen wir ein Wertpapier vor, das hinzugefügt werden soll. Das Ergebnis ist die Anzahl der Wertpapiere im Tresor (Array-Länge). Als nächstes führen wir einen Test für jedes Element des Arrays von Strukturen durch (rufen Sie die Testmethode mit den Eingabedaten des Elements auf) und vergleichen das Ergebnis mit dem Ergebnisfeld des aktuellen Elements. Es stellt sich so etwas heraus.

 //    func TestAdd(t *testing.T) { //     var s *Storage var security = storage.Security{ ID: "MSFT", } //   var tt = []struct { s storage.Security //   length int //   () }{ { s: security, length: 1, }, { s: security, length: 2, }, } var ss []storage.Security // tc - test case, tt - table tests for _, tc := range tt { //    err := s.Add(security) if err != nil { t.Error(err) } ss, err = s.Securities() if err != nil { t.Error(err) } if len(ss) != tc.length { t.Errorf("  :  %d,   %d", len(ss), tc.length) } } //     t.Logf("\n%+v\n\n", ss) } 

Nun, ein Test für die Dateninitialisierungsfunktion.

 //    func TestInitData(t *testing.T) { //     var s *Storage //    err := s.InitData() if err != nil { t.Error(err) } ss, err := s.Securities() if err != nil { t.Error(err) } if len(ss) < 1 { t.Errorf(" :  %d,   '> 1'", len(ss)) } //     t.Logf("\n%+v\n\n", ss[0]) } 

Als Ergebnis einer erfolgreichen Testausführung erhalten wir: 17.595s Abdeckung: 86,0% der Anweisungen.

Man kann sagen, dass es schön wäre, wenn eine separate Bibliothek eine 100% ige Abdeckung erhalten würde, aber speziell hier sind erfolglose Ausführungspfade (Fehler in Funktionen) aufgrund der Implementierungsfunktionen überhaupt nicht möglich - alles ist im Speicher, wir sind nirgendwo verbunden, wir sind von nichts abhängig. Es gibt eine formelle Fehlerbehandlung, da ein Schnittstellenvertrag bewirkt, dass der Fehler zurückgegeben wird und der Linter ihn benötigt.

Fahren wir mit dem Testen des ausführbaren Pakets fort - des Webservers. Hier muss gesagt werden, dass das "net / http / httptest" -Paket speziell zum Testen von http-Anforderungshandlern entwickelt wurde, da der Webserver eine Super-Standard-Konstruktion in Go-Programmen ist. Sie können einen Webserver simulieren, einen Anforderungshandler ausführen und die Antwort des Webservers in einer speziellen Struktur aufzeichnen. Wir werden es benutzen, es ist sehr einfach, sicher wird es Ihnen gefallen.

Gleichzeitig gibt es eine Meinung (und nicht nur meine), dass ein solcher Test für ein reales Arbeitssystem möglicherweise nicht sehr relevant ist. Grundsätzlich können Sie einen echten Server starten und in Tests echte Verbindungshandler aufrufen.

Es stimmt, es gibt eine andere Meinung (und nicht nur meine), dass es gut ist, Geschäftslogik von Systemen zur Manipulation realer Daten zu isolieren.

In diesem Sinne können wir sagen, dass wir Unit-Tests schreiben, keine Integrationstests, an denen andere Pakete und Services beteiligt sind. Obwohl ich hier auch der Meinung bin, dass eine bestimmte Go-Flexibilität es Ihnen ermöglicht, sich nicht auf Begriffe zu konzentrieren und die Tests zu schreiben, die für Ihre Aufgaben am besten geeignet sind. Lassen Sie mich ein Beispiel geben: Für Tests von API-Anforderungshandlern habe ich eine vereinfachte Kopie der Datenbank auf einem realen Server im Netzwerk erstellt, mit einem kleinen Datensatz initialisiert und Tests für reale Daten ausgeführt. Dieser Ansatz ist jedoch sehr situativ.

Zurück zu den Tests unseres Webservers. Um Tests zu schreiben, die vom tatsächlichen Speicher unabhängig sind, müssen wir einen Stub-Speicher entwickeln. Dies ist überhaupt nicht schwierig, da wir mit dem Repository über die Schnittstelle arbeiten (siehe den ersten Teil). Wir müssen lediglich einen eigenen Datentyp deklarieren und die Methoden des Speicherschnittstellenvertrags dafür implementieren, auch bei leeren Daten. So etwas wie das:

 //    -    type stub int //      var securities []storage.Security //    // ******************************* //     // InitData      func (s *stub) InitData() (err error) { //   -   var security = storage.Security{ ID: "MSFT", Name: "Microsoft", IssueDate: 1514764800, // 01/01/2018 } var quote = storage.Quote{ SecurityID: "MSFT", Num: 0, TimeStamp: 1514764800, Price: 100, } security.Quotes = append(security.Quotes, quote) securities = append(securities, security) return err } // Securities      func (s *stub) Securities() (data []storage.Security, err error) { return securities, err } //   // ***************** 

Jetzt können wir unseren Speicher mit einem Stub initialisieren. Wie kann man das machen? Zum Initialisieren der Testumgebung in Go einer nicht sehr alten Version wurde eine Funktion hinzugefügt:

 func TestMain(m *testing.M) 

Mit dieser Funktion können Sie alle Tests initialisieren und ausführen. Es sieht ungefähr so ​​aus:

 //    -   func TestMain(m *testing.M) { //     -    db = new(stub) //   () db.InitData() //     os.Exit(m.Run()) } 

Jetzt können wir Tests für API-Anforderungshandler schreiben. Wir haben zwei API-Endpunkte, zwei Handler und daher zwei Tests. Sie sind sehr ähnlich, also hier ist der erste von ihnen.

 //    func TestSecuritiesHandler(t *testing.T) { //     req, err := http.NewRequest(http.MethodGet, "/api/v1/securities", nil) if err != nil { t.Fatal(err) } // ResponseRecorder    rr := httptest.NewRecorder() handler := http.HandlerFunc(securitiesHandler) //       handler.ServeHTTP(rr, req) //  HTTP-  if rr.Code != http.StatusOK { t.Errorf(" :  %v,   %v", rr.Code, http.StatusOK) } //  ()     json    var ss []storage.Security err = json.NewDecoder(rr.Body).Decode(&ss) if err != nil { t.Fatal(err) } //       t.Logf("\n%+v\n\n", ss) } 

Der Kern des Tests besteht darin, eine http-Anforderung zu erstellen, eine Struktur zum Aufzeichnen der Serverantwort zu definieren, den Anforderungshandler zu starten und den Antworttext zu dekodieren (json in die Struktur). Aus Gründen der Übersichtlichkeit drucken wir die Antwort aus.

Es stellt sich heraus wie:
=== RUN TestSecuritiesHandler
0xc00005e3e0
- PASS: TestSecuritiesHandler (0,00s)
c: \ Benutzer \ dtsp \ YandexDisk \ go \ src \ moex_etf \ server \ server_test.go: 96:
[{ID: MSFT Name: Microsoft IssueDate: 1514764800 Quotes: [{SecurityID: MSFT Num: 0 TimeStamp: 1514764800 Price: 100}]}]

Pass
ok moex_etf / server 0.307s
Erfolg: Tests bestanden.
Github- Code.

Im nächsten, letzten Teil des Artikels werden wir eine Webanwendung entwickeln, mit der Diagramme der realen Aktienrenditen von ETFs der Moskauer Börse angezeigt werden können.

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


All Articles