Tests en go (golang)

a marqué ce sujet comme résolu.

Bonjour,

ça fait un petit moment que je fais du Go, et plus récemment une nouvelle application en Go et je n’ai pas encore fait de tests unitaires. (Oups :honte: )

J’ai une fonction qui me retourne un utilisateur (struct) dans un autre UserApi (struct pour le rendu en API).

func (u User) ToApi() UserApi {
    uApi := UserApi{
        ID:         u.Identifier.ID,
        Username:   u.Username,
        Email:      u.Email,
        Firstname:  u.Firstname,
        Lastname:   u.Lastname,
    }

    return uApi
}
models/user.go

Et le test unitaire associé:

func TestUser_ToApi(t *testing.T) {
    user := User{
        Identifier: Identifier{ID:1},
        Username:   "UName",
        Email:      "UEmail@wxtodo.local",
        Password:   "UPass",
        Firstname:  "UFirst",
        Lastname:   "ULast",
        Groups:     nil,
        Projects:   nil,
    }

    userApi := UserApi{
        ID:        user.ID,
        Username:  user.Username,
        Email:     user.Email,
        Firstname: user.Firstname,
        Lastname:  user.Lastname,
    }

    if diff := deep.Equal(user.ToApi(), userApi); diff != nil {
        t.Error(diff)
    }
}
models/user_test.go

Dans un premier temps, j’aurais aimé savoir si pour l’exemple donnée je suis dans le juste.

Ça, c’est pour le tests des méthodes ne faisant pas appel à ma base de données. Je suis totalement perdu pour savoir comment faire des tests sur mes méthodes traitant avec la base de données, ainsi que les méthodes qui renvoie les résultats sur mon API.

Voici un exemple de deux méthodes/fonctions pour lesquels je ne n’arrive pas écrire de tests. (Pour l’interaction avec la base de donnée, j’utilise gorm).

//Load user groups from database into &u
func (u *User) GetGroups() error {
    return dbengine.
        Preload("Groups", func(db *gorm.DB) *gorm.DB {
            return db.Order("groups.id ASC").Preload("Tasks")
        }).
        Find(u).
        Error
}
models/user.go
func ListAuthUserGroups(ctx *context.APIContext) {
    // ** Documentation swagger enlevé pour l'exemple

    err := ctx.User.GetGroups()
    if err != nil {
        //TODO: Log
        ctx.Status(http.StatusInternalServerError)
    }else {
        ctx.JSON(http.StatusOK, models.ToGroupsApi(ctx.User.Groups, false, false, true))
    }
}
api/users/user.go

Hier après-midi, j’ai regardé pas mal d’articles sur internet parlant de tests unitaires en liens avec la DB, mais c’est toujours assez flou. Ça parle de mock, etc. (Et dans bien des cas, pour une fonction de 10 lignes je me retrouverais avec un tests de 60 lignes, … :o )

Salut.

Alors, tout d’abord, il y a une petite chose qui m’interpelle, c’est que ta méthode GetGroups() semble faire appel à une variable var dbengine *gorm.DB globale plutôt que de l’accepter en argument.

Bon, en soi c’est pas la mort, mais en ce qui me concerne je préfère que les méthodes acceptent ce *gorm.DB en premier argument, notamment parce que cela permet de les faire fonctionner avec ou sans transaction. Mais soit. C’est un micro-truc, et si ça se trouve ça ne posera jamais de problème dans ton application.

Pour ce qui est des tests avec gORM, personnellement, j’ai eu à écrire ce genre de truc, qui fonctionne avec testify/suite :

package testutil

import (
    "github.com/jinzhu/gorm"
    "github.com/stretchr/testify/suite"
    "gitlab.com/[... mon repo du boulot...]/config"
    "gitlab.com/[... mon repo du boulot...]/migrate"
    "go.uber.org/zap"
)

// DBTestSuite helps creating safe test suites using the database.
// Specifically it helps ensuring that:
//
// * The database is migrated to the latest version when the suite starts,
// * Any data inserted into the database is cleaned up after the test is finished.
type DBTestSuite struct {
    suite.Suite
    db          *gorm.DB
    dirtyTables map[string]bool
    Logger      *zap.Logger
}

// DB returns a handle on the database.
func (s *DBTestSuite) DB() *gorm.DB {
    return s.db
}

// SetVerbose toggles database logs on/off
func (s *DBTestSuite) SetVerbose(verbose bool) {
    s.db.LogMode(verbose)
}

// SetupSuite opens the database and migrates it to the latest version during
// suite initialization
func (s *DBTestSuite) SetupSuite() {
        // !!! Configuration de la base de données
        // Le but est ici uniquement d'initialiser s.db
    config.Init(config.FromEnv)
    s.Logger = zap.L()
    require := s.Require()

    conf, err := config.GetDBConfig()
    require.NoError(err)

        // Ici j'effectue systématiquement une migration de la BDD vers la dernière version 
        // du schéma
    m, err := migrate.New(conf)
    require.NoError(err)

    require.NoError(m.Up())
    s.db, err = gorm.Open("postgres", conf.GormConfig())
    require.NoError(err)

        // Ici, c'est vachement plus intéressant
        // Je crée un hook qui va écouter chaque opération d'écriture (create) et enregistrer le nom de la table
        // À la fin de chaque test, je vais vider la table correspondante.
    s.dirtyTables = make(map[string]bool)
    s.db.LogMode(false)
    s.db.Callback().Create().After("gorm:create").Register("dirtyTablesHook", func(scope *gorm.Scope) {
        s.dirtyTables[scope.TableName()] = true
    })
}

// TearDownTest cleans up any table made dirty during the test
func (s *DBTestSuite) TearDownTest() {
    s.db.Transaction(func(tx *gorm.DB) error {
        for table := range s.dirtyTables {
            s.Logger.Debug("deleting entries", zap.String("table", table))
            if err := tx.Exec("DELETE FROM " + table + ";").Error; err != nil {
                s.Logger.Error("couldn't delete entries", zap.Error(err))
                return err
            }
            delete(s.dirtyTables, table)
        }
        return nil
    })
}

// TearDownSuite closes the database during suite finalization
func (s *DBTestSuite) TearDownSuite() {
    s.db.Close()
}

Je ne sais pas si tu as déjà utilisé ce genre d’abstraction TestSuite, mais ça rend les tests assez commodes à écrire.

Ce que fait ce code, concrètement, c’est définir un genre de suite qui permet :

  • de se connecter à ma base de données lors de l’initialisation de ma suite de tests,
  • d’appliquer automatiquement mes migrations pour que ma base soit bien configurée dans la toute dernière version du schéma,
  • de nettoyer les tables que j’ai salies dans mes tests, pour m’assurer mes tests soient bien indépendants les uns des autres.

Ça présuppose que mes tests auto sont exécutés sur une base "vide", dédiée.

N’hésite pas si tu as des questions ou si quelque chose n’est pas clair.

+0 -0

Alors, tout d’abord, il y a une petite chose qui m’interpelle, c’est que ta méthode GetGroups() semble faire appel à une variable var dbengine *gorm.DB globale plutôt que de l’accepter en argument.

Bon, en soi c’est pas la mort, mais en ce qui me concerne je préfère que les méthodes acceptent ce *gorm.DB en premier argument, notamment parce que cela permet de les faire fonctionner avec ou sans transaction. Mais soit. C’est un micro-truc, et si ça se trouve ça ne posera jamais de problème dans ton application.

Effectivement, il faudrait que je vois pour l’ajouter dans mon context pour y avoir accès depuis mes fonctions API, mais il me semble que ça me force un peu à me trimballer une variable partout, raison pour laquelle j’en avais fais une variable globale.

Je vais analyser et essayer d’implémenter ton code quand j’aurai un peu de temps.

d’appliquer automatiquement mes migrations pour que ma base soit bien configurée dans la toute dernière version du schéma,

Pour ce point justement, qu’utilises-tu pour les migrations ?

Car actuellement, j’ai un truc assez sale du genre (mais qui fonctionne pour du développement):

func DbMigrate(){
    dbengine.AutoMigrate(&User{})
    dbengine.AutoMigrate(&Project{}).
        AddForeignKey("user_id", "projects(id)", "RESTRICT", "RESTRICT")
    dbengine.AutoMigrate(&Group{}).
        AddForeignKey("user_id", "users(id)", "RESTRICT", "RESTRICT").
        AddForeignKey("project_id",  "projects(id)", "RESTRICT", "RESTRICT")
    dbengine.AutoMigrate(&Task{}).
        AddForeignKey("group_id", "groups(id)", "RESTRICT", "RESTRICT")
    dbengine.AutoMigrate(&File{}).
        AddForeignKey("task_id", "tasks(id)", "RESTRICT", "RESTRICT")
    dbengine.AutoMigrate(&Color{})
    dbengine.AutoMigrate(&Tag{}).
        AddForeignKey("color_id", "colors(id)", "RESTRICT", "RESTRICT")
}


Pour me familiariser avec ça, j’ai un peu pris référence sur le code de gitea. Pour leur migrations, ils ont, comme je peux le voir, développer leur propre système (https://github.com/go-gitea/gitea/tree/master/models/migrations).

Sais-tu s’il y a un outil qui fait ça pour nous ? (un peu comme laravel qui permet de créer modèle, migrations, etc. et de les appliquer ensuite)

Dans tous les cas, merci pour tes explications !

Alors, tout d’abord, il y a une petite chose qui m’interpelle, c’est que ta méthode GetGroups() semble faire appel à une variable var dbengine *gorm.DB globale plutôt que de l’accepter en argument.

Bon, en soi c’est pas la mort, mais en ce qui me concerne je préfère que les méthodes acceptent ce *gorm.DB en premier argument, notamment parce que cela permet de les faire fonctionner avec ou sans transaction. Mais soit. C’est un micro-truc, et si ça se trouve ça ne posera jamais de problème dans ton application.

Effectivement, il faudrait que je vois pour l’ajouter dans mon context pour y avoir accès depuis mes fonctions API, mais il me semble que ça me force un peu à me trimballer une variable partout, raison pour laquelle j’en avais fais une variable globale.

Attention ça a beaucoup de sens de faire ça au départ hein. Par contre effectivement, si tu veux rendre ton code plus facile à tester, écrire un middleware qui crée une transaction et passe ton *gorm.DB dans le contexte des requêtes est effectivement la façon canonique de s’y prendre.

Le soucis d’une variable globale risque de se poser pour les tests aussi. Ici ma suite est responsable d’initialiser sa DB, et quand je teste un service, je récupère cet objet et je le passe à l’initialisation de mon service.

Je vais analyser et essayer d’implémenter ton code quand j’aurai un peu de temps.

d’appliquer automatiquement mes migrations pour que ma base soit bien configurée dans la toute dernière version du schéma,

Pour ce point justement, qu’utilises-tu pour les migrations ?

Car actuellement, j’ai un truc assez sale du genre (mais qui fonctionne pour du développement):

func DbMigrate(){
    dbengine.AutoMigrate(&User{})
    dbengine.AutoMigrate(&Project{}).
        AddForeignKey("user_id", "projects(id)", "RESTRICT", "RESTRICT")
    dbengine.AutoMigrate(&Group{}).
        AddForeignKey("user_id", "users(id)", "RESTRICT", "RESTRICT").
        AddForeignKey("project_id",  "projects(id)", "RESTRICT", "RESTRICT")
    dbengine.AutoMigrate(&Task{}).
        AddForeignKey("group_id", "groups(id)", "RESTRICT", "RESTRICT")
    dbengine.AutoMigrate(&File{}).
        AddForeignKey("task_id", "tasks(id)", "RESTRICT", "RESTRICT")
    dbengine.AutoMigrate(&Color{})
    dbengine.AutoMigrate(&Tag{}).
        AddForeignKey("color_id", "colors(id)", "RESTRICT", "RESTRICT")
}


Pour me familiariser avec ça, j’ai un peu pris référence sur le code de gitea. Pour leur migrations, ils ont, comme je peux le voir, développer leur propre système (https://github.com/go-gitea/gitea/tree/master/models/migrations).

Sais-tu s’il y a un outil qui fait ça pour nous ? (un peu comme laravel qui permet de créer modèle, migrations, etc. et de les appliquer ensuite)

Dans tous les cas, merci pour tes explications !

WinXaito

Eh bien tant que tu n’as pas besoin de plus, je dirais que d’utiliser l’automigrate de gORM a du sens et je garderais ça.

Pour ma part j’utilise migrate, qui me pousse à écrire mes migrations up et down dans des fichiers en SQL directement.

Je sais que buffalo utilise encore un autre système appelé fizz, et qui s’adapte à son ORM à lui (pop), qui permet de générer tout un tas de trucs automatiquement, mais je préfère mon couple gORM + migrate, sachant que mes modèles sont la plupart du temps générés pour moi par l’extension gorm-grpc.

+0 -0
Connectez-vous pour pouvoir poster un message.
Connexion

Pas encore membre ?

Créez un compte en une minute pour profiter pleinement de toutes les fonctionnalités de Zeste de Savoir. Ici, tout est gratuit et sans publicité.
Créer un compte