DéveloppeurWeb.Com
    DéveloppeurWeb.Com
    • Agile Zone
    • AI Zone
    • Cloud Zone
    • Database Zone
    • DevOps Zone
    • Integration Zone
    • Web Dev Zone
    DéveloppeurWeb.Com
    Home»Web Dev Zone»Développement piloté par les tests avec la bibliothèque de test oclif : première partie
    Web Dev Zone

    Développement piloté par les tests avec la bibliothèque de test oclif : première partie

    novembre 8, 2021
    Développement piloté par les tests avec la bibliothèque de test oclif : première partie
    Share
    Facebook Twitter Pinterest Reddit WhatsApp Email

    Bien que l’écriture d’un outil CLI puisse être très amusante, la configuration initiale et le passe-partout (analyse des arguments et des indicateurs, validation, sous-commandes) sont généralement les mêmes pour chaque CLI, et c’est un frein. C’est là que le framework oclif sauve la mise. Le passe-partout pour l’écriture d’une CLI à commande unique ou à plusieurs commandes disparaît et vous pouvez rapidement accéder au code que vous réellement veux écrire.

    Mais attendez, il y a plus ! oclif dispose également d’un framework de test qui vous permet d’exécuter votre CLI de la même manière qu’un utilisateur, en capturant la sortie standard et les erreurs afin que vous puissiez tester les attentes. Dans cet article, je vais vous montrer comment écrire et tester facilement une application CLI oclif.

    Qu’allons-nous construire ?

    Nous sommes tous fatigués de travailler sur l’application TODO typique. Au lieu de cela, construisons quelque chose de différent mais simple. Nous utiliserons une approche de développement piloté par les tests (TDD) pour créer une application de suivi du temps. Notre CLI nous permettra de faire ce qui suit :

    • Ajouter des projets
    • Minuteurs de début et de fin sur ces projets
    • Afficher le total des dépenses sur un projet
    • Visualiser le temps passé sur chaque entrée pour un projet donné

    Voici un exemple d’interaction avec le time-tracker L’interface de ligne de commande ressemble à :

    ~ time-tracker add-project project-one
    Created new project "project-one"
    
    ~ time-tracker start-timer project-one
    Started a new time entry on "project-one"
    
    ~ time-tracker start-timer project-two
     >   Error: Project "project-two" does not exist
    
    ~ time-tracker add-project project-two
    Created new project "project-two"
    
    ~ time-tracker start-timer project-two
    Started a new time entry on "project-two"
    
    ~ time-tracker end-timer project-two
    Ended time entry for "project-two"
    
    ~ time-tracker list-projects
    project-one (0h 0m 13.20s)
    - 2021-09-20T13:13:09.192Z - 2021-09-20T13:13:22.394Z (0h 0m 13.20s)
    project-two (0h 0m 7.79s)
    - 2021-09-20T13:13:22.394Z - 2021-09-20T13:13:30.189Z (0h 0m 7.79s)

    Nous gérerons toutes les données sur les projets ajoutés et les minuteurs actifs dans une « base de données » (un simple fichier de données JSON).

    Le code source de notre projet d’application de suivi du temps peut être trouvé ici.

    Puisque nous procédons à la manière TDD, plongeons-nous dans… les tests d’abord !

    Notre Time-Tracker : Fonctionnalités et Tests

    Au fur et à mesure que nous décrivons les fonctionnalités de notre application, nous devrions penser aux tests que nous pouvons écrire pour affirmer les attentes que nous avons pour ces fonctionnalités. Voici une liste des fonctionnalités de notre application :

    • Créer un nouveau projet
      • Chemin heureux: Le nouveau projet est créé et son enregistrement est stocké dans la base de données sous-jacente. L’utilisateur reçoit un message de confirmation.
      • Triste chemin: Si le projet existe déjà, alors un message d’erreur apparaîtra à l’utilisateur. La base de données sous-jacente ne sera pas modifiée.
    • Démarrer un minuteur sur un projet
      • Chemin heureux: Le projet demandé existe déjà, nous pouvons donc commencer une nouvelle entrée de temps, en définissant le startTime à la date/heure actuelle. L’utilisateur recevra une notification lorsque la minuterie commencera.
      • Chemin heureux: Si le timer est déjà en cours d’exécution sur un autre projet, alors ce timer s’arrêtera et un nouveau timer commencera sur le projet demandé. L’utilisateur recevra une notification lorsque la minuterie commencera.
      • Triste chemin: Si le projet n’existe pas, alors un message d’erreur apparaîtra à l’utilisateur. La base de données sous-jacente ne sera pas modifiée.
    • Mettre fin à un minuteur sur un projet
      • Chemin heureux: Un timer est actif sur le projet demandé, nous pouvons donc mettre fin à ce timer et notifier l’utilisateur.
      • Triste chemin: Si le projet n’existe pas, alors un message d’erreur apparaîtra à l’utilisateur. La base de données sous-jacente ne sera pas modifiée.
      • Triste chemin: Si le projet existe sans minuterie active, l’utilisateur en sera informé. La base de données sous-jacente ne sera pas modifiée.
    • Projet de liste
      • Chemin heureux: Tous les projets, les temps totaux, les entrées et les temps d’entrée sont affichés à l’utilisateur.
    • Existence de la base de données (pour toutes les commandes)
      • Triste chemin: Si la time.json n’existe pas dans le répertoire courant, alors un message d’erreur apparaît à l’utilisateur.

    Pour le stockage des données – notre « base de données » – nous stockerons nos entrées de temps sur le disque au format JSON, dans un fichier appelé time.json. Voici un exemple de ce à quoi ce fichier peut ressembler :

    {
      "activeProject": "project-two",
      "projects": {
        "project-one": {
          "activeEntry":null,
          "entries": [
            {
              "startTime": "2021-09-18T06:25:55.874Z",
              "endTime": "2021-09-18T06:26:03.021Z"
            }, {
              "startTime": "2021-09-18T06:26:09.883Z",
              "endTime": "2021-09-18T06:26:47.585Z"
            }
          ]
        },
        "project-two": {
          "activeEntry": 1,
          "entries": [
            {
              "startTime": "2021-09-18T06:26:47.585Z",
              "endTime": "2021-09-18T06:27:13.776Z"
            }, {
              "startTime": "2021-09-18T06:52:54.791Z",
              "endTime": null
            }
          ]
        }
      }
    }

    Décisions de conception

    Enfin, couvrons certaines des décisions de conception pour notre application globale.

    Tout d’abord, nous allons stocker un activeProject au plus haut niveau de nos données JSON. Nous pouvons l’utiliser pour vérifier rapidement quel projet est actif. Deuxièmement, nous allons stocker un activeEntry champ dans chaque projet, qui stocke l’index de l’entrée en cours de traitement.

    Avec ces deux informations, on peut naviguer directement vers le projet actif et son entrée active afin de terminer le timer. Nous pouvons également déterminer instantanément si le projet a des entrées actives ou s’il y a des projets actifs.

    Configuration du projet

    Maintenant que nous avons jeté les bases, créons un nouveau projet et commençons à creuser.

    npx oclif multi time-tracker
    

    Cette commande crée une nouvelle application oclif multi-commandes. Avec une CLI multi-commandes, nous pouvons exécuter des commandes telles que time-tracker add-project project-one et time-tracker start-timer project-one. Dans ces exemples, les deux add-project et start-timer sommes séparé commandes, chacune stockée dans son propre fichier source dans le projet, mais elles tombent toutes sous l’égide time-tracker CLI.

    Un mot sur les talons

    Nous voulons profiter des assistants de test fournis par @oclif/test. Pour tester notre application particulière, nous aurons besoin d’écrire un simple stub. Voici pourquoi:

    Notre application écrit dans un timer.json fichier sur le système de fichiers. Imaginez si nous exécutions nos tests en parallèle et avions 10 tests qui écrivaient tous dans le même fichier en même temps. Cela deviendrait désordonné et produirait des résultats imprévisibles.

    Une meilleure approche serait de faire en sorte que chaque test écrive dans son propre fichier, teste ces fichiers et nettoie après nous-mêmes. Mieux encore, chaque test pourrait écrire dans un objet en mémoire au lieu d’un fichier, et nous pouvons affirmer nos attentes sur cet objet.

    La meilleure pratique lors de l’écriture de tests unitaires est de remplacer le pilote par autre chose. Dans notre cas, nous supprimerons la valeur par défaut FilesystemStorage chauffeur avec un MemoryStorage conducteur.

    @oclif/test est un simple wrapper autour de @oclif/fancy-test qui ajoute des fonctionnalités autour du test des commandes CLI. Nous allons utiliser la fonctionnalité de stub dans @oclif/fancy-test pour remplacer le pilote de stockage dans notre commande de test.

    Notre première commande : ajouter un projet

    Parlons maintenant de la commande « add project » et des parties importantes liées à la moquerie du système de fichiers. Chaque nouveau projet oclif commence par un hello.js fichier dans src/commands. Nous l’avons renommé en add-project.js dossier et rempli avec le strict minimum.

    // PATH: src/commands/add-project.js
    
    const {Command} = require('@oclif/command')
    const FilesystemStorage = require('../storage/filesystem')
    
    class AddProjectCommand extends Command {
      async run() {}
    }
    
    // This is the important line!
    AddProjectCommand.storage = new FilesystemStorage()
    
    AddProjectCommand.description = 'Add a new project to the time tracking database'
    
    AddProjectCommand.args = []
    
    module.exports = AddProjectCommand

    Stockage échangeable pour les tests

    Remarquez comment j’affecte statiquement un FilesystemStorage exemple à AddProjectCommand.storage. Cela me permet, dans mes tests, d’échanger le stockage du système de fichiers avec une implémentation de stockage en mémoire. Regardons le FilesystemStorage et MemoryStorage cours ci-dessous.

    // PATH: src/storage/filesystem.js
    
    const fs = require('fs/promises')
    
    class FilesystemStorage {
      constructor(initialData = {}) {
        this.data = initialData
      }
    
      load() {
        return fs.readFile('./time.json').then(file => {
          return JSON.parse(file.toString('utf-8'))
        }).catch(() => {
          // If reading the file results in an error then assume that the file didn't exist and return an empty object
          return Promise.resolve(this.data)
        })
      }
    
      save(data) {
        return fs.writeFile('./time.json', JSON.stringify(data))
      }
    }
    
    module.exports = FilesystemStorage

    // PATH: src/storage/memory.js
    
    class MemoryStorage {
      constructor(initialData = {}) {
        this.data = initialData
      }
    
      load() {
        return Promise.resolve(this.data)
      }
    
      save(data) {
        this.data = data
        return Promise.resolve()
      }
    }
    
    module.exports = MemoryStorage

    FilesystemStorage et MemoryStorage ont la même interface, nous pouvons donc échanger l’un contre l’autre dans nos tests.

    Le premier test de la commande Ajouter un projet

    Dans test/commands, nous avons renommé hello.test.js à add-project.test.js, et nous avons écrit notre premier test :

    // PATH: test/commands/add-project.test.js
    
    const { expect, test } = require('@oclif/test')
    const AddProjectCommand = require('../../src/commands/add-project')
    const MemoryStorage = require('../../src/storage/memory')
    
    describe('add project', () => {
      test
        .stdout()
        .stub(AddProjectCommand, 'storage', new MemoryStorage({}))
        .command(['add-project', 'project-one'])
        .it('should add a new project', async ctx => {
          expect(await AddProjectCommand.storage.load()).to.eql({
            activeProject: null,
            projects: {
              'project-one': {
                activeEntry: null,
                entries: [],
              },
            },
          })
          expect(ctx.stdout).to.contain('Created new project "project-one"')
        })
    })

    La magie opère dans le stub appel. Nous échangeons le FilesystemStorage avec MemoryStorage (avec un objet vide pour les données initiales). Ensuite, nous affirmons des attentes sur les contenus de stockage.

    Déballage du test Commande de @oclif/test

    Avant d’implémenter notre commande, assurons-nous de bien comprendre notre fichier de test. Notre describe bloquer les appels test, qui est le point d’entrée @oclif/fancy-test (réexporté de @oclif/test).

    Ensuite, le .stdout() La méthode capture la sortie de la commande, vous permettant d’affirmer vos attentes en utilisant ctx.stdout. Il y a aussi .stderr() méthode, mais nous verrons plus tard qu’il existe une autre méthode plus préférée pour gérer les erreurs dans @oclif/fancy-test.

    Pour la plupart des applications, vous ne feriez normalement pas d’assertions contre ce qui est écrit sur la sortie standard. Cependant, dans le cas d’une CLI, il s’agit de l’une de vos principales interfaces avec l’utilisateur, il est donc logique de tester par rapport à la sortie standard.

    Gardez à l’esprit qu’il y a un piège majeur ici! Si tu utilises console.log pour déboguer pendant que vous développez, puis .stdout() capturera également cette sortie. Sauf si vous vous opposez ctx.stdout, vous ne verrez probablement jamais cette sortie.

    .stub(AddProjectCommand, 'storage', new MemoryStorage({}))

    Nous avons parlé de la .stub méthode un peu déjà, mais ce que nous faisons ici est de remplacer la propriété statique de notre commande par MemoryStorage au lieu de la valeur par défaut FilesystemStorage.

    .command(['add-project', 'project-one'])
    

    La méthode .command c’est là que les choses deviennent vraiment cool avec @oclif/test. Cette ligne appelle…

    Share. Facebook Twitter Pinterest LinkedIn WhatsApp Reddit Email
    Add A Comment

    Leave A Reply Cancel Reply

    Catégories

    • Politique de cookies
    • Politique de confidentialité
    • CONTACT
    • Politique du DMCA
    • CONDITIONS D’UTILISATION
    • Avertissement
    © 2023 DéveloppeurWeb.Com.

    Type above and press Enter to search. Press Esc to cancel.