5 techniques avancées de test Go

Salut à tous! Il reste moins d'une semaine avant le début du cours «Développeur Golang» et nous continuons à partager des informations utiles sur le sujet. C'est parti!



Go a une bibliothèque intégrée bonne et fiable pour les tests. Si vous écrivez sur Go, vous le savez déjà. Dans cet article, nous parlerons de plusieurs stratégies qui peuvent améliorer vos compétences de test avec Go. De l'expérience de l'écriture de notre impressionnante base de code sur Go, nous avons appris que ces stratégies fonctionnent vraiment et permettent ainsi d'économiser du temps et des efforts pour travailler avec le code.

Utiliser des suites de tests

Si vous apprenez par vous-même une seule chose utile de cet article, ce doit être l'utilisation de suites de tests. Pour ceux qui ne connaissent pas ce concept, le test par kits est le processus de développement d'un test pour tester une interface commune qui peut être utilisée sur de nombreuses implémentations de cette interface. Ci-dessous, vous pouvez voir comment nous réussissons plusieurs implémentations Thinger différentes et les Thinger avec les mêmes tests.

 type Thinger interface { DoThing(input string) (Result, error) } // Suite tests all the functionality that Thingers should implement func Suite(t *testing.T, impl Thinger) { res, _ := impl.DoThing("thing") if res != expected { t.Fail("unexpected result") } } // TestOne tests the first implementation of Thinger func TestOne(t *testing.T) { one := one.NewOne() Suite(t, one) } // TestOne tests another implementation of Thinger func TestTwo(t *testing.T) { two := two.NewTwo() Suite(t, two) } 

Les lecteurs chanceux ont travaillé avec des bases de code qui utilisent cette méthode. Souvent utilisé dans les tests de systèmes basés sur des plugins qui sont écrits pour tester une interface, il peut être utilisé par toutes les implémentations de cette interface pour comprendre comment ils répondent aux exigences de comportement.

L'utilisation de cette approche permettra potentiellement de gagner des heures, des jours et même suffisamment de temps pour résoudre le problème de l'égalité des classes P et NP . De plus, lors du remplacement d'un système de base par un autre, la nécessité d'écrire (un grand nombre) de tests supplémentaires disparaît et il est également certain que cette approche ne perturbera pas le fonctionnement de votre application. Implicitement, vous devez créer une interface qui définit la zone de la zone testée. À l'aide de l'injection de dépendances, vous pouvez personnaliser un ensemble à partir d'un package qui est transmis à l'implémentation de l'ensemble du package.

Vous pouvez trouver un exemple complet ici . Malgré le fait que cet exemple soit tiré par les cheveux, on peut imaginer qu'une base de données est distante et que l'autre est en mémoire.

Un autre exemple intéressant de cette technique se trouve dans la bibliothèque standard du package golang.org/x/net/nettest . Il fournit un moyen de vérifier que net.Conn satisfait l'interface.

Évitez la contamination de l'interface

Vous ne pouvez pas parler de test dans Go, mais ne parlez pas d'interfaces.

Les interfaces sont importantes dans le contexte des tests, car elles sont l'outil le plus puissant de notre arsenal de tests, vous devez donc les utiliser correctement.

Les packages exportent souvent des interfaces vers les développeurs, ce qui conduit au fait que:

A) Les développeurs créent leur propre maquette pour implémenter le package;
B) Le package exporte sa propre maquette.

"Plus l'interface est grande, plus l'abstraction est faible"
- Rob Pike, Sayings of Go

Les interfaces doivent être soigneusement vérifiées avant l'exportation. Il est souvent tentant d'exporter des interfaces pour donner aux utilisateurs la possibilité de simuler le comportement dont ils ont besoin. Au lieu de cela, documentez les interfaces qui conviennent à vos structures afin de ne pas créer une relation étroite entre le package consommateur et le vôtre. Un bon exemple de cela est le package d' erreurs .

Lorsque nous avons une interface que nous ne voulons pas exporter, nous pouvons utiliser la sous-arborescence interne / package pour la sauvegarder dans le package. Ainsi, nous ne pouvons pas avoir peur que l'utilisateur final puisse dépendre de lui, et, par conséquent, peut être flexible dans le changement d'interface en fonction des nouvelles exigences. Habituellement, nous créons des interfaces avec des dépendances externes afin de pouvoir exécuter des tests localement.

Cette approche permet à l'utilisateur d'implémenter ses propres petites interfaces en enveloppant simplement une partie de la bibliothèque pour le test. Pour plus d'informations sur ce concept, lisez l' article rakyl sur la pollution des interfaces .

N'exportez pas de primitives de concurrence

Go propose des primitives de simultanéité faciles à utiliser qui peuvent également parfois conduire à leur surutilisation en raison de la même simplicité. Tout d'abord, nous sommes préoccupés par les chaînes et le package de synchronisation. Parfois, il est tentant d'exporter une chaîne depuis votre package pour que d'autres puissent l'utiliser. De plus, une erreur courante consiste à intégrer sync.Mutex sans le définir sur privé. Comme d'habitude, cela n'est pas toujours mauvais, mais cela crée certains problèmes lors du test de votre programme.

Si vous exportez des chaînes, vous compliquez en outre la vie de l'utilisateur du package, ce qui ne vaut pas la peine. Dès que le canal est exporté du package, vous créez des difficultés lors du test pour celui qui utilise ce canal. Pour réussir les tests, l'utilisateur doit savoir:

  • Lorsque les données finissent par être envoyées sur le canal.
  • Y a-t-il eu des erreurs lors de la réception des données.
  • Comment un paquet vide-t-il le canal une fois terminé, s'il est purgé?
  • Comment encapsuler une API de package pour ne pas l'appeler directement?

Jetez un œil à l'exemple de lecture de file d'attente. Voici un exemple de bibliothèque qui lit à partir de la file d'attente et fournit à l'utilisateur un flux de lecture.

 type Reader struct {...} func (r *Reader) ReadChan() <-chan Msg {...} 

L'utilisateur de votre bibliothèque souhaite maintenant implémenter un test pour son consommateur:

 func TestConsumer(t testing.T) { cons := &Consumer{ r: libqueue.NewReader(), } for msg := range cons.r.ReadChan() { // Test thing. } } 


L'utilisateur peut alors décider que l'injection de dépendances est une bonne idée et écrire ses propres messages dans le canal:

 func TestConsumer(t testing.T, q queueIface) { cons := &Consumer{ r: q, } for msg := range cons.r.ReadChan() { // Test thing. } } 


Attendez, qu'en est-il des erreurs?

 func TestConsumer(t testing.T, q queueIface) { cons := &Consumer{ r: q, } for { select { case msg := <-cons.r.ReadChan(): // Test thing. case err := <-cons.r.ErrChan(): // What caused this again? } } } 


Maintenant, nous devons en quelque sorte générer des événements afin d'écrire réellement sur ce stub, qui reproduit le comportement de la bibliothèque que nous utilisons. Si la bibliothèque vient d'écrire l'API synchrone, nous pourrions ajouter tout le parallélisme au code client, afin que les tests deviennent plus faciles.

 func TestConsumer(t testing.T, q queueIface) { cons := &Consumer{ r: q, } msg, err := cons.r.ReadMsg() // handle err, test thing } 


En cas de doute, n'oubliez pas qu'il est toujours facile d'ajouter du parallélisme au package consommateur (package consommateur) et qu'il est difficile, voire impossible de le supprimer après l'exportation depuis la bibliothèque. Et surtout, n'oubliez pas d'écrire dans la documentation du package si la structure / le package est sûr pour un accès simultané à plusieurs goroutines.
Parfois, il est toujours souhaitable ou nécessaire d'exporter la chaîne. Afin d'atténuer certains des problèmes mentionnés ci-dessus, vous pouvez fournir des canaux via des accesseurs au lieu d'un accès direct et les laisser ouverts uniquement pour la lecture ( ←chan ) ou uniquement pour l'écriture ( chan← ) lors de la déclaration.

Utilisez net/http/httptest

Httptest permet d'exécuter http.Handler code http.Handler sans démarrer de serveur ni lier à un port. Cela accélère les tests et vous permet d'exécuter des tests en parallèle à moindre coût.

Voici un exemple du même test implémenté de deux manières. Il n'y a rien de grandiose ici, mais cette approche réduit la quantité de code et économise les ressources.

 func TestServe(t *testing.T) { // The method to use if you want to practice typing s := &http.Server{ Handler: http.HandlerFunc(ServeHTTP), } // Pick port automatically for parallel tests and to avoid conflicts l, err := net.Listen("tcp", ":0") if err != nil { t.Fatal(err) } defer l.Close() go s.Serve(l) res, err := http.Get("http://" + l.Addr().String() + "/?sloths=arecool") if err != nil { log.Fatal(err) } greeting, err := ioutil.ReadAll(res.Body) res.Body.Close() if err != nil { log.Fatal(err) } fmt.Println(string(greeting)) } func TestServeMemory(t *testing.T) { // Less verbose and more flexible way req := httptest.NewRequest("GET", "http://example.com/?sloths=arecool", nil) w := httptest.NewRecorder() ServeHTTP(w, req) greeting, err := ioutil.ReadAll(w.Body) if err != nil { log.Fatal(err) } fmt.Println(string(greeting)) } 

La caractéristique la plus importante est peut-être qu'avec httptest vous ne pouvez diviser le test qu'en fonction que vous souhaitez tester. Aucun routeur, middleware ou autre effet secondaire qui se produit lors de la configuration des serveurs, des services, des usines de processeurs, des usines de processeurs ou toute autre chose que vous pensez serait une bonne idée.

Pour voir ce principe en action, consultez l'article de Marc Berger .

Utiliser un package distinct _test

La plupart des tests de l'écosystème sont créés dans les fichiers pkg_test.go , mais restent toujours dans le même package: package pkg . Un package de test distinct est le package que vous créez dans le nouveau fichier, foo_test.go , dans le répertoire du module que vous souhaitez tester, foo/ , avec le package foo_test déclaration package foo_test . De là, vous pouvez importer github.com/example/foo et d'autres dépendances. Cette fonctionnalité vous permet de faire beaucoup de choses. C'est la solution recommandée pour les dépendances cycliques dans les tests, elle empêche l'apparition de «tests fragiles» et permet au développeur de ressentir ce que c'est que d'utiliser votre propre package. Si votre paquet est difficile à utiliser, alors tester avec cette méthode sera également difficile.

Cette stratégie empêche les tests fragiles en restreignant l'accès aux variables privées. En particulier, si vos tests échouent et que vous utilisez des packages de test distincts, il est presque garanti qu'un client utilisant une fonction qui interrompt les tests s'arrêtera également lors de son appel.

Enfin, cela permet d'éviter les cycles d'importation dans les tests. La plupart des packages sont plus susceptibles de dépendre d'autres packages que vous avez écrits en plus des tests, vous vous retrouverez donc dans une situation où le cycle d'importation se produit naturellement. Un package externe est situé au-dessus des deux packages dans la hiérarchie des packages. Prenons un exemple tiré du langage de programmation Go (chapitre 11, section 2.4), où net/url implémente un analyseur d'URL que net/http importe pour utilisation. Cependant, net / url doit être testé avec un cas d'utilisation réel en important net / http . Ainsi, net/url_test .

Désormais, lorsque vous utilisez un package de test distinct, vous devrez peut-être accéder aux entités non exportées dans le package où elles étaient auparavant disponibles. Certains développeurs sont confrontés à cela pour la première fois lorsqu'ils testent quelque chose en fonction du temps (par exemple, time.Now devient un stub en utilisant une fonction). Dans ce cas, nous pouvons utiliser un fichier supplémentaire pour fournir des entités exclusivement pendant les tests, car les fichiers _test.go exclus des _test.go régulières.

De quoi devez-vous vous souvenir?

Il est important de se rappeler qu'aucune des méthodes décrites ci-dessus n'est une panacée. La meilleure approche dans toute entreprise consiste à analyser la situation de manière critique et à choisir indépendamment la meilleure solution au problème.

Vous voulez en savoir plus sur les tests avec Go?
Lisez ces articles:

Les tests pilotés par table d'écriture de Dave Cheney dans Go
Le chapitre Go Programming Language sur les tests.
Ou regardez ces vidéos:
Présentation de Hashimoto Advanced Testing With Go de Gophercon 2017
Andrew Gerrand parle des techniques de test de 2014

Nous espérons que cette traduction vous a été utile. Nous attendons les commentaires, et tous ceux qui veulent en savoir plus sur le cours, nous vous invitons à la journée portes ouvertes , qui se tiendra le 23 mai.

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


All Articles