Tech'n bolts home about archive // tbd-in-practice ddd // (drafts)
{ version:1.3, what:["java", "scala", "nosql", "amqp", "programing", "functional", "[t|b|d]dd", ...] }

[T|B|D]DD par la pratique 2 - Installation et mise en place

Introduction

Commençons par glaner à gauche et à droite les modules dont nous aurons besoin, nous voulons:

  • un module pour faire du BDD
  • un module pour faire du TDD
  • un module pour la persistence de notre modèle, nous prendrons dans un 1er temps une base de données postgres
  • un module pour effectuer des tâches asynchrones (~actor)
  • un module pour générer les uuid de nos entités

Preparation

Installer nodejs

Voir NodeJS et Installation. Pour ma part, j'ai suivi le lien precompiled package for MacOS.

Assurons-nous que le simple "Hello world" marche:

$ node -v
    v0.4.11
    $ echo "console.log('Hello World');" > hello.js
    $ node hello.js
    Hello World
    $ _

Installer Webworker (for actor like)

node-webworker

A WebWorkers implementation for NodeJS

$ npm install webworker
    webworker@0.8.4 ./node_modules/webworker
    $ _

Installer vows (for BDD)

VowsJS

Asynchronous behaviour driven development for Node.

$ npm install vows
    vows@0.5.11 ./node_modules/vows 
    └── eyes@0.1.6
    $ _

Installer nodeunit (for TDD)

nodeunit

$ npm install nodeunit
    nodeunit@0.5.5 ./node_modules/nodeunit 
    $ _

Installer node-postgres (Non-blocking PostgreSQL client)

node-postgres

$ npm install pg
    ...
    Checking for node prefix                 : ok /usr 
    Checking for program pg_config           : not found 
    .../node_modules/pg/wscript:16: error: The program ['pg_config'] is required
    pg@0.5.8 ./node_modules/pg 
    └── generic-pool@1.0.6
    $ _

In the mean time if you get a compilation failure during installation you have still successfully installed the module; however, you cannot use the native bindings -- only the pure javascript bindings.

Très bien, prenons ça pour argent comptant dans un premier temps, et nous nous contenterons de la version pure javascript (qui devrait suffir amplement pour démarrer).

Installer node-uuid (Generation de UUID de type 4)

node-uuid

$ npm install node-uuid
   node-uuid@1.2.0 ./node_modules/node-uuid
   $ _

Simplifions et centralisons les dépendences...

Installer toutes ces dépendances une à une devient rapidement fastidieux, surtout s'il faut le répéter à chaque fois que le projet est récupérer depuis les sources. Heureusement, il existe un moyen de centraliser et conserver ces dépendances en créant un fichier package.json. Ce fichier est très similaire au fichier pom.xml de Maven. Il décrit de manière succinte l'appplication ainsi que ses dépendances. Voir Introduction To npm pour plus d'information.

Par example:

{
      "author": "Arnauld",
      "name": "scrum-board",
      "description": "Scrum board: rambling around [D|T|B]DD",
      "version": "0.0.1",
      "repository": {
        "url": ""
      },
      "engines": {
        "node": "*"
      },
      "dependencies": {
        "webworker": "*",
        "vows": "*",
        "nodeunit": "*",
        "pg": "*",
        "node-uuid": "*"
      },
      "devDependencies": {},
      "main": "app.js"
    }

Du coup, la récupération des dépendances est beaucoup plus immédiate grâce à la commande npm install. Toutes les dépendances sont ainsi installées en une unique commande.

Structure du projet

Le projet prendra la forme suivante:

<project_dir>/
  +-- lib/
  |     +-- domain.js
  |     +-- event_store.js
  |     +-- repository.js
  |     +-- ...
  +-- node_modules/
  |     ...
  +-- samples/
  |     +-- hello.js
  |     +-- ...
  +-- specs/
  |     +-- project_specs.js
  |     +-- user_specs.js
  |     +-- ...
  +-- test/
  |     +-- user_test.js
  |     +-- domain_test.js
  |     +-- ...
  +-- package.json
  +-- watchr-conf.rb
  +-- ...

C'est à dire que

  • dans lib/ sera contenu le code de notre application
  • dans node_modules les modules requis et installés pour NodeJS
  • dans samples/ des snippets et autres petits tests sans importance
  • dans specs les tests fonctionnels (BDD)
  • dans test les tests unitaires (TDD)

Tests des specs: executer tous les tests fonctionnels présents dans le dossier specs/

node_modules/.bin/vows --spec specs/*

Tests unitaires: executer tous les tests unitaires présents dans le dossier test/

node_modules/.bin/nodeunit test

Test en continue

Préparons désormais notre environement de test de continue. Il s'agit de mettre en place des jobs qui seront régulièrement déclenchés (sur modification d'un fichier par exemple). Il est ainsi possible de tester continuellement l'application, en relançant les tests à chaque fois qu'un fichier source est modifié. Lorsque notre base de tests commencera à être conséquente, nous essaierons alors de rendre le mécanisme un peu plus malin afin de ne pas pénaliser notre environement en lançant l'intégralité des tests à chaque changement.

Parmis ces jobs nous allons aussi mettre en place un outil de vérification de syntaxe. L'execution du javascript étant relativement permissive dans certains environements d'execution, nous aurons ainsi une syntaxe plus propre, et plus portable. Cette vérification sera faite via la bibliothèque jslint.

L'outils que nous allons utilisé est un script Ruby appellé watchr. Il permet de brancher des appels de fonctions à chaque fois qu'un fichier surveillé est modifié. La liste des fichiers surveillés est déterminée par une expression régulière.

Installation de watchr (sous entend que ruby et rubygem soit correctement installé)

gem install watchr
    gem install ruby-fsevent

Installation de jslint (module NodeJS) afin de tester en continue la syntaxe javascript;

npm install jslint

Le fichier de configuration de watchr indique: * que nous surveillons tous les fichiers js du dossier lib et pour chacun d'eux, en cas de modification, nous déclenchons l'execution du program jslint sur le fichier détecté, lançons les tests unitaires, et les tests fonctionels. * que nous surveillons tous les fichiers js du dossier test et pour chacun d'eux, en cas de modification, nous vérifions sa syntaxe et déclenchons l'execution de tous tests. * que nous surveillons tous les fichiers js du dossier specs et pour chacun d'eux, en cas de modification, nous vérifions sa syntaxe et déclenchons l'execution des tests fonctionnels.

watchr-conf.rb

watch('^(lib/(.*)\.js)') do |m|
      jslint_check("#{m[1]}")
      test()
      specs()
    end

    watch('^(test/(.*)\.js)') do |m|
      jslint_check("#{m[1]}")
      test()
    end

    watch('^(specs/(.*)\.js)') do |m|
      jslint_check("#{m[1]}")
      specs()
    end

    def jslint_check(files_to_check)
      #system('clear')
      puts "Checking #{files_to_check}"
      system("node_modules/.bin/jslint #{files_to_check}")
    end

    def test()
      puts "Start tests"
      system("node_modules/.bin/nodeunit test")
    end

    def specs()
      puts "Start behavior tests"
      system("node_modules/.bin/vows --spec specs/*")
    end

start watchr:

watchr watchr-conf.rb

Pour plus de détails, je vous invite à consulter le livre Continuous Testing qui - même s'il traite essentiellement de Ruby, Rails et Javascript - donnent de bonne idées pour l'étendre à d'autres technologies.

Un démarrage en douceur

Dans une console, démarrons notre script de test continu watchr watchr-conf.rb.

Commençons par le test fonctionnel de création d'un projet, nous voulons que la création d'un projet:

  • retourne un objet de type Project
  • retourne un projet dont le nom est bien celui fournit

specs/project_specs.js

var vows = require('vows'),
      assert = require('assert');

  var domain = require('../lib/domain')

  vows.describe('Project').addBatch({
      'A new project created with a given name': {
          topic: function () { 
              return domain.create_project("mccallum")
          },

          'should return an instance of Project' : function(project) {
              assert.instanceOf (project, domain.Project);
          },

          'should have the specified name': function (project) {
              assert.equal (project.name(), 'mccallum');
          },
      }
  }).export(module); // Export the Suite

Après la sauvegarde, on obtient la sortie suivante sur la console:

Checking specs/project_specs.js

  specs/project_specs.js
  /*jslint node: true, es5: true */
    1 4,39: Expected ';' and instead saw 'vows'.
      var domain = require('../lib/domain')
    2 9,53: Expected ';' and instead saw '}'.
      return domain.create_project("mccallum")
  Start behavior tests

  node.js:134
          throw e; // process.nextTick error, or 'error' event on first tick
          ^
  Error: Cannot find module '../lib/domain'
      at Function._resolveFilename (module.js:317:11)
      at Function._load (module.js:262:25)
      at require (module.js:346:19)

On remarquera que notre javascript n'est pas tout à fait valide et qu'il manque deux ; aux lignes 4 et 9. Par ailleurs, nos tests échouent dû à l'absence de notre fichier lib/domain.js, ce qui est normal puisque nous ne l'avons pas encore écrit!

Après quelques tatonements (comment fait-on l'OOP en javascript...), on obtient le fichier suivant:

lib/domain.js

/**
   *  Project
   */
  var Project = function(project_name) {
    this._name = project_name;
  };

  exports.create_project = function(project_name) {
    return new Project(project_name);
  };

  // tells nodeJS to make the `Project` visible from outside this file
  // when using `require`
  exports.Project = Project

Après sauvegarde:

Checking lib/domain.js

  lib/domain.js
  /*jslint node: true, es5: true */
    1 12,26: Expected ';' and instead saw '(end)'.
      exports.Project = Project

  Start tests

  Start behavior tests

  ♢ Project

    A new project created with a given name
      ✓ should return an instance of Project
      ✗ should have the specified name
      TypeError: Object [object Object] has no method 'name'
      at Object.<anonymous> (/Users/arnauld/Projects/cqrs-ramblings/node-app/specs/project_specs.js:17:35)
      at runTest (/Users/arnauld/Projects/cqrs-ramblings/node-app/node_modules/vows/lib/vows.js:93:26)
      at EventEmitter.<anonymous> (/Users/arnauld/Projects/cqrs-ramblings/node-app/node_modules/vows/lib/vows.js:71:9)
      at EventEmitter.emit (events.js:81:20)
      at Array.0 (/Users/arnauld/Projects/cqrs-ramblings/node-app/node_modules/vows/lib/vows/suite.js:150:58)
      at EventEmitter._tickCallback (node.js:126:26)
   
  ✗ Errored » 1 honored ∙ 1 errored (0.008s)

Un de nos tests fonctionnel passe et l'autre échoue lamentablement. Le projet créé est bien une instance de Projet en revanche il ne dispose pas de la méthode name qui devrait permettre de renvoyer son nom. Fixons encore une fois le ; qui manque, et rajoutons la méthode manquante.

lib/domain.js

Project.prototype = {
    name : function () { return this._name; }
  };

Nous obtenons finalement la sortie suivante:

Checking lib/domain.js

  lib/domain.js
  /*jslint node: true, es5: true */
  No errors found.

  Start tests

  Start behavior tests

  ♢ Project

    A new project created with a given name
      ✓ should return an instance of Project
      ✓ should have the specified name
   
  ✓ OK » 2 honored (0.006s)

Hourra!!

Je passe rapidement sur la génération automatique d'un uuid pour notre projet (nous utilisons pour cela le module node-uuid qui fournit, via la variable uuid definie par le require, une méthode de génération), et notre code ressemble désormais à:

lib/domain.js

var uuid = require('node-uuid');

  /**
   *  Project
   */
  var Project = function(uuid, project_name) {
    this._name = project_name;
    this._uuid = uuid;
  };

  // *public* methods
  Project.prototype = {
    name : function () { return this._name; },
    uuid : function () { return this._uuid; }
  };

  exports.create_project = function(project_name) {
    var generated_id = uuid(); // ask `node-uuid` to generate a new one
    return new Project(, project_name);
  };

  exports.Project = Project;

Des tests unitaires ont été ajoutés avant chaque ajout de méthode sur notre classe Project et ressemble désormais à:

test/project_test.js

var domain = require("../lib/domain");

  var UUID_PATTERN = /[0-9a-f]{8}\-[0-9a-f]{4}\-[0-9a-f]{4}\-[0-9a-f]{4}\-[0-9a-f]{8}/;

  exports["create_project return the specified name"] = function (test) {
    var project = domain.create_project("mccallum");
    test.equal(project.name(), "mccallum");
    test.done();
  };

  exports["create_project generate a valid uuid"] = function (test) {
    var project = domain.create_project("mccallum");
    test.equal(UUID_PATTERN.test(project.uuid()), true);
    test.done();
  };

Notre fichiers de tests fonctionnels c'est aussi enrichi de quelques assertions supplémentaires notament sur l'unicité de notre uuid et sa représentation:

specs/project_specs.js

var vows = require('vows'),
      assert = require('assert');

  var domain = require('../lib/domain');

  var UUID_PATTERN = /[0-9a-f]{8}\-[0-9a-f]{4}\-[0-9a-f]{4}\-[0-9a-f]{4}\-[0-9a-f]{8}/;

  vows.describe('Project').addBatch({
      'A new project created with a given name': {
          topic: function () {
              return domain.create_project("mccallum");
          },

          'should return an instance of Project' : function(project) {
              assert.instanceOf (project, domain.Project);
          },

          'should have the specified name': function (project) {
              assert.equal (project.name(), 'mccallum');
          },

          'and a generated uuid': function (project) {
              assert.equal(UUID_PATTERN.test(project.uuid()), true);
          }
      },
      'New projects': {
          topic: function () { 
              return [ domain.create_project("mccallum"), domain.create_project("mccallum")];
          },

          'should have differents uuid': function (result) {
              assert.notEqual (result[0].uuid(), result[1].uuid());
          }
      }
  }).export(module); // Export the Suite

Notre console affiche donc fièrement:

Start tests
  --------------------------

  project_test
  ✔ create_project return the specified name
  ✔ create_project generate a valid uuid

  OK: 2 assertions (15ms)

  Start behavior tests
  --------------------------

  ♢ Project

    A new project created with a given name
      ✓ should return an instance of Project
      ✓ should have the specified name
      ✓ and a generated uuid
    New projects
      ✓ should have differents uuid
   
  ✓ OK » 4 honored (0.003s)

Et l'Event Sourcing dans tout ça ??

Tout ça c'est bien mais ce n'est pas très conforme avec notre idée de l'[Event Sourcing][event-sourcing]. En effet, la création du projet consiste bien en une transition d'état de rien vers un nouveau projet, et nous ne conservons aucune données de cette transition. Qui plus est, le projet est porteur de son état, il n'est donc pas possible de suivre les modifications qu'il subit, comme un changement de nom.

Rajoutons donc un évènement ProjectCreated, cet évènement sera porteur du nom du projet. Mais que devient notre uuid ? sa valeur est portée par le projet, et nous souhaitons qu'un projet puisse être reconstruit uniquement à partir de ses évènements. Nous déplaçons donc la génération du uuid et considérons que celui-ci est un attribut de notre évènement.

Commençons par écrire les tests unitaires qui décrivent ce que nous souhaitons:

test/project_test.js

...
  exports["create_project must generate an event of type 'project_created' in history"] = function (test) {
    var project = domain.create_project("mccallum");

    var events = project.events();
    test.ok(events instanceof Array);
    test.equal(events.length, 1);
    test.equal(events[0].event_type(), "project_created");
    test.done();
  };

Et la console nous affiche un échec dû à l'absence de notre méthode events() utilisée pour récupérer l'historique:

Checking test/project_test.js
  --------------------------

  test/project_test.js
  /*jslint node: true, es5: true */
  No errors found.

  Start tests
  --------------------------

  project_test
  ✔ create_project return the specified name
  ✔ create_project generate a valid uuid
  ✖ create_project must generate an event of type 'project_created' in history

  TypeError: Object #<Object> has no method 'events'
      at /Users/arnauld/Projects/cqrs-ramblings/node-app/test/project_test.js:20:26
      ...

Après quelques cycles (red+green+refactor) où nous rajoutons la méthode events qui renvoie systématiquement un tableau vide, puis un objet bidon.

Notre fichier de tests resemblent à:

...
  exports["a new project must have an `events` method to retrieve its history"] = function (test) {
    var project = domain.create_project("mccallum");

    var events = project.events();
    test.ok(events instanceof Array);
    test.done();
  };
  exports["a new project must have a single `event` in its history"] = function (test) {
    var project = domain.create_project("mccallum");

    var events = project.events();
    test.ok(events instanceof Array);
    test.equal(events.length, 1);
    test.done();
  };
  exports["a new project must have a single `event` in its history of type 'project_created'"] = function (test) {
    var project = domain.create_project("mccallum");

    var events = project.events();
    test.ok(events instanceof Array);
    test.equal(events.length, 1);
    test.equal(events[0].event_type(), "project_created");
    test.done();
  };

Et notre console affiche désormais que nous bloquons désormais sur le type de notre évènement.

Start tests
  --------------------------

  project_test
  ✔ create_project return the specified name
  ✔ create_project generate a valid uuid
  ✔ a new project must have an `events` method to retrieve its history
  ✔ a new project must have a single `event` in its history
  ✖ a new project must have a single `event` in its history of type 'project_created'

  TypeError: Object dummy_event has no method 'event_type'
      at /Users/arnauld/Projects/cqrs-ramblings/node-app/test/project_test.js:38:23
  ...  
  FAILURES: 1/8 assertions failed (11ms)

Modifions notre méthode de création de projet en passant non plus le nom et l'identifiant du projet mais l'évènement souhaité. Dans la foulée nous rajoutons la méthode apply qui va appliquer cet évènement à notre projet. De la même manière que si l'on rejouait l'historique du projet. Notre code ressemble désormais à (je passe toutes les petites galères de syntaxes javascript, jslint étant là pour me rappeller à l'ordre à chaque sauvegarde):

lib/domain.js

var uuid = require('node-uuid');

  /**
   *  Project
   */

  var ProjectCreated = function(project_id, project_name) {
    // wrapping functions to make values *immutables*
    this.event_type   = function() { return "project_created"; };
    this.project_name = function() { return project_name; };
    this.project_id   = function() { return project_id;   };
  };

  var Project = function(project_id, project_name) {
    this.apply(new ProjectCreated(project_id, project_name));
  };

  // public method
  Project.prototype = {
    name : function () { return this._name; },
    uuid : function () { return this._uuid; },
    events: function () { return ["dummy_event"]; },
    apply: function (event) {
      switch(event.event_type()) {
        case "project_created" :
          this._name = event.project_name();
          this._uuid = event.project_id();
          break;
        default:
          throw new Error("Unknown event type: " + event.event_type());
      }
    }
  };

  exports.create_project = function(project_name) {
    return new Project(uuid(), project_name);
  };

  exports.Project = Project;

Après sauvegarde, on vérifie que les tests précédents continuent de fonctionner correctement, même après notre refactoring. Hourra!! même nos tests fonctionnels continuent de passer.

Start behavior tests
  --------------------------

  ♢ Project

    A new project created with a given name
      ✓ should return an instance of Project
      ✓ should have the specified name
      ✓ and a generated uuid
    New projects
      ✓ should have differents uuid
   
  ✓ OK » 4 honored (0.002s)

Nous avons toujours le même test unitaire qui ne passe pas puisque nous n'avons pas modifié la gestion de l'historique encore.

Ajoutons désormais l'historique à notre projet. Et tous nos tests passent! un peu de refactoring et voila finalement notre code:

lib/domain.js

...
  // public method
  Project.prototype = {
    name : function () { return this._name; },
    uuid : function () { return this._uuid; },
    events: function () { return this._events; }
    apply: function (event) {
      switch(event.event_type()) {
        case "project_created" :
          this._name = event.project_name();
          this._uuid = event.project_id();
          break;
        default:
          throw new Error("Unknown event type: " + event.event_type());
      }

      // still there means the event was correctly handled, thus keep it!
      if(typeof this._events === 'undefined') {
        this._events = [];
      }
      this._events[this._events.length] = event;
    }
  };

Restons sur un succès, et arrêtons nous là pour aujourd'hui.

Dans notre prochain article, nous généraliserons la gestion de l'historique afin de pouvoir la réutiliser dans nos autres entités. Nous nous inspirerons du design des AggregateRoot tel que généralement décrit dans les articles autours cqrs et dans Super Simple CQRS Example - github. Pour plus d'information, je vous invite à consulter les liens ici. Enfin nous mettrons en place la reconstruction du projet par son historique.

Fork me on GitHub