Maison  >  Article  >  développement back-end  >  De PHPUnit à Go : tests unitaires basés sur les données pour les développeurs Go

De PHPUnit à Go : tests unitaires basés sur les données pour les développeurs Go

Patricia Arquette
Patricia Arquetteoriginal
2024-11-12 19:21:02244parcourir

From PHPUnit to Go: Data-Driven Unit Testing for Go Developers

Dans cet article, nous explorerons comment intégrer l'état d'esprit des tests unitaires PHP, en particulier l'approche du fournisseur de données du framework PHPUnit, dans Go. Si vous êtes un développeur PHP expérimenté, vous connaissez probablement le modèle du fournisseur de données : collecter les données de test séparément dans des tableaux bruts et introduire ces données dans une fonction de test. Cette approche rend les tests unitaires plus propres, plus maintenables et adhère à des principes tels que Ouvert/Fermé.

Pourquoi l'approche du fournisseur de données ?

L'utilisation d'une approche de fournisseur de données pour structurer les tests unitaires dans Go offre plusieurs avantages, notamment :

Lisibilité et extensibilité améliorées : les tests sont organisés visuellement, avec des tableaux clairement séparés en haut représentant chaque scénario de test. La clé de chaque tableau décrit le scénario, tandis que son contenu contient les données permettant de tester ce scénario. Cette structure rend le fichier agréable à travailler et facile à étendre.

Séparation des préoccupations : le modèle de fournisseur de données maintient les données et la logique de test séparées, ce qui donne lieu à une fonction légère et découplée qui peut rester largement inchangée au fil du temps. L'ajout d'un nouveau scénario nécessite uniquement d'ajouter plus de données au fournisseur, en gardant la fonction de test ouverte aux extensions mais fermée pour modification – une application pratique du principe ouvert/fermé dans les tests.

Dans certains projets, j'ai même vu des scénarios suffisamment denses pour justifier l'utilisation d'un fichier JSON distinct comme source de données, construit manuellement et transmis au fournisseur, qui à son tour fournit des données à la fonction de test.

Quand est-il très encouragé de recourir à des fournisseurs de données ?

L'utilisation de fournisseurs de données est particulièrement encouragée lorsque vous disposez d'un grand nombre de scénarios de test avec des données variables : chaque scénario de test est conceptuellement similaire mais ne diffère que par l'entrée et la sortie attendue.

Le mélange des données et de la logique dans une seule fonction de test peut réduire l'expérience du développeur (DX). Cela conduit souvent à :

Surcharge de verbosité : code redondant qui répète des instructions avec de légères variations de données, conduisant à une base de code verbeuse sans avantage supplémentaire.

Clarté réduite : l'analyse de la fonction de test devient une corvée lorsque l'on tente d'isoler les données de test réelles du code environnant, ce que l'approche du fournisseur de données atténue naturellement.

Bien, alors qu'est-ce qu'un fournisseur de données exactement ?

Le modèle DataProvider dans PHPUnit où, essentiellement, la fonction fournisseur fournit à la fonction de test différents ensembles de données qui sont consommés dans une boucle implicite. Il garantit le principe DRY (Don't Repeat Yourself) et s'aligne également sur le principe ouvert/fermé, en facilitant l'ajout ou la modification de scénarios de test sans altérer la logique de la fonction de test de base.

Résoudre le problème sans fournisseur de données ?

Pour illustrer les inconvénients de la verbosité, de la duplication de code et des problèmes de maintenance, voici un extrait d'un exemple de test unitaire pour la fonction de tri à bulles sans l'aide des fournisseurs de données :

<?php

declare(strict_types=1);

use PHPUnit\Framework\TestCase;

final class BubbleSortTest extends TestCase
{
    public function testBubbleSortEmptyArray()
    {
        $this->assertSame([], BubbleSort([]));
    }

    public function testBubbleSortOneElement()
    {
        $this->assertSame([0], BubbleSort([0]));
    }

    public function testBubbleSortTwoElementsSorted()
    {
        $this->assertSame([5, 144], BubbleSort([5, 144]));
    }

    public function testBubbleSortTwoElementsUnsorted()
    {
        $this->assertSame([-7, 10], BubbleSort([10, -7]));
    }

    public function testBubbleSortMultipleElements()
    {
        $this->assertSame([1, 2, 3, 4], BubbleSort([1, 3, 4, 2]));
    }

    // And so on for each test case, could be 30 cases for example.

    public function testBubbleSortDescendingOrder()
    {
        $this->assertSame([1, 2, 3, 4, 5], BubbleSort([5, 4, 3, 2, 1]));
    }

    public function testBubbleSortBoundaryValues()
    {
        $this->assertSame([-2147483647, 2147483648], BubbleSort([2147483648, -2147483647]));
    }
}

Y a-t-il des problèmes avec le code ci-dessus ? bien sûr :

Verbosité : chaque cas de test nécessite une méthode distincte, ce qui entraîne une base de code volumineuse et répétitive.

Duplication : La logique de test est répétée dans chaque méthode, variant uniquement en fonction de l'entrée et de la sortie attendue.

Violation ouverte/fermée : l'ajout de nouveaux cas de test nécessite de modifier la structure de la classe de test en créant plus de méthodes.

Résoudre le problème du fournisseur de données !

Voici la même suite de tests refactorisée pour utiliser un fournisseur de données

<?php

declare(strict_types=1);

use PHPUnit\Framework\TestCase;

final class BubbleSortTest extends TestCase
{
    /**
     * Provides test data for bubble sort algorithm.
     *
     * @return array<string, array>
     */
    public function bubbleSortDataProvider(): array
    {
        return [
            'empty' => [[], []],
            'oneElement' => [[0], [0]],
            'twoElementsSorted' => [[5, 144], [5, 144]],
            'twoElementsUnsorted' => [[10, -7], [-7, 10]],
            'moreThanOneElement' => [[1, 3, 4, 2], [1, 2, 3, 4]],
            'moreThanOneElementWithRepetition' => [[1, 4, 4, 2], [1, 2, 4, 4]],
            'moreThanOneElement2' => [[7, 7, 1, 0, 99, -5, 10], [-5, 0, 1, 7, 7, 10, 99]],
            'sameElement' => [[1, 1, 1, 1], [1, 1, 1, 1]],
            'negativeNumbers' => [[-5, -2, -10, -1, -3], [-10, -5, -3, -2, -1]],
            'descendingOrder' => [[5, 4, 3, 2, 1], [1, 2, 3, 4, 5]],
            'randomOrder' => [[9, 2, 7, 4, 1, 6, 3, 8, 5], [1, 2, 3, 4, 5, 6, 7, 8, 9]],
            'duplicateElements' => [[2, 2, 1, 1, 3, 3, 4, 4], [1, 1, 2, 2, 3, 3, 4, 4]],
            'largeArray' => [[-1, -10000, -12345, -2032, -23, 0, 0, 0, 0, 10, 10000, 1024, 1024354, 155, 174, 1955, 2, 255, 3, 322, 4741, 96524], [-1, -10000, -12345, -2032, -23, 0, 0, 0, 0, 10, 10000, 1024, 1024354, 155, 174, 1955, 2, 255, 3, 322, 4741, 96524]],
            'singleNegativeElement' => [[-7], [-7]],
            'arrayWithZeroes' => [[0, -2, 0, 3, 0], [-2, 0, 0, 0, 3]],
            'ascendingOrder' => [[1, 2, 3, 4, 5], [1, 2, 3, 4, 5]],
            'descendingOrderWithDuplicates' => [[5, 5, 4, 3, 3, 2, 1], [1, 2, 3, 3, 4, 5, 5]],
            'boundaryValues' => [[2147483648, -2147483647], [-2147483647, 2147483648]],
            'mixedSignNumbers' => [[-1, 0, 1, -2, 2], [-2, -1, 0, 1, 2]],
        ];
    }

    /**
     * @dataProvider bubbleSortDataProvider
     *
     * @param array<int> $input
     * @param array<int> $expected
     */
    public function testBubbleSort(array $input, array $expected)
    {
        $this->assertSame($expected, BubbleSort($input));
    }
}

Y a-t-il des avantages à utiliser le fournisseur de données ? oh ouais :

Concision : Toutes les données de test sont centralisées dans une seule méthode, éliminant ainsi le besoin de plusieurs fonctions pour chaque scénario.

Lisibilité améliorée : Chaque cas de test est bien organisé, avec des clés descriptives pour chaque scénario.

Principe ouvert/fermé : de nouveaux cas peuvent être ajoutés au fournisseur de données sans altérer la logique de test de base.

DX (expérience de développement) amélioré : la structure des tests est propre, attrayante pour les yeux, ce qui motive même les développeurs paresseux à l'étendre, à la déboguer ou à la mettre à jour.

Faire venir les fournisseurs de données

  • Go n'a pas de modèle de fournisseur de données natif comme PHPUnit, nous devons donc utiliser une approche différente. Il peut y avoir de nombreuses implémentations avec plusieurs niveaux de complexité. Voici une implémentation moyenne qui pourrait être candidate pour simuler un fournisseur de données dans Go Land.
package sort

import (
    "testing"

    "github.com/stretchr/testify/assert"
)

type TestData struct {
    ArrayList    map[string][]int
    ExpectedList map[string][]int
}

const (
    maxInt32 = int32(^uint32(0) >> 1)
    minInt32 = -maxInt32 - 1
)

var testData = &TestData{
    ArrayList: map[string][]int{
        "empty":                            {},
        "oneElement":                       {0},
        "twoElementsSorted":                {5, 144},
        "twoElementsUnsorted":              {10, -7},
        "moreThanOneElement":               {1, 3, 4, 2},
        "moreThanOneElementWithRepetition": {1, 4, 4, 2},
        "moreThanOneElement2":              {7, 7, 1, 0, 99, -5, 10},
        "sameElement":                      {1, 1, 1, 1},
        "negativeNumbers":                  {-5, -2, -10, -1, -3},
        "descendingOrder":                  {5, 4, 3, 2, 1},
        "randomOrder":                      {9, 2, 7, 4, 1, 6, 3, 8, 5},
        "duplicateElements":                {2, 2, 1, 1, 3, 3, 4, 4},
        "largeArray":                       {-1, -10000, -12345, -2032, -23, 0, 0, 0, 0, 10, 10000, 1024, 1024354, 155, 174, 1955, 2, 255, 3, 322, 4741, 96524},
        "singleNegativeElement":            {-7},
        "arrayWithZeroes":                  {0, -2, 0, 3, 0},
        "ascendingOrder":                   {1, 2, 3, 4, 5},
        "descendingOrderWithDuplicates":    {5, 5, 4, 3, 3, 2, 1},
        "boundaryValues":                   {2147483648, -2147483647},
        "mixedSignNumbers":                 {-1, 0, 1, -2, 2},
    },
    ExpectedList: map[string][]int{
        "empty":                            {},
        "oneElement":                       {0},
        "twoElementsSorted":                {5, 144},
        "twoElementsUnsorted":              {-7, 10},
        "moreThanOneElement":               {1, 2, 3, 4},
        "moreThanOneElementWithRepetition": {1, 2, 4, 4},
        "moreThanOneElement2":              {-5, 0, 1, 7, 7, 10, 99},
        "sameElement":                      {1, 1, 1, 1},
        "negativeNumbers":                  {-10, -5, -3, -2, -1},
        "descendingOrder":                  {1, 2, 3, 4, 5},
        "randomOrder":                      {1, 2, 3, 4, 5, 6, 7, 8, 9},
        "duplicateElements":                {1, 1, 2, 2, 3, 3, 4, 4},
        "largeArray":                       {-1, -10000, -12345, -2032, -23, 0, 0, 0, 0, 10, 10000, 1024, 1024354, 155, 174, 1955, 2, 255, 3, 322, 4741, 96524},
        "singleNegativeElement":            {-7},
        "arrayWithZeroes":                  {-2, 0, 0, 0, 3},
        "ascendingOrder":                   {1, 2, 3, 4, 5},
        "descendingOrderWithDuplicates":    {1, 2, 3, 3, 4, 5, 5},
        "boundaryValues":                   {-2147483647, 2147483648},
        "mixedSignNumbers":                 {-2, -1, 0, 1, 2},
    },
}

func TestBubble(t *testing.T) {

    for testCase, array := range testData.ArrayList {
        t.Run(testCase, func(t *testing.T) {
            actual := Bubble(array)
            assert.ElementsMatch(t, actual, testData.ExpectedList[testCase])
        })

    }
}
  • Nous définissons essentiellement deux cartes/listes : une pour les données d'entrée et la seconde pour les données attendues. Nous veillons à ce que chaque scénario des deux côtés soit référencé via la même clé de carte des deux côtés.
  • L'exécution des tests est alors une question de boucle dans une fonction simple qui parcourt les listes d'entrées/attendues préparées.
  • À l'exception de certains types de passe-partout ponctuels, les modifications des tests ne devraient se produire que du côté des données, la plupart du temps, aucun changement ne devrait altérer la logique de la fonction exécutant les tests, atteignant ainsi les objectifs dont nous avons parlé ci-dessus : réduire le travail de test se résume à une question de préparation des données brutes.

Bonus : un référentiel Github implémentant la logique présentée dans cet article de blog peut être trouvé ici https://github.com/MedUnes/dsa-go. Jusqu'à présent, il contient des actions Github exécutant ces tests et affichant même ce super célèbre badge vert ;)

À bientôt dans le prochain article informatif [espérons-le] !

Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!

Déclaration:
Le contenu de cet article est volontairement contribué par les internautes et les droits d'auteur appartiennent à l'auteur original. Ce site n'assume aucune responsabilité légale correspondante. Si vous trouvez un contenu suspecté de plagiat ou de contrefaçon, veuillez contacter admin@php.cn