Dans la première partie de cette série sur la bibliothèque de tests oclif, nous avons utilisé une approche de développement piloté par les tests pour construire notre time-tracker
CLI. Nous avons parlé du framework oclif, qui aide les développeurs à se passer de la configuration et du passe-partout afin qu’ils puissent écrire la viande de leurs applications CLI. Nous avons également parlé de @oclif/test et @oclif/fancy-test, qui s’occupent de la configuration et du démontage répétitifs afin que les développeurs puissent se concentrer sur l’écriture de leurs tests Mocha.
Notre time-tracker
l’application est une CLI multi-commandes. Nous avons déjà écrit des tests et implémenté notre première commande pour ajouter un nouveau projet à notre tracker. Ensuite, nous allons écrire des tests et implémenter notre commande « start timer ».
Pour rappel, l’application finale est publiée sur GitHub comme référence au cas où vous vous heurterez à un barrage routier.
Premier test de la commande Start Timer
Maintenant que nous pouvons ajouter un nouveau projet à notre suivi du temps, nous devons pouvoir démarrer le chronomètre pour ce projet. L’utilisation de la commande ressemblerait à ceci :
time-tracker start-timer project-one
Puisque nous adoptons une approche TDD, nous allons commencer par écrire le test. Pour notre test de chemin heureux, « project-one » existe déjà, et nous pouvons simplement démarrer la minuterie pour cela.
// PATH: test/commands/start-timer.test.js
const {expect, test} = require('@oclif/test')
const StartTimerCommand = require('../../src/commands/start-timer')
const MemoryStorage = require('../../src/storage/memory')
const {generateDb} = require('../test-helpers')
const someDate = 1631943984467
describe('start timer', () => {
test
.stdout()
.stub(StartTimerCommand, 'storage', new MemoryStorage(generateDb('project-one')))
.stub(Date, 'now', () => someDate)
.command(['start-timer', 'project-one'])
.it('should start a timer for "project-one"', async ctx => {
expect(await StartTimerCommand.storage.load()).to.eql({
activeProject: 'project-one',
projects: {
'project-one': {
activeEntry: 0,
entries: [
{
startTime: new Date(someDate),
endTime: null,
},
],
},
},
})
expect(ctx.stdout).to.contain('Started a new time entry on "project-one"')
})
})
Il y a beaucoup de similitude entre ce test et le premier test de notre commande « ajouter un projet ». Une différence, cependant, est le supplément stub()
appel. Puisque nous allons démarrer la minuterie avec new Date(Date.now())
, notre code de test écrasera de manière préventive Date.now()
rendre someDate
. Bien que nous ne nous soucions pas de la valeur de someDate
est, ce qui est important, c’est qu’il est fixé.
Lorsque nous exécutons notre test, nous obtenons l’erreur suivante :
Error: Cannot find module '../../src/commands/start-timer'
Il est temps d’écrire du code d’implémentation !
Commencer à implémenter la commande Start Time
Nous devons créer un fichier pour notre start-timer
commander. Nous dupliquons le add-project.js
fichier et renommez-le en start-timer.js
. Nous éliminons la plupart des run
méthode, et nous renommons la classe de commande en StartTimerCommand
.
const {Command, flags} = require('@oclif/command')
const FilesystemStorage = require('../storage/filesystem')
class StartTimerCommand extends Command {
async run() {
const {args} = this.parse(StartTimerCommand)
const db = await StartTimerCommand.storage.load()
await StartTimerCommand.storage.save(db)
}
}
StartTimerCommand.storage = new FilesystemStorage()
StartTimerCommand.description = `Start a new timer for a project`
StartTimerCommand.flags = {
name: flags.string({char: 'n', description: 'name to print'}),
}
module.exports = StartTimerCommand
Maintenant, lorsque nous exécutons à nouveau le test, nous voyons que le db
n’a pas été mis à jour comme nous l’avions prévu.
1) start timer should start a timer for "project-one": AssertionError: expected { Object (activeProject, projects) } to deeply equal { Object (activeProject, projects) } + expected - actual { - "activeProject": [null] + "activeProject": "project-one" "projects": { "project-one": { - "activeEntry": [null] - "entries": [] + "activeEntry": 0 + "entries": [ + { + "endTime": [null] + "startTime": [Date: 2021-09-18T05:46:24.467Z] + } + ] } } } at Context.<anonymous> (test/commands/start-timer.test.js:16:55) at async Object.run (node_modules/fancy-test/lib/base.js:44:29) at async Context.run (node_modules/fancy-test/lib/base.js:68:25)
Pendant que nous y sommes, nous savons également que nous devrions enregistrer quelque chose pour dire à l’utilisateur ce qui vient de se passer. Mettons donc à jour la méthode run avec le code pour le faire.
const {args} = this.parse(StartTimerCommand)
const db = await StartTimerCommand.storage.load()
if (db.projects && db.projects[args.projectName]) {
db.activeProject = args.projectName
// Set the active entry before we push so we can take advantage of the fact
// that the current length is the index of the next insert
db.projects[args.projectName].activeEntry = db.projects[args.projectName].entries.length
db.projects[args.projectName].entries.push({startTime: new Date(Date.now()), endTime: null})
}
this.log(`Started a new time entry on "${args.projectName}"`)
await StartTimerCommand.storage.save(db)
En exécutant à nouveau le test, nous constatons que nos tests sont tous réussis !
add project
✓ should add a new project
✓ should return an error if the project already exists (59ms)
start timer
✓ should start a timer for "project-one"
Triste chemin : démarrer une minuterie sur un projet inexistant
Ensuite, nous devons informer l’utilisateur s’il tente de démarrer une minuterie sur un projet qui n’existe pas. Commençons par écrire un test pour cela.
test .stdout() .stub(StartTimerCommand, 'storage', new MemoryStorage(generateDb('project-one'))) .stub(Date, 'now', () => someDate) .command(['start-timer', 'project-does-not-exist']) .catch('Project "project-does-not-exist" does not exist') .it('should return an error if the user attempts to start a timer on a project that doesn't exist', async _ => { // Expect that the storage is unchanged expect(await StartTimerCommand.storage.load()).to.eql({ activeProject: null, projects: { 'project-one': { activeEntry: null, entries: [], }, }, }) })
Et, nous échouons à nouveau.
1 failing 1) start timer should return an error if the user attempts to start a timer on a project that doesn't exist: Error: expected error to be thrown at Object.run (node_modules/fancy-test/lib/catch.js:8:19) at Context.run (node_modules/fancy-test/lib/base.js:68:36)
Écrivons du code pour corriger cette erreur. Nous ajoutons l’extrait de code suivant au début du run
méthode, juste après avoir chargé le db
du stockage.
if (!db.projects?.[args.projectName]) { this.error(`Project "${args.projectName}" does not exist`) }
On refait les tests.
add project ✓ should add a new project (47ms) ✓ should return an error if the project already exists (75ms) start timer ✓ should start a timer for "project-one" ✓ should return an error if the user attempts to start a timer on a project that doesn't exist
J’y suis arrivé! Bien sûr, il y a une autre chose que cette commande devrait faire. Imaginons que nous ayons déjà démarré une minuterie sur project-one
et nous voulons passer rapidement la minuterie à project-two
. Nous nous attendrions à ce que la minuterie en marche project-one
s’arrêtera et une nouvelle minuterie s’allumera project-two
va commencer.
Arrêtez une minuterie, démarrez une autre
Nous répétons notre cycle rouge-vert TDD en écrivant d’abord un test pour représenter la fonctionnalité manquante.
test .stdout() .stub(StartTimerCommand, 'storage', new MemoryStorage({ activeProject: 'project-one', projects: { 'project-one': { activeEntry: 0, entries: [ { startTime: new Date(someStartDate), endTime: null, }, ], }, 'project-two': { activeEntry: null, entries: [], }, }, })) .stub(Date, 'now', () => someDate) .command(['start-timer', 'project-two']) .it('should end the running timer from another project before starting a timer on the requested one', async ctx => { // Expect that the storage is unchanged expect(await StartTimerCommand.storage.load()).to.eql({ activeProject: 'project-two', projects: { 'project-one': { activeEntry: null, entries: [ { startTime: new Date(someStartDate), endTime: new Date(someDate), }, ], }, 'project-two': { activeEntry: 0, entries: [ { startTime: new Date(someDate), endTime: null, }, ], }, }, }) expect(ctx.stdout).to.contain('Started a new time entry on "project-two"') })
Ce test nécessite un autre horodatage, que nous appelons someStartDate
. Nous ajoutons que près du haut de notre start-timer.test.js
déposer:
const someStartDate = 1631936940178 const someDate = 1631943984467
Ce test est plus long que les autres tests, mais c’est parce que nous avions besoin d’un db
initialisé dans MemoryStorage pour représenter ce cas de test. Vous pouvez voir que, initialement, nous avons une entrée avec un startTime
et non endTime
dans project-one
. Dans l’assertion, vous remarquerez que le endTime
dans project-one
est renseigné, et il y a une nouvelle entrée active dans project-two
avec un startTime
et non endTime
.
Lorsque nous exécutons notre suite de tests, nous voyons l’erreur suivante :
1) start timer should end the running timer from another project before starting a timer on the requested one: AssertionError: expected { Object (activeProject, projects) } to deeply equal { Object (activeProject, projects) } + expected - actual { "activeProject": "project-two" "projects": { "project-one": { - "activeEntry": 0 + "activeEntry": [null] "entries": [ { - "endTime": [null] + "endTime": [Date: 2021-09-18T05:46:24.467Z] "startTime": [Date: 2021-09-18T03:49:00.178Z] } ] } at Context.<anonymous> (test/commands/start-timer.test.js:76:55) at async Object.run (node_modules/fancy-test/lib/base.js:44:29) at async Context.run (node_modules/fancy-test/lib/base.js:68:25)
Cette erreur nous indique que notre CLI a correctement créé une nouvelle entrée dans project-two
, mais il n’a pas d’abord mis fin à la minuterie sur project-one
. Notre application n’a pas non plus changé le activeEntry
de 0
à null
dans project-one
comme on s’y attendait.
Corrigeons le code pour résoudre ce problème. Juste après avoir vérifié que le projet demandé…