Nous écrivons une application d'apprentissage en Go et Javascript pour évaluer les rendements réels des actions. Partie 2 - Tester le backend

Dans la première partie de l' article, nous avons écrit un petit serveur web, qui est le backend de notre système d'information. Cette partie n'était pas particulièrement intéressante, bien qu'elle démontre l'utilisation de l'interface et l'une des méthodes de travail avec les goroutines. Cela et un autre peuvent être intéressants pour les développeurs débutants.

La deuxième partie est beaucoup plus intéressante et utile, car nous y écrirons des tests unitaires pour le serveur lui-même et le package de bibliothèque qui implémente l'entrepôt de données. Commençons.


photo d'ici

Alors, permettez-moi de vous rappeler que notre application se compose d'un module exécutable (serveur Web, API), d'un module de stockage (structures de données d'entité, interface de contrat pour les fournisseurs de stockage) et de modules de fournisseur de stockage (dans notre exemple, il n'y a qu'un seul module qui exécute l'interface de stockage données en mémoire).

Nous allons tester le module exécutable et l'implémentation du stockage. Le module de contrat ne contient pas de code pouvant être testé. Il n'y a que des déclarations de type.
Pour les tests, nous n'utiliserons que les capacités de la bibliothèque standard - tests et packages httptest. À mon avis, ils suffisent, bien qu'il existe de nombreux cadres de test différents. Regardez-les, vous les aimerez peut-être. De mon point de vue, les programmes sur Go n'ont pas vraiment besoin des cadres (de divers types) qui existent actuellement. Ce n'est pas Javascript pour vous, ce qui sera discuté dans la troisième partie de l'article.

Tout d'abord, quelques mots sur la méthodologie de test que j'utilise pour les programmes Go.

Tout d'abord , je dois dire que j'aime vraiment Go simplement parce qu'il n'entraîne pas le programmeur dans un cadre rigide. Bien que certains développeurs, en toute honnêteté, aiment se plonger dans le cadre apporté par le PL précédent. Disons, le même Rob Pike, a déclaré qu'il ne voyait pas de problème à copier le code, si c'était plus facile. Un tel copier-coller est même dans la bibliothèque standard. Au lieu d'importer le package, l'un des auteurs de la langue a simplement copié le texte d'une fonction (vérification Unicode). Dans ce test, le package Unicode est importé, donc tout est OK.

Soit dit en passant, dans ce sens (dans le sens de la flexibilité du langage), une technique intéressante peut être utilisée lors de la rédaction des tests. L'essentiel est le suivant: nous savons que les contrats d'interface dans Go sont exécutés implicitement. Autrement dit, nous pouvons déclarer un type, écrire des méthodes pour celui-ci et exécuter une sorte de contrat. Peut-être même sans le savoir. Ceci est connu et compris. Cependant, cela fonctionne également dans la direction opposée. Si l'auteur d'un module n'a pas écrit d'interface qui nous aiderait à créer un talon pour tester notre package, alors nous pouvons déclarer l'interface dans notre test, qui sera exécuté dans un package tiers. Une idée fructueuse, bien qu'elle ne soit pas utile dans notre application de formation.

Deuxièmement , quelques mots sur le moment de la rédaction des tests. Comme chacun le sait, il existe des opinions différentes sur le moment de rédiger des tests unitaires. Les idées principales sont les suivantes:

  • Nous écrivons des tests avant d'écrire du code (TDD). Ainsi, nous comprenons mieux la tâche et fixons des critères de qualité.
  • Nous écrivons des tests en écrivant du code, ou même un peu plus tard (nous considérerons ce prototypage incrémental).
  • Nous écrirons des tests un peu plus tard, si le temps le permet. Et ce n'est pas une blague. Parfois, les conditions sont telles que physiquement, il n'y a pas de temps.

Je ne pense pas qu'il y ait la seule opinion correcte à ce sujet. Je vais partager le mien et demander aux lecteurs de commenter dans les commentaires. Mon opinion est la suivante:

  • Développer des packages autonomes sur TDD, cela simplifie vraiment la question, surtout lorsque le lancement de l'application pour vérification est un processus gourmand en ressources. Par exemple, j'ai récemment développé un système de surveillance de véhicule GPS / GLONASS. Les packages de pilotes pour les protocoles ne peuvent être développés que par des tests, car le lancement et la vérification manuelle d'une application nécessitent d'attendre les données des trackers, ce qui est extrêmement gênant. Pour les tests, j'ai pris des échantillons de paquets de données, les ai enregistrés dans des tests de table et je n'ai pas démarré le serveur tant que les pilotes n'étaient pas prêts.
  • Si la structure du code n'est pas claire, j'essaie d'abord de créer un prototype fonctionnel minimal. Ensuite, j'écris des tests, ou même un peu de polissage du code, puis uniquement des tests.
  • Pour les modules exécutables, j'écris d'abord un prototype. Tests plus tard. Je ne teste pas du tout le code exécutable évident (vous pouvez taper le lancement du serveur http de main dans une fonction distincte et l'appeler dans le test, mais pourquoi tester la bibliothèque standard?)

Sur cette base, dans notre application de formation, le fournisseur de stockage RAM a été écrit à travers des tests. L'exécutable du serveur a été créé via un prototype.

Commençons par les tests d'implémentation du référentiel.

Dans le référentiel, nous avons la méthode de fabrique New (), qui renvoie un pointeur sur une instance du type de stockage. Il existe également des méthodes pour obtenir des devis Securities (), ajouter du papier à la liste Add () et initialiser le stockage avec les données du serveur Mosbirzh InitData ().

Test du constructeur (les termes OOP sont utilisés librement, de manière informelle. En parfaite conformité avec la position de OOP dans 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) } 

Dans ce test, sans besoin particulier, le seul moyen dans Go a été démontré pour vérifier le type d'une variable est la réflexion (reflect.TypeOf (memoryStorage)). La surutilisation de ce module n'est pas recommandée. Les défis sont lourds et n'en valent pas la peine. D'un autre côté, quoi d'autre à vérifier dans ce test en plus du manque d'erreur?

Ensuite, nous testons la réception des devis et l'ajout de papier. Ces tests se dupliquent partiellement, mais ce n'est pas critique (dans le test d'ajout de papier, la méthode d'obtention des devis pour vérification est appelée). En général, j'écris parfois un test pour toutes les opérations CRUD pour une entité particulière. Autrement dit, dans le test, je crée une entité, je la lis, je la change, je la relis, je la supprime, je la relis. Pas très élégant, mais les défauts évidents ne sont pas visibles.

Test de cotation.

 //    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) } } 

Tout est assez évident ici.

Testez maintenant pour ajouter du papier. Dans ce test, à des fins éducatives (sans besoin réel), nous utiliserons une technique de test de table très pratique. Son essence est la suivante: nous créons un tableau de structures sans nom, chacune contenant les données d'entrée pour le test et le résultat attendu. Dans notre cas, nous soumettons un titre à ajouter, le résultat est le nombre de titres dans le coffre-fort (longueur du tableau). Ensuite, nous effectuons un test pour chaque élément du tableau de structures (appelez la méthode de test avec les données d'entrée de l'élément) et comparons le résultat avec le champ de résultat de l'élément actuel. Il s'avère quelque chose comme ça.

 //    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) } 

Eh bien, un test pour la fonction d'initialisation des données.

 //    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]) } 

À la suite de l'exécution réussie des tests, nous obtenons: 17,595 de couverture: 86,0% des déclarations.

Vous pouvez dire qu'il serait bien qu'une bibliothèque distincte obtienne une couverture à 100%, mais spécifiquement ici, les chemins d'exécution infructueux (erreurs dans les fonctions) sont impossibles du tout, en raison des fonctionnalités d'implémentation - tout est en mémoire, nous ne sommes connectés nulle part, nous ne dépendons de rien. Il y a formellement la gestion des erreurs, car un contrat d'interface provoque le renvoi de l'erreur et le linter l'exige.

Passons à tester le package exécutable - le serveur Web. Il faut dire que le serveur web étant une construction super standard dans les programmes Go, le package «net / http / httptest» a été spécialement développé pour tester les gestionnaires de requêtes http. Il vous permet de simuler un serveur Web, d'exécuter un gestionnaire de demandes et d'enregistrer la réponse du serveur Web dans une structure spéciale. Nous allons l'utiliser, c'est très simple, c'est sûr que vous l'aimerez.

En même temps, il y a une opinion (et pas seulement la mienne) qu'un tel test peut ne pas être très pertinent pour un vrai système de travail. Vous pouvez, en principe, démarrer un vrai serveur et appeler des gestionnaires de connexion réels dans les tests.

Certes, il existe une autre opinion (et pas seulement la mienne) selon laquelle il est bon d'isoler la logique métier des systèmes de manipulation de données réelles.

En ce sens, nous pouvons dire que nous écrivons des tests unitaires, pas des tests d'intégration impliquant d'autres packages et services. Bien qu'ici, je suis également d'avis qu'une certaine flexibilité de Go vous permet de ne pas vous concentrer sur les termes et d'écrire les tests qui conviennent le mieux à vos tâches. Permettez-moi de vous donner un exemple: pour les tests des gestionnaires de demande d'API, j'ai fait une copie simplifiée de la base de données sur un vrai serveur sur le réseau, initialisée avec un petit ensemble de données et exécuté des tests sur des données réelles. Mais cette approche est très situationnelle.

Retour aux tests de notre serveur web. Afin d'écrire des tests indépendants du stockage réel, nous devons développer un stockage de stub. Ce n'est pas difficile du tout, puisque nous travaillons avec le référentiel via l'interface (voir la première partie). Tout ce que nous devons faire est de déclarer un type de données qui nous est propre et de mettre en œuvre les méthodes du contrat d'interface de stockage pour celui-ci, même avec des données vides. Quelque chose comme ça:

 //    -    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 } //   // ***************** 

Nous pouvons maintenant initialiser notre stockage avec un talon. Comment faire Dans le but d'initialiser l'environnement de test dans Go d'une version pas très ancienne, une fonction a été ajoutée:

 func TestMain(m *testing.M) 

Cette fonction vous permet d'initialiser et d'exécuter tous les tests. Cela ressemble à ceci:

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

Nous pouvons maintenant écrire des tests pour les gestionnaires de requêtes API. Nous avons deux points de terminaison API, deux gestionnaires et donc deux tests. Ils sont très similaires, voici donc le premier d'entre eux.

 //    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) } 

L'essence du test est la suivante: créer une requête http, définir une structure pour enregistrer la réponse du serveur, démarrer le gestionnaire de requêtes, décoder le corps de la réponse (json dans la structure). Eh bien, pour plus de clarté, nous imprimons la réponse.

Il s'avère que quelque chose comme:
=== RUN TestSecuritiesHandler
0xc00005e3e0
- PASS: TestSecuritiesHandler (0.00s)
c: \ Users \ dtsp \ YandexDisk \ go \ src \ moex_etf \ server \ server_test.go: 96:
[{ID: Nom MSFT: Microsoft IssueDate: 1514764800 Citations: [{SecurityID: MSFT Num: 0 TimeStamp: 1514764800 Price: 100}]}]

Passe
ok moex_etf / server 0.307s
Succès: tests réussis.
Code Github .

Dans la dernière partie de l'article, nous développerons une application Web pour afficher les graphiques des rendements réels des actions des ETF de la Bourse de Moscou.

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


All Articles