Golang teste en dehors de gotour



Personne n'aime écrire des tests. Bien sûr, je plaisante, tout le monde aime les écrire! Comme le diront les chefs d'équipe et les RH, la bonne réponse lors des entretiens est que j'aime vraiment et que j'écris des tests. Mais soudain, vous aimez écrire des tests dans une autre langue. Comment commencez-vous à écrire du code go couvert par le test?

Partie 1. Test du gestionnaire


Au sortir de la boîte, il y a un support pour le serveur http dans "net / http", donc vous pouvez le soulever sans aucun effort. Les opportunités qui se sont ouvertes nous permettent de nous sentir extrêmement puissants, et donc notre code rendra le 42ème utilisateur.

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 } 

Ce code reçoit le paramètre id utilisateur en entrée, puis émule la présence de l'utilisateur dans la base de données et revient. Maintenant, nous devons le tester ...

Il y a une chose merveilleuse «net / http / httptest», elle vous permet de simuler un appel à notre handler'a puis de comparer la réponse.

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

Partie 2. Chérie, nous avons une API externe ici


Et pourquoi avons-nous besoin de reprendre notre souffle, si nous venons de nous échauffer? Dans nos services, tôt ou tard, des API externes apparaîtront. C'est une étrange bête qui se cache souvent et qui peut se comporter à sa guise. Pour les tests, nous aimerions un collègue plus accommodant. Et notre httptest récemment découvert nous aidera également ici. Par exemple, le code d'appel est une API externe avec transfert de données supplémentaire.

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

Pour vaincre cela, nous pouvons créer une maquette d'API externe, l'option la plus simple est:

  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 contiendra une chaîne au format `http: //127.0.0.1: 49799`, qui sera la maquette api qui appelle notre implémentation

Partie 3. Travaillons avec la base


Il existe un moyen simple: relever le docker avec la base, migrer les rouleaux, les appareils et exécuter notre excellent service. Mais essayons d'écrire des tests avec un minimum de dépendances avec des services externes.

L'implémentation de travailler avec la base dans go vous permet de remplacer le pilote lui-même, et, en contournant 100 pages de code et de réflexion, je vous suggère de prendre la bibliothèque github.com/DATA-DOG/go-sqlmock
Vous pouvez gérer sql.Db sur le dock. Prenons un exemple un peu plus intéressant, dans lequel il y aura une forme orm.

 func DbListener(db *gorm.DB) { user := User{} transaction := db.Begin() transaction.First(&user, 1) transaction.Model(&user).Update("counter", user.Counter+1) transaction.Commit() } 

J'espère que cet exemple vous a au moins fait réfléchir sur la façon de le tester. Dans "mock.ExpectExec", vous pouvez remplacer une expression régulière couvrant le cas dont vous avez besoin. La seule chose à retenir est que l'ordre dans lequel l'attente est définie doit correspondre à l'ordre et au nombre d'appels.

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

J'ai trouvé de nombreux exemples pour tester la base ici .

Partie 4. Travailler avec le système de fichiers


Nous nous sommes essayés dans différents domaines et nous sommes réconciliés que tout est bon pour se mouiller. Tout n'est pas si clair ici. Je suggère deux approches, mouillez-vous ou utilisez le système de fichiers.

Option 1 - nous sommes tous mouillés sur github.com/spf13/afero

Avantages :
  • Vous n'avez rien à refaire si vous utilisez déjà cette bibliothèque. (mais alors vous vous ennuyez de le lire)
  • Travailler avec un système de fichiers virtuel, ce qui accélérera considérablement vos tests.


Inconvénients :
  • La modification du code existant est requise.
  • Le chmod ne fonctionne pas sur le système de fichiers virtuel. Mais ça peut être des fonctionnalités puisque la documentation indique «Évitez les problèmes de sécurité et les autorisations».

De ces quelques points, j'ai immédiatement fait 2 tests. Dans la version avec le système de fichiers, j'ai créé un fichier illisible et vérifié le fonctionnement du système.

 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 } 

L'utilisation de afero.Fs nécessite un minimum de modifications, mais fondamentalement rien ne change dans le 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 } 

Mais notre plaisir ne sera complet que si nous découvrons combien plus rapide est afero que natif.
Minute de référence:

 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 

Ainsi, la bibliothèque est un ordre de grandeur en avance sur la norme, mais l'utilisation du système de fichiers virtuel ou du vrai est à votre discrétion.

Je recommande:

haisum.imtqy.com/2017/09/11/golang-ioutil-readall
matthias-endler.de/2018/go-io-testing

Postface


Honnêtement, j'aime vraiment la couverture à 100%, mais le code non-bibliothèque n'en a pas besoin. Et même cela ne garantit pas la protection contre les erreurs. Concentrez-vous sur les exigences métier et non sur la capacité d'une fonction à renvoyer 10 erreurs différentes.

Pour ceux qui aiment piquer du code et exécuter des tests, un référentiel .

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


All Articles