Taux de couverture des tests unitaires en golang

Cet article s'inspire de l'article de Rob Pike sur le blog officiel de golang (en anglais). Les exemples de cet article ne fonctionnent qu'avec une version 1.2 ou supérieure de golang.

Outillage

La couverture de test est un indicateur de qualité logicielle qui décrit le pourcentage du code qui est sollicité lorsque l'on exécute les tests unitaires automatisés qui lui sont associés. La bibliothèque standard de go fournit de quoi :
  • Développer des tests unitaires
  • Exécuter des tests unitaires
  • Faire un rapport sur la couverture des tests unitaires
  • Visualiser dans le code go les lignes qui sont sollicitées par les tests unitaires
L'approche de go n'est pas d'instrumenter le binaire puis – par débogage dynamique – de déterminer les endroits du code qui ont été sollicités par les tests unitaires, mais de compiler une version spéciale de votre programme à tester. Cette version spéciale de votre programme inclura une instrumentation qui générera un dump exhaustif de l'exécution.

Exemple de code et de test unitaire

Voici un exemple de code source en golang ne contenant qu'un seul fichier. Allez dans le répertoire désigné par votre variable d'environnement $GOPATH ; puis dans le répertoire src ; créez un sous-répertoire mesure. Dans ce répertoire ($GOPATH/src/mesure), créez le fichier mesure.go :
package mesure

func Taille(a int) string {
    switch {
    case a < 0:
        return "negatif"
    case a == 0:
        return "zéro"
    case a < 10:
        return "petit"
    case a < 100:
        return "grand"
    case a < 1000:
        return "énorme"
    }
    return "énorme"
}
Dans le même répertoire ($GOPATH/src/mesure), créez le fichier mesure_test.go. Ce test unitaire va exécuter deux fois la méthode Taille : une fois avec le paramètre "-1" et une seconde fois avec le paramètre "5".
package mesure

import "testing"

type Test struct {
    in  int
    out string
}

var tests = []Test{
    {-1, "negatif"},
    {5, "petit"},
}

func TestTaille(t *testing.T) {
    for i, test := range tests {
        taille := Taille(test.in)
        if taille != test.out {
            t.Errorf("#%d: Taille(%d)=%s; attendu %s", i, test.in, taille, test.out)
        }
    }
}
Ouvrez une console et allez dans le répertoire où sont stockées les sources de ce package ($GOPATH/src/mesure), puis lancez les tests unitaires :
%go test
PASS
ok      mesure  0.038s
Depuis la version 1.2, il est possible d'obtenir le taux de couverture des tests unitaires à l'aide de l'option -cover :
%go test -cover
PASS
coverage: 42.9% of statements
ok      mesure  0.045s
Comment cet indicateur est-il calculé ? Comme expliqué dans l'introduction, une version spéciale de l'exécutable – dans laquelle go injecte l'instrumentation des tests unitaires – est compilée. Regardons à quoi ressemble code source de cette version spéciale :
func Taille(a int) string {
    GoCover.Count[0] = 1
    switch {
    case a < 0:
        GoCover.Count[2] = 1
        return "negatif"
    case a == 0:
        GoCover.Count[3] = 1
        return "zéro"
    case a < 10:
        GoCover.Count[4] = 1
        return "petit"
    case a < 100:
        GoCover.Count[5] = 1
        return "grand"
    case a < 1000:
        GoCover.Count[6] = 1
        return "énorme"
    }
    GoCover.Count[1] = 1
    return "énorme"
}
Chaque partie exécutable du programme est complétée avec un compteur d'exécution qui, une fois qu'il est exécuté, enregistre que cette section a été exécutée. Le compteur est instrumenté dans le code source par l'outil de calcul de la couverture de test. Une fois que le code a été exécuté, l'outil fait les comptes et il est capable de déduire le pourcentage du code qui a été exécuté. Par exemple – en relisant le test unitaire – on sait que les lignes suivantes seront exécutées par les tests unitaires (soit un peu moins que la moitié du code):
  • switch { et à deux reprises, puisque le test unitaire fait deux exécutions (avec "-1" et "5")
  • case a < 0:
  • return "negatif"
  • case a < 10:
  • return "petit"
Cette instrumentation du code pourrait avoir l'air coûteuse, cependant le résultat compilé ne correspond qu'à une seule instruction MOV en langage machine – pour chaque ligne de code. En réalité, l'overhead (ou surplus dû à l'instrumentation du code contrôlant le taux de couverture) n'est que de 3% lorsque le test est plus réaliste et qu'il couvre davantage de cas. Il est donc raisonnable d'ajouter le calcul du taux de couverture dans votre processus de test unitaire.

On rappelle qu'il ne s'agit là que d'une version spéciale de votre exécutable et dédiée au test unitaire (la version compilée par la commande go build ne contiendra pas cette instrumentation).

Visualiser les résultats

L'indicateur du taux de couverture est intéressant, mais il faudrait savoir ce qui a été exécuté. La commande suivante
go test -coverprofile=coverage.out
Permet de générer un fichier de statistique (dans ce exemple, coverage.out) avec les lignes de code qui ont été exécutées ou pas
mode: set
mesure\mesure.go:3.27,4.12 1 1
mesure\mesure.go:16.5,16.21 1 0
mesure\mesure.go:5.5,6.25 1 1
mesure\mesure.go:7.5,8.22 1 0
mesure\mesure.go:9.5,10.23 1 1
mesure\mesure.go:11.5,12.23 1 0
mesure\mesure.go:13.5,14.25 1 0
On peut ensuite exploiter ce fichier de statistique en ligne de commande. Par exemple, avec cette commande qui donne le taux de couverture fonction par fonction :
go tool cover -func=coverage.out
L'outil est également capable de générer un rapport HTML montrant les lignes de code sollicitées par le test unitaire (la commande ouvre le fichier HTML avec votre navigateur par défaut) :
go tool cover -html=coverage.out
Toujours avec notre exemple de code, voici ce qui serait généré : en vert, les lignes couvertes par les tests unitaires ; en rouge, les lignes non couvertes et en gris, les lignes où le code n'a pas été instrumenté :
  • Exemple de rapport de couverture de test unitaire en go

Les heat maps

Un des avantages de cette approche (instrumentation dans le code source) est qu'il est possible d'obtenir d'autres statistiques. Par exemple, on pourrait se demander non seulement si une ligne de code a été exécutée, mais également combien de fois elle a été exécutée. La commande go test accepte une option -covermode pouvant avoir les valeurs suivantes :
  • set : déterminer si une ligne de code a été exécutée ou pas.
  • count : compter le nombre de fois qu'une ligne de code a été exécutée.
  • atomic : comme la valeur count, mais avec une méthode plus précise adaptée aux exécutions en parallèle.
La valeur par défaut est set et nous avons vu précédemment son fonctionnement et un exemple de fichier de sortie. Essayons maintenant l'option count avec notre exemple de code et de test unitaire. Lancez la commande suivante afin de compter le nombre d'exécutions des lignes de code couvertes par nos tests unitaires :
go test -covermode=count -coverprofile=count.out
Cette commande produit le fichier ci-dessous. On voit effectivement que la ligne contenant l'instruction switch a été exécutée deux fois (car il y a deux cas de test et ils passent tous les deux par ce point commun) :
mode: count
mesure\mesure.go:3.27,4.12 1 2
mesure\mesure.go:16.5,16.21 1 0
mesure\mesure.go:5.5,6.25 1 1
mesure\mesure.go:7.5,8.22 1 0
mesure\mesure.go:9.5,10.23 1 1
mesure\mesure.go:11.5,12.23 1 0
mesure\mesure.go:13.5,14.25 1 0
Comme précédemment, nous pouvons produire un rapport HTML qui montre cette fois-ci les endroits du code les plus sollicités par les tests unitaires :
  • Exemple de rapport de heatmap de couverture

L'intensité des lignes colorées en vert varie en fonction de la sollicitation du code : du plus clair pour les lignes de code les plus exécutées au plus foncé pour les lignes de code par lesquelles on est passé le moins souvent. Si vous passez la souris au-dessus d'une ligne de code, vous pouvez obtenir le nombre de fois qu'elle a été exécutée. Une information intéressante pour du profiling, car vous pourriez examiner les endroits du code les plus sollicités et décider s'il y a matière à les optimiser. Bien que cela ne soit pas la seule approche possible pour optimiser le code golang. Nous examinerons les outils dédiés à l'optimisation dans un prochain article.

Étiquettes :   test   cover 
Portrait de Benjamin BALET
Benjamin BALET
Consultant APM

Retrouvez mes cooordonées

Benjamin BALET sur viadeo






Vous aimerez aussi

Les lois de la réflexion

Une traduction du blog officiel de golang expliquant le mécanisme de la réflexion en Go.   Lire »

Comment écrire du code Go ?

Traduction d'une partie des spécifications officielles du langage Go, cet article explique comment développer en Go.   Lire »

Gobs le format natif d'échange de données en Go

Traduction d'un article du blog officiel expliquant comment échanger des données entre deux programmes golang grâce à un format natif   Lire »

Canaux et go routines avec ou sans état

Exemples d'utilisation du type chan et des go routines stateful et stateless   Lire »

Le modèle mémoire du runtime et du langage go

Traduction d'une partie des spécifications officielles du langage go, cet article explique comment go gère la mémoire.   Lire »

Commentaires

Soyez le premier à commenter cet article

Publier un commentaires

Tous les commentaires sont soumis à modération. Les balises HTML sont pour la plupart autorisées. Les liens contextuels et intéressants sont en follow.

(requis)
(requis)
(requis, mais non publié)
(recommandé si vous souhaitez être publié)