Tests End-to-End avec Docker Partie 5 : Docker Newman

Cet article fait suite à Tests End-to-End avec Docker parties 1 à 4.

Cette 5 ième partie montre comment utiliser l’application Postman et sa ligne de commande Newman pour exécuter des tests d’intégration de services REST.

Newman docker est utilisé afin d’étendre à l’exécution des tests d’api la technologie Docker par ailleurs déjà utilisée pour les phases précédentes du pipeline.

L’exemple complet est disponible sur github (branche e2e-docker-ansible).

Voir le README pour lancer le test E2E, l’exemple a été testé sous Mac OS avec Docker for Mac .


Le changement par rapport à la partie précédente de l’article est le remplacement du script bash pour tester les services avec curl par l’outil de test Postman

Il faut d’abord créer une collection de test Postman avec l’application, la collection de l’exemple a été créée avec la Mac App.

capture-decran-2016-12-12-a-22-58-35La collection contient un enchaînement de 4 requêtes http

  • recherche personne telle que nom=nom1 (elle existe déjà via l’alimentation batch)
  • post 1 personne telle que nom=nom3
  • post 1 autre personne telle que nom=nom3
  • recherche personnes telles que nom=nom3

A noter également le host de l’url résolu avec la variable {{restapp.host}}  du fichier d’environnement json de Postman.

Détail des assertions de test de l’url get de recherche de personnes

capture-decran-2016-12-12-a-23-12-36

  • vérification status response http 200
  • log et extraction de la réponse json
  • assertions sur les attributs de la réponse json

Pour voir l’exemple complet dans l’application Postman

  • importer la collection au format json
  • importer aussi l’environnement au format json et adapter la valeur du host

Voir tutoriaux du site Postman pour plus de détails.

Une fois la collection et l’environnement Postman créés, il suffit de les exécuter avec la ligne de commande Postman, autrement dit Newman

Le lanceur run.sh des tests E2E est légèrement remanié pour exécuter la collection à l’aide du container Docker newman. (voir ci-dessous #e2e : test postman)

capture-decran-2016-12-12-a-23-35-44

Le pipeline est donc définit comme ceci :

  • build image mysql avec ses schémas
  • package batch & build image docker
  • package restapp & build image docker
  • pre E2E deploy 
    • création structure
    • déploiement config
    • lancement containers
  • pre E2E prepare
    • insérer les données initiales via le batch
  • E2E tests
    • exécution collection postman via docker newman
  • post E2E = stop containers

 

Publicité

Tests End-to-End avec Docker Partie 4 : Docker & Ansible

Cet article fait suite à Tests End-to-End avec Docker parties 1 à 3.

Cette 4 ième partie montre comment utiliser Ansible pour orchestrer le démarrage des images Docker.

L’exemple complet est disponible sur github (branche e2e-docker-ansible).

Voir le README pour lancer le test E2E, l’exemple a été testé sous Mac OS avec Docker for Mac .


Explication de ce qui change avec Ansible sur la phase de préparation des tests e2e par rapport à l’approche bash

  • Un playbook ansible au format YAML spécifie le lancement des containers mysql et webapp
  • Pour exécuter le lancement il suffit d’utiliser la commande ansible-playbook avec le fichier deploy.yml ci-après (voir run complet de toutes les phases)
- name: run mysql and webapp docker containers
  hosts: 127.0.0.1
  connection: local

  tasks:

  - name: create log directory
    file: path={{ log }} state=absent
    file: path={{ log }} state=directory

  - name: create properties directory
    file: path={{ properties_dest }} state=directory

  - include: download.yml name=application

  - include: download.yml name=batch

  - name: run mysql
    docker:
      name: mysqldb
      image: mysql-sample
      ports: "3306:3306"
      env:
        MYSQL_ROOT_PASSWORD: dba
      state: started

  - include: wait.yml image=mysqldb

  - name: run webapp
    docker:
      name: webapp
      image: spring-boot-sample
      ports: "8080:8080"
      volumes:
      - "{{ log }}:/var/log"
      - "{{ properties_dest }}:/properties"
      state: started

  - include: wait.yml image=webapp
  • création de la structure nécessaire pour les logs et configurations
  • téléchargement des configurations
  • description lancement des containers avec module docker
  • attente des healthcheck ok des containers

download.yml : détail du téléchargement d’un type de fichier de configuration

- name: download {{ name }} properties
  get_url:
    url: "{{ properties_url }}/{{ name }}.properties"
    dest: "{{ properties_dest }}/{{ name }}.properties"
    force: yes

wait.yml : détail du traitement d’attente d’un container up & healthy

- name: wait {{ image }} up
  shell: docker inspect --format={{ '{{' }}.State.Health.Status{{ '}}' }} {{ image }}
  register: result
  until: result.stdout == "healthy"
  retries: 30
  delay: 5

Les autres phases ne sont pas remaniées par rapport à la partie 3

Concernant l’usage de docker compose, il est aussi possible de décrire et lancer les containers docker grâce à cet outil mais

  • on ne dispose pas de la même souplesse pour insérer les vérifications de healthchek par exemple (feature demandée mais non intégrée à ce jour)
  • la partie gestion des configurations des playbook est hors scope

Le pipeline est donc définit comme ceci :

  • build image mysql avec ses schémas
  • package batch & build image docker
  • package restapp & build image docker
  • pre e2e deploy 
    • création structure
    • déploiement config
    • lancement containers
  • pre e2e prepare
    • insérer les données initiales via le batch
  • e2e tests
  • post e2e = stop containers

Pour pratiquer ce pipeline dans un outil d’intégration continue on pourra s’appuyer sur l’usage de la docker registry afin de « pusher » les images docker après chaque phase de build et de « puller » les images en phase pré e2e.

Tests End-to-End avec Docker Partie 3 : Docker Maven Plugin

Cet article fait suite à Tests End-to-End avec Docker parties 1 & 2.

Cette 3 ième partie montre comment intégrer le build des images docker dans la phase package de l’outil de build Maven.

L’exemple complet est disponible sur github (branche e2e-docker-maven).

Voir le README pour lancer le test E2E, l’exemple a été testé sous Mac OS avec Docker for Mac .


Explication de ce qui change par rapport à l’approche de construction des images sans Maven

  •  Build avec maven package construit le packaging du batch et de la restapp, mais aussi les images docker
  • Ajout de la configuration du Docker maven plugin de fabric8 dans le pom.xml du batch
<plugin>
    <groupId>io.fabric8</groupId>
    <artifactId>docker-maven-plugin</artifactId>
    <version>0.15.16</version>
    <configuration>
        <verbose>true</verbose>
        <images>
            <image>
                <name>springbatch-sample:latest</name>
                <build>
                    <dockerFile>Dockerfile</dockerFile>
                    <assembly>
                        <descriptorRef>artifact</descriptorRef>
                    </assembly>
                </build>
            </image>
        </images>
    </configuration>
    <executions>
        <execution>
            <id>build</id>
            <phase>package</phase>
            <goals>
                <goal>build</goal>
            </goals>
        </execution>
    </executions>
</plugin>
  • Utilisation du Dockerfile dans répertoire par défaut src/main/docker
  • L’assembly prédéfinit artifact récupère le package jar spring boot afin de le copier dans l’image
    • dans Dockerfile on a « COPY maven/springbatch-sample*.jar /springbatch-sample.jar »
  • Exécution du goal build est liée à la phase package de maven

Dans le pom.xml de la restapp, le principe est le même que pour le batch

<plugin>
    <groupId>io.fabric8</groupId>
    <artifactId>docker-maven-plugin</artifactId>
    <version>0.15.16</version>
    <configuration>
        <verbose>true</verbose>
        <images>
            <image>
                <name>spring-boot-sample:latest</name>
                <build>
                    <dockerFile>Dockerfile</dockerFile>
                    <assembly>
                        <descriptorRef>artifact</descriptorRef>
                    </assembly>
                </build>
            </image>
        </images>
    </configuration>
    <executions>
        <execution>
            <phase>package</phase>
            <goals>
                <goal>build</goal>
            </goals>
        </execution>
    </executions>
</plugin>

Script résultant de build des images

mysql=$1
batch=$2
restapp=$3

cd $mysql && docker build -t mysql-sample:latest .
cd $batch && mvn clean package
cd $restapp && mvn clean package

Les autres phases ne sont pas remaniées par rapport à la partie 2

Le docker maven plugin de fabric8 permettrait aussi de lancer les containers dans la phase verify d’un build maven mais dans le cas présent cela ne rentre pas dans le pipeline qui est définit par la séquence suivante

  • build image mysql avec ses schémas
  • package batch & build image docker
  • package restapp & build image docker
  • pre e2e = run containers docker 
    • cette étape de préparation est responsable d’orchestrer les containers
    • renseigner l’environnement à utiliser
    • insérer les données initiales via le batch
  • e2e tests
  • post e2e = stop containers

Tests End-to-End avec Docker Partie 2

Cet article fait suite à Tests End-to-End avec Docker.

Cette 2 ième partie montre comment construire ses propres images avec les Dockerfile  au lieu de provisionner des images officielles par commandes bash.

L’exemple complet est disponible sur github (branche e2e-dockerfile).

Voir le README pour lancer le test E2E, l’exemple a été testé sous Mac OS avec Docker for Mac (l’installation de docker machine n’est plus nécessaire).


Explication de ce qui change par rapport à l’approche sans Dockerfile phase par phase (build, pré-intégration, test, post-intégration)

Build

  •  Build maven et
  • Build images docker des repos mysql, batch et restapp, il faut lancer la commande docker build à la racine de chaque repo.
  • La commande build construit les images en exécutant le Dockerfile présent dans le contexte relatif au repo.
    • docker build -t mysql-sample:latest .
    • docker build -t springbatch-sample:latest .
    • docker build -t spring-boot-sample:latest .

Mysql Dockerfile (voir repo mysql-sample)

FROM mysql:latest

COPY functional-schema.sql /docker-entrypoint-initdb.d/
COPY technical-schema.sql /docker-entrypoint-initdb.d/
COPY healthcheck.sh /healthcheck.sh

EXPOSE 3306

HEALTHCHECK --interval=3s --retries=10 CMD /healthcheck.sh
  • Copie des schémas base de données technique Spring batch et fonctionnelle dans l’image docker à l’emplacement /docker-entrypoint-initdb.d
  • docker-entrypoint-initdb.d est un répertoire standard fourni par l’image officielle mysql (FROM mysql)
  • Les scripts copiés dans /docker-entrypoint-initdb.d sont exécutés au démarrage du container (docker run).
  • Copie du script healthcheck dans l’image
  • Exposition du port standard mysql aux autres containers
  • Déclaration du script healthchek à exécuter au démarrage du container
    • S’exécutera 10 fois maximum à intervalles de 3 secondes jusqu’au succès

Healthcheck mysql

set -e

echo "SELECT 1 FROM USER;" | mysql --user="root" --password="$MYSQL_ROOT_PASSWORD" functional > /dev/null
echo "SELECT 1 FROM BATCH_JOB_SEQ;" | mysql --user="root" --password="$MYSQL_ROOT_PASSWORD" technical > /dev/null

exit $?
  • Sortie du script avec code 0 si les requêtes de test des schémas sont exécutées avec succès.

Batch Dockerfile (voir repo springbatch-sample)

FROM java:8

COPY springbatch-sample/target/springbatch-sample*.jar /springbatch-sample.jar

ENTRYPOINT ["java","-Dbatch.properties.path=file:/properties/batch.properties",
"-Djob.name=alimentationJob","-jar",
"/springbatch-sample.jar","input.file.path=/input/alimentation.csv"]
  • Copie du package au format jar dans l’image docker
  • ENTRYPOINT décrit la commande à exécuter au démarrage du container

Restapp Dockerfile (voir repo spring-boot-sample)

FROM java:8

COPY target/spring-boot-sample*.jar /spring-boot-sample.jar
COPY healthcheck.sh /healthcheck.sh

EXPOSE 8080

HEALTHCHECK --interval=3s --timeout=3s --retries=10 CMD /healthcheck.sh

ENTRYPOINT bash -c "java -jar /spring-boot-sample.jar --spring.config.location=file:/properties/application.properties 2>&1 | tee /var/log/restapp.log"
  • Copie du package au format jar dans l’image docker
  • Copie du healthcheck dans l’image
  • Exposition du port 8080 aux autres containers
  • Déclaration du healthchek à exécuter au démarrage du container
    • curl sur le port 8080
  • ENTRYPOINT décrit la commande à exécuter au démarrage du container

Pré-intégration (voir script docker-run.sh)

  • Lancement container docker mysql-sample
  • Attente du succès du healthcheck mysql
  • Lancement container batch-sample
  • Lancement container spring-boot-sample
  • Attente du succès du healthcheck de la restapp

Script docker-run.sh

work=$1
e2e=$2

cd $work && rm -rf log && mkdir log

docker run --name mysqldb -d -e MYSQL_ROOT_PASSWORD=dba -p 3306:3306 mysql-sample

$e2e/pre-integration/wait/wait.sh mysqldb

docker run --rm  -v $work/configuration/properties:/properties -v $e2e/pre-integration/input:/input springbatch-sample > $work/log/batch.log	

docker run --name webapp -d -v $work/log/:/var/log -v $work/configuration/properties:/properties -p 8080:8080 spring-boot-sample

$e2e/pre-integration/wait/wait.sh webapp

Script wait.sh

while [ $(docker inspect --format='{{.State.Health.Status}}' $1) != "healthy" ];
 do sleep 1; echo -n ".";
done
  • Boucle d’attente sur la commande docker inspect
  • State.Health.Status correspond au path json à filtrer du résultat de la commande inspect
  • State.Health.Status est égal à « healthy » quand le résultat du script déclaré dans HEALTHCHECK est en succès

Test des ressources REST avec CURL

Post-intégration

  • Arrêt et suppression des containers docker
  • Suppressions des images
docker rmi springbatch-sample
docker stop webapp && docker rm webapp && docker rmi spring-boot-sample
docker stop mysqldb && docker rm mysqldb && docker rmi mysql-sample
  • Chaque rebuild d’image avec le même tag latest et la copie d’un jar « snapshot » différent va générer la présence d’une nouvelle image dans l’emplacement local des images (docker images -a)
    • l’exécution de la commande docker rmi s’impose donc sous peine de voir l’espace disque local rapidement saturé !
  • Docker for Mac stocke les images docker dans le fichier ~/Library/Containers/com.docker.docker/Data/com.docker.driver.amd64-linux/Docker.qcow2
  • Voici un lien utile sur l’utilisation de l’outil qmenu-img afin de récupérer de l’espace disque après suppression des images (docker rmi ne suffit pas pour récupérer l’espace !)

Conclusion

  • Nous avons vu un moyen d’exploiter les commandes standard de docker au lieu d’écrire des scripts bash
  • La notion d’image exécutable vs package jar à déployer
  • Cette approche peut être complétée par l’utilisation d’un plugin maven pour docker

 

Tests unitaires View Backbone.js avec Mocha, Chai et Sinon.js

Dans un précédent article nous avons vu comment écrire des tests unitaires JS Model & Collection sur la todo liste backbone.

On va poursuivre ici par les tests des composants Backbone de type View.

Mocha est le framework de test utilisé, complété par Chai pour les assertions et Sinon.JS pour les doublures (spies, stub, mock).

L’environnement d’exécution nécessite seulement Node.js, pas besoin de browser pour lancer les tests unitaires, nous allons voir comment exécuter des assertions sur le dom dans Node.js.

L’exemple complet est disponible sur Github sur la branche webdriverio-it-tests (voir le README pour exécuter les tests unitaires).

Configuration

La configuration grunt (voir article TU model) est responsable d’exécuter le helper global main.js

Mise en place de Chai et Sinon dans le helper main.js

 var chai = require('chai');
 var sinon = require('sinon');
 var sinonChai = require('sinon-chai');
 chai.use(sinonChai);

 global.sinon = sinon;
 global.expect = chai.expect;
...

Mise en place de jsdom dans le helper main.js

 var jsdom = require('jsdom');
 var fs = require('fs');
 var markup = fs.readFileSync('app/index.html');

 global.window = jsdom.jsdom(markup).defaultView;

jsdom est une implémentation javascript du dom qui permet de se passer d’un browser.

Tests unitaires du composant Todo View

describe('TodoView', function() {
var Todo = requirejs('models/todo');
var TodoView = requirejs('views/todoView');
var Common = requirejs('common');
var $ = requirejs('jquery');
var todo, todoView;
beforeEach(function() {
todo = new Todo();
todoView = new TodoView({model:todo});
});
afterEach(function() {
todoView.remove();
});
it('tagName should equal li', function() {
expect(todoView.tagName).to.equal('li');
});
it('render view', function() {
var view = new TodoView({model: new Todo({title: 'todo1', completed: true})});
view.render();
expect(view.$input.val()).to.equal('todo1');
expect(view.$el.hasClass('completed')).to.be.true;
});
describe('listeners', function() {
it('when model title change then render view', function() {
todo.set('title','title updated');
expect(todoView.$input.val()).to.equal('title updated');
});
it('when model destroy then remove view', function() {
var spy = sinon.spy(todoView,'remove');
todoView.initialize();
todo.destroy();
expect(spy).have.been.called;
});
it('when model trigger visible then view has class hidden', function() {
Common.TodoFilter = 'completed';
todo.trigger('visible');
expect(todoView.$el.hasClass('hidden')).to.be.true;
});
});
describe('events', function() {
var createEvent = function(eventName, keyCode) {
var e = $.Event(eventName);
e.which = keyCode;
return e;
};
var setupOnEditModeWithValue = function(val) {
todo.set('title',val);
todoView.$el.find('label').dblclick();
};
describe('when click', function() {
beforeEach(function() {
todoView.render();
});
it('class toggle then model completed', function() {
var spy = sinon.stub(todo,'save');
todoView.$el.find('.toggle').click();
expect(todo.get('completed')).to.be.true;
expect(spy).have.callCount(1);
});
it('class destroy then destroy model', function() {
var spy = sinon.spy(todo,'destroy');
todoView.$el.find('.destroy').click();
expect(spy).have.callCount(1);
});
});
it('when double click label then switch to edit mode', function() {
todo.set('title','todo1');
todoView.$el.find('label').dblclick();
expect(todoView.$el.hasClass('editing')).to.be.true;
expect(todoView.$el.find('.edit:focus')).to.have.length(1);
});
describe('when keypress class edit', function() {
it('on enter then save', function() {
var spy = sinon.stub(todo,'save');
setupOnEditModeWithValue('todo1');
todoView.$el.find('.edit').trigger(createEvent('keypress', Common.ENTER_KEY));
expect(todoView.$el.hasClass('editing')).to.be.false;
expect(spy).have.callCount(1);
});
describe('but not save', function() {
var spy;
beforeEach(function() {
spy = sinon.spy(todo,'save');
});
[{value: '', description: 'empty'},
{value: ' ', description: 'with space'}]
.forEach(function(test) {
it('when enter and title '+ test.description, function() {
setupOnEditModeWithValue(test.value);
todoView.$el.find('.edit').trigger(createEvent('keypress', Common.ENTER_KEY));
expect(todoView.$el.hasClass('editing')).to.be.false;
});
});
it('when not press enter', function() {
setupOnEditModeWithValue('todo1');
todoView.$el.find('.edit').trigger(createEvent('keypress', 65));
expect(todoView.$el.hasClass('editing')).to.be.true;
});
afterEach(function() {
expect(spy).have.callCount(0);
});
});
});
it('when blur class edit then save', function() {
var spy = sinon.stub(todo,'save');
setupOnEditModeWithValue('todo1');
todoView.$input.focusout();
expect(todoView.$el.hasClass('editing')).to.be.false;
expect(spy).have.callCount(1);
});
describe('when keydown class edit', function() {
beforeEach(function() {
setupOnEditModeWithValue('todo1');
todoView.$input.val('todo1 updated');
});
it('on escape key then revert title', function() {
todoView.$el.find('.edit').trigger(createEvent('keydown', Common.ESC_KEY));
expect(todoView.$input.val()).to.equal('todo1');
expect(todoView.$el.hasClass('editing')).to.be.false;
});
it('but no escape key fire no changes', function() {
todoView.$el.find('.edit').trigger(createEvent('keydown', 65));
expect(todoView.$input.val()).to.equal('todo1 updated');
expect(todoView.$el.hasClass('editing')).to.be.true;
});
});
});
describe('is hidden', function() {
var tests = [
{filter: 'completed', completed: false, hidden: true},
{filter: 'completed', completed: true, hidden: false},
{filter: 'pending', completed: false, hidden: false},
{filter: 'pending', completed: true, hidden: true},
{filter: '', completed: false, hidden: false},
{filter: '', completed: true, hidden: false},
];
tests.forEach(function(test) {
it('when filter '+ (test.filter===''?'empty':test.filter)+' and todo '+(test.completed?'':'not ')+'completed then hidden '+test.hidden, function() {
Common.TodoFilter = test.filter;
todo.set('completed',test.completed);
expect(todoView.isHidden()).to.equal(test.hidden);
});
});
});
});
  • Les appels imbriqués de describe permettent d’organiser les tests et améliorer la lisibilité du reporting
  • Le découpage des tests est fait selon les listeners puis les events
  • Les assertions se font sur le dom de l’élément de la view
  • Les stubs Sinon.JS sont utilisés pour substituer une implémentation vide aux méthodes du model afin de ne pas déclencher d’appels à l’api REST
  • Les spy Sinon.JS sont utilisés pour vérifier l’appel des méthodes du model
  • Les tests paramétrés permettent de couvrir tous les cas de la méthode isHidden en évitant la duplication

 

Tests unitaires du composant App View

describe('AppView', function() {
var todos = requirejs('collections/todos');
var AppView = requirejs('views/appView');
var Common = requirejs('common');
var $ = requirejs('jquery');
var appView;
var spyFetch;
before(function() {
spyFetch = sinon.stub(todos,'fetch');
appView = new AppView();
});
beforeEach(function() {
todos.reset();
});
after(function() {
appView.remove();
});
describe('new AppView', function() {
it('el id should equal todoapp', function() {
expect(appView.el.id).to.equal('todoapp');
});
it('when initialize then fetch', function() {
expect(spyFetch).have.been.called;
});
after(function() {
spyFetch.restore();
});
});
describe('listeners', function() {
it('when add collection then render todo in TodoView', function() {
todos.add({title: 'new todo', completed: false});
expect(appView.$el.find('#todo-list li:last-child .edit').val()).to.equal('new todo');
});
it('when reset collection then render todos in TodoViews', function() {
todos.reset([{title: 'todo1'}, {title: 'todo2'}]);
expect(appView.$el.find('#todo-list li')).to.have.length(2);
});
it('when filter collection then render todos in TodoViews with filter', function() {
todos.set([{title: 'todo1', completed: true}, {title: 'todo2', completed: false}]);
Common.TodoFilter = 'completed';
todos.trigger('filter');
expect(appView.$el.find('#todo-list li.hidden')).to.have.length(1);
expect(appView.$el.find('#todo-list li:not(.hidden) .edit').val()).to.equal('todo1');
})
});
describe('events', function() {
describe('when keypress id new-todo ', function() {
var spyCreate;
var createEvent = function(eventName, keyCode) {
var e = $.Event(eventName);
e.which = keyCode;
return e;
};
before(function() {
spyCreate = sinon.stub(todos, 'create');
});
beforeEach(function() {
appView.$input.val('todo1');
});
afterEach(function() {
spyCreate.reset();
});
it('on enter key then todos create', function() {
appView.$el.find('#new-todo').trigger(createEvent('keypress', Common.ENTER_KEY));
expect(appView.$input.val()).to.be.empty;
expect(spyCreate).to.have.been.calledWith({title: 'todo1', completed: false});
});
it('but no enter key fire no todos create call', function() {
appView.$el.find('#new-todo').trigger(createEvent('keypress', 65));
expect(appView.$input.val()).to.be.equal('todo1');
expect(spyCreate).have.callCount(0);
});
});
it('when click id toggle-all then render all todos as completed', function() {
todos.set([{title: 'todo1'}, {title: 'todo2'}]);
todos.each(function (todo) {
sinon.stub(todo,'save', function() {
todo.set('completed', true);
});
});
appView.$el.find('#toggle-all').click();
expect(appView.$el.find('#todo-list li.completed')).to.have.length(2);
});
});
});
  • Une seule instance de AppView pour tous les tests
  • La collection Todos est ré initialisée à chaque test
  • Les stubs Sinon.JS sont utilisés pour substituer une implémentation du model qui met seulement à jour le model sans déclencher d’appels à l’api REST
  • Les spy Sinon.JS sont utilisés pour vérifier l’appel des méthodes de la collection avec les paramètres attendus

 

Les TU Views + TU Model & Collection permettent donc de couvrir toutes les lignes / branches du code de l’application todo liste en fournissant un effort minimum pour chaque méthode de chaque composant.

L’ajout de tests Web d’intégration en plus des TU permet de construire une stratégie de test complète et une répartition de l’effort de test équilibrée.

 

 

Tests unitaires Model & Collection Backbone.js avec Mocha et Chai

Cet article montre comment écrire des tests unitaires JS en prenant comme application exemple la todo liste backbone codée avec le framework Backbone.js et le chargeur de modules RequireJS.

On se focalise ici sur les tests des composants Backbone de type Model et Collection.

Mocha est le framework de test que l’on utilise, complété par Chai pour les assertions.

L’environnement d’exécution nécessite seulement Node.js, il n’y a donc pas besoin de browser pour lancer les tests unitaires.

L’exemple complet est disponible sur Github sur la branche webdriverio-it-tests (voir le README pour exécuter les tests unitaires).

Configuration

La configuration des tests est faite par grunt et la task grunt-mocha-test pour exécuter les tests mocha dans Node.js, lance notamment le helper global main.js.

Extrait du Gruntfile.js

...
  mochaTest: {
    test: {
        options: {
            reporter: 'spec',
            timeout: 1000,
            require: 'test/spec/helpers/main.js'
        },
        src: ['test/spec/*.js']
    }
  }
...

Mise en place de Chai et des dépendances de modules dans le helper main.js

 var chai = require('chai');
 global.expect = chai.expect;
...

Tests unitaires du composant Model Todo

describe('Todo', function() {
var Todo = requirejs('models/todo');
var todo;
beforeEach(function() {
todo = new Todo();
});
it('defaults are empty title and not completed', function() {
expect(todo.get('title')).to.be.a('string').and.to.be.empty;
expect(todo.get('completed')).to.be.false;
});
it('when toggle then completed', function() {
todo.toggle();
expect(todo.get('completed')).to.be.true;
});
});
  • Les méthodes Mocha describe et it décrivent le comportement attendu du composant Todo comme s’il s’agissait d’une spécification
  • La méthode beforeEach de Mocha fournit une nouvelle instance de Todo à chaque test
  • Les assertions sont exécutées avec Chai dans le style expect

Tests unitaires du composant Collection Todos

describe('Todos', function() {
var Todo = requirejs('models/todo');
var todos = requirejs('collections/todos');
beforeEach(function() {
todos.reset();
});
it('filtering completed', function() {
todos.set([{title: 'todo1', completed: true}, {title: 'todo2', completed: false}, {completed: true}]);
var todosCompleted = todos.completed();
expect(todosCompleted).to.have.length(2);
expect(todosCompleted[0].get('completed')).to.be.true;
expect(todosCompleted[1].get('completed')).to.be.true;
});
it('filtering remaining', function() {
var todo1 = new Todo();
todo1.toggle();
var todo2 = new Todo();
todos.set([todo1, todo2]);
var todosRemaining = todos.remaining();
expect(todosRemaining).to.have.length(1);
expect(todosRemaining[0]).to.eql(todo2);
});
});
  • Une seule instance de la collection est ré initialisée à chaque test par la méthode beforeEach
  • Chaque méthode de test contient des sauts de ligne pour séparer
    • le setup particulier au test
    • l’action testée
    •  et les assertions

 

Et voila, le modèle de l’application est couvert par les tests indépendamment du code des Backbone Views.

Nous allons justement voir dans cet article comment implémenter les tests unitaires des Views.

 

 

Tests Web en JS partie 3/3 – Pattern Page Object avec Selenium et BDD avec Cucumber-js

Suite à l’article 2/3 je vais montrer comment faire des tests web End to End toujours avec le formalisme BDD à l’aide de Cucumber-js et l’implémentation du composant Page Object avec WebDriverJs.

WebdriverIO est aussi présenté en tant qu’alternative à WebDriverJs.

On obtient donc la stack de test :

  • WebDriverJs + Cucumber-js sur Node.js + protocole WebDriver pour communiquer avec le browser
  • ou bien WebdriverIO + Cucumber-js sur Node.js + protocole WebDriver

L’exemple complet en Cucumber-js + WebDriverJs est disponible sur  Github sur la branche seleniumjs-it-tests (voir README pour l’exécution des tests).

Composant Page Object avec WebDriverJs

[gist https://gist.github.com/95746c4f8858ceb8e49c /]
  • Contient tous les appels à l’API WebDriverJs
  • Encapsule les sélecteurs CSS
  • Expose les fonctions pour lancer les actions sur le SUT (System Under Test)
  • Expose les fonctions pour récupérer les informations sur l’état du SUT
  • les appels à WebDriverJs retournent des Promises

Ecriture des TI avec Mocha / Chai, exemple de l’ajout des todos

[gist https://gist.github.com/46c4fd89d52fe9d9241c /]
  •  Description de la suite avec la syntaxe Mocha
  • Lance les actions par l’intermédiaire du Page Object
  • Effectue les vérifications avec les assertions Chai
  • L’extension Chai as Promised permet de vérifier les Promises de WebDriverJs
  • Q.all permet d’attendre la résolution de plusieurs Promises

Implémentation des step Cucumber-js pour faire la liaison avec le BDD

[gist https://gist.github.com/7c9f9a43d1b23192763e /]
  •  Instance Cucumber this fournie les fonctions Given, When, Then
  • Given génère des todos en tant que pré-requis de certains scénarios
  • When lance les actions par l’intermédiaire du Page Object
  • Then effectue les vérifications avec les assertions Chai et l’extension Chai as Promised pour traiter les Promises issues de WebDriverJs

Limitations de WebDriverJs

  • Beaucoup plus lent que CasperJS (sachant Zombie.js est encore plus rapide que CasperJS).

Cas d’usage possible

  • Tests End to End multi-browser (y compris en mode headless avec PhantomJS)
  • API très riche pour reproduire ce que fait l’utilisateur final
  • Attention à l’abus de selenium ! Voir à ce sujet l’article sur la pyramide des tests.

 

Alternative WebdriverIO pour l’implémentation du Page Object

L’exemple complet en Cucumber-js + WebdriverIO est disponible sur Github sur la branche webdriverio-it-tests (voir README pour l’exécution des tests).

Composant Page Object avec WebdriverIO

[gist https://gist.github.com/2e5a6276c7c26cc3c5ec /]

Par rapport à WebDriverJs l’avantage est que l’API est plus concise et lisible.

En conclusion

Un dernier mot sur l’activité des frameworks de tests d’intégration et fonctionnels :

  • CasperJS n’apporte plus de nouveautés depuis un certain temps, mais la richesse de son API et sa documentation pourraient encourager d’autres contributeurs ! A suivre…
  • Concernant Selenium on est en présence d’un écosystème très vivant avec de multiples portages et notamment plusieurs implémentations en JS.
  • Cucumber est également très vivant et populaire et bénéficie de portage en plusieurs langages.

 

 

 

Tests Web en JS partie 2/3 – Pattern Page Object avec Zombie.js et BDD avec Cucumber-js

Suite à l’article 1/3 je vais montrer comment faire des tests web fonctionnels avec le formalisme BDD à l’aide de Cucumber-js et l’implémentation du composant Page Object avec Zombie.js.

A la différence de CasperJS qui s’appuie sur PhantomJS pour l’exécution des tests, Zombie.js s’exécute dans Node.js.

On obtient donc la stack de test :

  • Zombie.js + Cucumber-js sur Node.js
  • Versus CasperJS sur PhantomJS dans l’article précédent

L’exemple complet en CucumberJs + ZombieJs est disponible sur Github sur la branche zombiejs-it-tests (voir README pour l’exécution des tests).

Composant Page Object avec Zombie.js

function TodoPage() {
'use strict';
var ENTER_KEY = 13;
var idNew = '#new-todo';
var title = 'title';
var titleList = '#header > h1';
var children = '#todo-list li';
var childrenVisible = '#todo-list li:not(.hidden)';
var nthChild = function(n) {
return children+':nth-child('+n+')';
};
var nthChildAndSelector = function(selector,n) {
return nthChild(n) + selector;
};
var enterKey = function(selector) {
browser.evaluate("var e = $.Event('keypress');e.which = "+ENTER_KEY+";$('"+selector+"').trigger(e);");
};
var nthLabel = _.partial(nthChildAndSelector,' label');
var nthInput = _.partial(nthChildAndSelector,' input.edit');
var nthDestroyBtn = _.partial(nthChildAndSelector,' button.destroy');
var nthCheckbox = _.partial(nthChildAndSelector,' input.toggle');
var nthCompleted = _.partial(nthChildAndSelector,'.completed');
var nthHidden = _.partial(nthChildAndSelector,'.hidden');
var nthNotHidden = _.partial(nthChildAndSelector,':not(.hidden)');
var browser = new Browser();
this.before = function(done){
browser.visit(config.url,done);
browser.evaluate("window.localStorage.clear();");
};
this.title = function() {
return browser.text(title);
};
this.titleList = function() {
return browser.text(titleList);
};
this.typeNew = function(newTodo) {
browser.fill(idNew, newTodo);
return this;
};
this.enterNew = function() {
enterKey(idNew);
};
this.nbVisible = function() {
return browser.querySelectorAll(childrenVisible).length;
};
this.nthText = function(nth) {
return browser.text(nthLabel(nth));
};
this.mouseOverNth = function(nth) {
browser.fire(nthChild(nth),'mouseover');
return this;
};
this.deleteNth = function(nth) {
browser.click(nthDestroyBtn(nth));
};
this.done = function(nth) {
browser.click(nthCheckbox(nth));
};
this.nthCompleted = function(nth) {
return _.isElement(browser.querySelector(nthCompleted(nth)));
};
this.doubleClickNth = function(nth) {
browser.fire(nthLabel(nth),'dblclick');
};
this.editNth = function(nth,todo) {
browser.fill(nthInput(nth),browser.text(nthLabel(nth))+todo);
return this;
};
this.enterNth = function(nth) {
enterKey(nthInput(nth));
};
this.first = _.partial(this.nthText,1);
this.mouseOverFirst = _.partial(this.mouseOverNth,1);
this.deleteFirst = _.partial(this.deleteNth,1);
this.doubleClickFirst = _.partial(this.doubleClickNth,1);
this.editFirst = _.partial(this.editNth,1);
this.enterFirst = _.partial(this.enterNth,1);
}
module.exports = new TodoPage();
  • Contient tous les appels à l’API Zombie.js
  • Encapsule les sélecteurs CSS
  • Expose les fonctions pour lancer les actions sur le SUT (System Under Test)
  • Expose les fonctions pour récupérer les informations sur l’état du SUT

Ecriture des TI avec Mocha / Chai, exemple de l’ajout des todos

describe('Todo add scenario', function() {
before(function(done) {
todo.before(done);
});
it('titre de la page', function() {
expect(todo.title()).to.equal('TodoListWithBB');
});
it('titre de la todo list', function() {
expect(todo.titleList()).to.equal('todos');
});
it('ajout premier dans la todo list',function() {
todo.typeNew('first todo').enterNew();
expect(todo.first()).to.equal('first todo');
});
it('ajout deuxieme todo dans la todo list',function() {
todo.typeNew('second todo').enterNew();
expect(todo.nbVisible()).to.equal(2);
expect(todo.nthText(2)).to.equal('second todo');
});
});
  • Description de la suite avec la syntaxe Mocha
  • Lance les actions par l’intermédiaire du Page Object
  • Effectue les vérifications avec les assertions Chai

Scénario de l’ajout en BDD

Feature: todo add feature
Scenario: add todos in todo list
When je saisis le todo "first todo"
And je saisis le todo "second todo"
Then le todo "first todo" est placé en position 1 dans la liste
And le todo "second todo" est placé en position 2 dans la liste
view raw todoAdd.feature hosted with ❤ by GitHub

Implémentation des step Cucumber-js pour faire la liaison avec le BDD

module.exports = function() {
this.Before(function (callback) {
todo.before(callback);
});
this.Given(/(\d+) todos dans la liste/, function (nbTodos, callback) {
for(var i=1;i<=parseInt(nbTodos);i++) {
todo.typeNew("todo "+i).enterNew();
}
callback();
});
this.When(/je saisis le todo "(.*)"/, function (aTodo, callback) {
todo.typeNew(aTodo).enterNew();
callback();
});
this.Then(/le todo "(.*)" est placé en position (\d+) dans la liste/, function (aTodo, position, callback) {
expect(todo.nthText(position)).to.equal(aTodo);
callback();
});
this.When(/je supprime le todo placé en position (\d+) dans la liste/, function (position, callback) {
todo.mouseOverNth(position).deleteNth(position);
callback();
});
this.Then(/la liste contient (\d+) todo\(s\)/, function (nbTodos, callback) {
expect(todo.nbVisible()).to.equal(parseInt(nbTodos));
callback();
});
this.When(/je coche le "(premier|deuxième|troisième)" todo/, function (nth,callback) {
todo.done(nthToPosition(nth));
callback();
});
this.When(/je décoche le "(premier|deuxième|troisième)" todo/, function (nth,callback) {
todo.undo(nthToPosition(nth));
callback();
});
this.When(/je coche tous les todos/, function (callback) {
todo.doneAll();
callback();
});
this.When(/je décoche tous les todos/, function (callback) {
todo.undoAll();
callback();
});
this.Then(/le "(premier|deuxième|troisième)" todo est fait/, function (nth,callback) {
expect(todo.nthCompleted(nthToPosition(nth))).to.be.true;
callback();
});
this.Then(/le "(premier|deuxième|troisième)" todo est à faire/, function (nth,callback) {
expect(todo.nthCompleted(nthToPosition(nth))).to.be.false;
callback();
});
this.When(/j'édite le "(premier|deuxième|troisième)" todo avec la valeur "(.*)"/, function (nth,valeur,callback) {
var position = nthToPosition(nth);
todo.doubleClickNth(position);
todo.editNth(position,valeur).enterNth(position);
callback();
});
function nthToPosition(nth) {
return ['premier','deuxième','troisième'].indexOf(nth)+1
}
};
  • Instance Cucumber this fournie les fonctions Given, When, Then
  • Given génère des todos en tant que pré-requis de certains scénarios
  • When lance les actions par l’intermédiaire du Page Object
  • Then effectue les vérifications avec les assertions Chai

Limitations de Zombie.js

  • Ne s’exécute pas dans un browser standard
  • Documentation succinte
  • Recours à l’exécution d’expressions JS pour certaines actions
  • Click sur les filtres des todos gérés par le routeur Backbone.js non pris en compte
  • Pas de screenshot pour le debug

Cas d’usage possible

  • Tests fonctionnels BDD headless vraiment « Insanely fast » (comme le dit le slogan de Zombie.js)
  •   Mais pas des tests vraiment End To End car n’utilise pas un vrai browser et risque de ne pas toujours reproduire fidèlement le comportement du end user
  • Remarque : l’écriture des TI en plus du BDD est présentée ici à titre de comparaison de l’article 1 mais n’est pas toujours indispensable dans un projet réel

Pour réaliser des tests BDD vraiment orientés End to End avec les browsers utilisés par les utilisateurs finaux on va utiliser la techno Selenium dans le dernier article de la série.

Tests Web en JS partie 1/3 – Pattern Page Object avec CasperJS

Voici une série de 3 articles sur les tests d’intégration web en Javascript.

La popularité de la todo liste backbone comme exemple de code en fait un candidat idéal pour présenter les technos de test JS.

Un point commun de tous les exemples présentés est l’utilisation du pattern Page Object pour bien séparer l’écriture de la logique de test de la technique d’accès au système testé (SUT).

Les frameworks utilisés pour chaque articles :

L’exemple complet en CasperJS est disponible sur Github sur la branche master (voir le README pour exécuter les tests).

Composant Page Object avec CasperJS

function TodoPage() {
'use strict';
var idNew = '#new-todo';
var title = '#header > h1';
var children = '#todo-list li';
var nthChild = function(n) {
return children+':nth-child('+n+')';
};
var nthChildAndSelector = function(selector,n) {
return nthChild(n) + selector;
};
var nthLabel = _.partial(nthChildAndSelector,' label');
var nthInput = _.partial(nthChildAndSelector,' input.edit');
var nthDestroyBtn = _.partial(nthChildAndSelector,' button.destroy');
var nthCheckbox = _.partial(nthChildAndSelector,' input.toggle');
var nthHidden = _.partial(nthChildAndSelector,'.hidden');
var nthNotHidden = _.partial(nthChildAndSelector,':not(.hidden)');
this.before = function() {
casper.start(casper.cli.get("urlstart"), function() {
return casper.evaluate(function() {
window.localStorage.clear();
});
});
};
this.test = function(fn) {
casper.then(fn);
}
this.titleList = function() {
return casper.evaluate(function(selector) {
return __utils__.findOne(selector).textContent;
},title);
};
this.typeNew = function(newTodo) {
casper.sendKeys(idNew, newTodo);
return this;
};
this.enterNew = function() {
casper.sendKeys(idNew, casper.page.event.key.Enter);
};
this.nbVisible = function() {
return casper.evaluate(function(selector) {
return _.chain(_.toArray(__utils__.findAll(selector)))
.filter(function(element){return window.getComputedStyle(element).display !== 'none';})
.reduce(function(count) { return ++count;},0)
.value();
},children);
};
this.nthText = function(nth) {
return casper.evaluate(function(selector) {
return __utils__.findOne(selector).textContent;
},nthLabel(nth));
};
this.mouseOverNth = function(nth) {
mouse.move(nthChild(nth));
return this;
};
this.deleteNth = function(nth) {
casper.click(nthDestroyBtn(nth));
};
this.done = function(nth) {
casper.click(nthCheckbox(nth));
};
this.nthCompleted = function(nth) {
return casper.evaluate(function(selector) {
return __utils__.findOne(selector).checked;
},nthCheckbox(nth));
};
this.doubleClickNth = function(nth) {
mouse.doubleclick(nthLabel(nth));
};
this.editNth = function(nth,todo) {
casper.sendKeys(nthInput(nth),todo);
return this;
};
this.enterNth = function(nth) {
casper.sendKeys(nthInput(nth), casper.page.event.key.Enter);
};
this.first = _.partial(this.nthText,1);
this.mouseOverFirst = _.partial(this.mouseOverNth,1);
this.deleteFirst = _.partial(this.deleteNth,1);
this.doubleClickFirst = _.partial(this.doubleClickNth,1);
this.editFirst = _.partial(this.editNth,1);
this.enterFirst = _.partial(this.enterNth,1);
}
module.exports = new TodoPage();
  • Contient tous les appels à l’API Casper
  • Encapsule les sélecteurs CSS
  • Expose les fonctions pour lancer les actions sur le SUT
  • Expose les fonctions pour récupérer les informations sur l’état du SUT

Ecriture des TI avec Mocha / Chai, exemple de l’ajout des todos

describe('Todo add scenario', function() {
before(function () {
todo.before();
});
it('titre de la page', function() {
todo.test(function() {
expect("TodoListWithBB").to.matchTitle;
});
});
it('titre de la todo list', function() {
todo.test(function() {
expect(todo.titleList()).to.equal('todos');
});
});
it('ajout premier dans la todo list',function() {
todo.test(function() {
todo.typeNew('first todo').enterNew();
expect(todo.first()).to.equal('first todo');
});
});
it('ajout deuxieme todo dans la todo list',function() {
todo.test(function() {
todo.typeNew('second todo').enterNew();
expect(todo.nbVisible()).to.equal(2);
expect(todo.nthText(2)).to.equal('second todo');
});
});
});
  • Description de la suite avec la syntaxe Mocha
  • Lance les actions par l’intermédiaire du Page Object
  • Effectue les vérifications avec les assertions Chai
  • L’adhérence avec CasperJS en terme de gestion des étapes est masquée par la fonction test du Page Object

Limitations de CasperJS

  • Conçu au dessus de PhantomJS ne s’intègre donc pas par défaut avec le framework BDD Cucumber-js qui s’exécute dans Node.js
  • Les browsers les plus répandus (Chrome, Firefox,…) ne sont pas supportés

Cas d’usage possible

  • Tests d’intégrations frontend headless avec stub service

Dans l’article 2/3 on va utiliser le framework Zombie.js pour mettre en place simplement des tests BDD avec Cucumber-js.

Tests End-to-End avec Docker

Ce post montre avec un exemple concret comment utiliser Docker pour faire un test e2e qui déroule le scénario :

  • Etant donnée une base de données Mysql
  • Un batch alimente la base de données avec un fichier csv
  • Une application REST est déployée telle que son stockage s’appuie la base de données
  • On vérifie que l’api REST permet de consulter les données persistées et d’en ajouter

Pré – requis pour exécuter l’exemple

  • JDK 8 et Maven
  • Sur un hôte non Linux, un moyen rapide pour lancer des container docker en partant de rien est d’installer docker machine et le client docker (l’exemple a été testé sous Mac OS).
  • Démarrer la VM host des container docker et configurer les variables d’environnement pour le client docker (voir doc d’installation docker machine)

Cloner les repos Github du test e2e, sources et configuration

Pour exécuter le test e2e complet, lancer la commande : cd e2etest && source env.sh && ./run.sh


Explication de l’exécution du test phase par phase (build, pré-intégration, test, post-intégration)

Build Maven du batch springbatch-sample et de la restapp spring-boot-sample à partir des sources

  •  mvn clean package

Pré-intégration (voir script docker-run.sh)

  • lancement container docker mysql « mysqldb »
  • exécution dans container « mysqldb » d’un ping mysqladmin à intervalle d’une seconde pour attendre que mysql soit prêt à recevoir des connexions
  • exécution création schémas base de données technique Spring batch et fonctionnelle dans le container « mysqldb »
  • lancement du batch java Spring batch dans le container java8 « java »
  • lancement container java8 « webapp » et démarrage du jar restapp exécutable
  • exécution dans container « webapp » d’une vérification à intervalle d’une seconde que tomcat est disponible sur port 8080

docker-run.sh et explications commandes docker :

docker run --name mysqldb -d -v $work/configuration/:/configuration -v $e2e/pre-integration/:/pre-integration \
 -e MYSQL_USER=mysql -e MYSQL_PASSWORD=mysql -e MYSQL_ROOT_PASSWORD=dba -p 3306:3306 mysql:5.7.7
docker exec mysqldb bash /pre-integration/wait/mysql.sh

docker exec mysqldb bash -c "mysql -u root -pdba < configuration/sql/functional-schema.sql"
docker exec mysqldb bash -c "mysql -u root -pdba < configuration/sql/technical-schema.sql"

batch_name=springbatch-sample
job_name=alimentationJob
job_param=input.file.path=/pre-integration/input/alimentation.csv
docker run --rm -v $work/configuration/:/configuration -v $work/dist/:/dist -v $e2e/pre-integration/:/pre-integration \
 java:8 java -Dbatch.properties.path=file:/configuration/properties/batch.properties -Djob.name=$job_name \
 -jar /dist/$batch_name.jar $job_param \
 > $work/log/batch.log

restapp_name=spring-boot-sample
redirect_log="2>&1 | tee /var/log/restapp.log"
run_restapp="java -jar /dist/$restapp_name.jar --spring.config.location=file:/configuration/properties/application.properties $redirect_log"
docker run --name webapp -d -v $work/configuration/:/configuration -v $work/dist/:/dist -v $work/log/:/var/log \
 -v $e2e/pre-integration/:/pre-integration --expose 8080 -p 8080:8080 \
 java:8 bash -c "$run_restapp"
docker exec webapp bash /pre-integration/wait/tomcat.sh
  • docker run : commande pour lancer les containers « mysqldb », « java » et « webapp »
  • les containers « mysqldb » et « webapp » s’exécutent en fond (mode detached -d)
  • suppression du container « java » après après la fin de l’exécution du batch avec la clause –rm
  • les ressource présentes sur l’hôte sont partagées avec la clause -v (jar, fichiers de conf,..)
  • container « webapp » expose et publie le port 8080 vers le host –expose et -p
  • docker exec lance des scripts bash à l’intérieur du container pour attendre que mysql et tomcat soient up (voir scripts complémentaires d’attente ci-après)

Scripts d’attente

  • démarrage mysql
while [ $(mysqladmin -u root -pdba ping | grep 'mysqld is alive' | wc -l) -ne 1 ] 
do
 sleep 1
 echo -n "."
done
  • démarrage tomcat
while [ $(cat /var/log/restapp.log | grep 'Tomcat started on port' | wc -l) -ne 1 ] 
do
 sleep 1
 echo -n "."
done

Test des ressources REST avec CURL

curl -s http://$host:8080/personnes/search?nom=nom1 2>&1 | tee $work/log/test.log
echo -e "\nSHOULD FOUND 1 personne"
curl -H "Content-Type: application/json" -X POST -d '{"nom":"nom3","prenom":"prenom3"}' http://$host:8080/personnes
curl -H "Content-Type: application/json" -X POST -d '{"nom":"nom3","prenom":"prenom4"}' http://$host:8080/personnes
curl -s http://$host:8080/personnes/search?nom=nom3 2>&1 | tee -a $work/log/test.log
echo -e "\nSHOULD FOUND 2 personnes"
  • une ressource personne telle que nom=nom1 existe déjà suite au lancement du batch
  • 2 ressources personne telles que nom=nom3 sont ajoutées avec succès
  • les 2 ressources personne telles nom=nom3 sont bien récupérées

Post-intégration

  • arrêt et suppression des containers docker
docker stop webbap
docker rm webapp
docker stop mysqldb
docker rm mysqldb

Pour aller plus loin dans l’industrialisation

  • utiliser un repository d’artefacts pour récupérer les jars
  • mapper les phases du test e2e dans les phases standard d’un build Maven
  • le test curl peut être remplacé par un client de test REST basé sur JUnit dans la phase Maven intégration-test