14 KiB
Qualification et intégration continue
William Petit - S.C.O.P. Cadoles
Qu'est ce que la qualification ?
Qu'est ce que l'intégration continue ?
Les tests applicatifs
Les tests applicatifs ont pour objectif de valider le bon fonctionnement de l'application d'un point de vue métier, c'est à dire de vérifier que celle ci répond aux attentes et aux contraintes du client d'un point de vue fonctionnel.
Ils permettent également et surtout aux développeurs de détecter les régressions dans le cycle de développement.
La pyramide des tests applicatifs
Tests unitaires
Objectifs
Tester le bon fonctionnement des plus petites unités logiques/fonctionnelles de l'application: les fonctions/méthodes d'objets.
Exemple Vérifier qu'une méthode de calcul du prix taxé d'un produit retourne la valeur attendue en fonction des paramètres passés.
Cycle de vie
- Définir le comportement attendu de la classe/fonction à implémenter.
- Créer le cas de test validant ce comportement.
- Exécuter le test. Il ne devrait pas être passant, l'implémentation étant incomplète à ce stade.
- Implémenter la fonction/classe pour que le test soit passant.
Caractéristiques clés
- Ils sont nombreux.
- Ils sont mis en place dès le début de l'implémentation, voir avant (voir "Test Driven Development").
- Ils sont rapides à exécuter.
- Ils permettent de détecter rapidement les régressions.
- Ils permettent de rassurer le développeur qui doit apporter des évolutions à une base de code existante.
- Ils s'exécutent en continu sur le poste du développeur et à chaque changement sur le serveur de gestion des sources.
- Ils ne devraient pas nécessiter de dépendances externes.
Exemple: Tests unitaires en Javascript (Mocha)
// Fichier test/my-test-suite.js
const assert = require('assert');
// Déclaration de la suite de tests
describe('My suite', function() {
// Test synchrone
it('should not fail', function() {
assert.equal(true, 1); // true == 1 en javascript
});
// Test asynchrone
it('should do something async', function() {
return doSomethingAsync()
.then(result => {
assert.ok(result); // On vérifie que le résultat n'est pas null
})
.catch(err => {
assert.ifError(err); // On vérifie qu'aucune erreur n'est remontée
})
;
});
});
Et le style dans tout ça ?
- Javascript: https://eslint.org/
- PHP: https://github.com/phpro/grumphp
Exercice: Tests unitaires d'une fonction de validation d'un numéro de carte bancaire en Javascript
Soit un numéro de carte bancaire donné, implémenter un test pour la fonction validant le code de celle ci PUIS implémenter la fonction qui permet de valider ce code.
Ressources
Tests d'intégration
Objectifs
Tester la bonne communication et interaction des différents "modules" composant une application.
Exemple Vérifier que qu'une transaction est correctement enregistrée dans la base de données lorsque le module de paiement est utilisé.
Caractéristiques clés
- Ils sont moins nombreux que les tests unitaires.
- Ils peuvent prendre un peu de temps à s'exécuter.
- Ils sont mis en place au cours de l'implémentation initiale et maintenus au fur et à mesure de l'évolution de l'application.
- Ils peuvent nécessiter des dépendances externes, mais généralement pas l'ensemble de l'infrastructure.
- Ils peuvent nécessiter des données d'amorçage.
- Ils impliquent souvent la mise en place de composants "factices" pour simuler une partie de l'application.
- Ils s'exécutent à la demande sur le poste du développeur et sur le serveur d'intégration continue à intervalles réguliers.
Cycle de vie
Les tests d'intégration devraient être abordés (en général) en seconde moitiée d'itération, au moment de la finalisation des modules applicatifs prêts à être livrés.
Ils devraient cibler en premier les modules critiques de l'application, notamment ceux ayant pour rôle de contrôler/valider le cycle de vie des données métiers.
Les tests d'intégration peuvent être un bon témoin/validateur de l'état de réussite d'une itération.
Exemple: Test d'une API REST avec Symfony3
- Implémenter une API JSON/REST basique avec Symfony3 permettant d'enregistrer/lister dans une base de données (sqlite3) une "annonce" avec les propriétés suivantes:
- Titre
- Description
- Type
- Prix
- Implémenter avec la librairie standard de Symfony3 les tests suivants:
- Ajout d'une nouvelle annonce et vérification du bon enregistrement de la nouvelle annonce via un appel sur l'URL d'affichage des annonces
Ressources
Tests fonctionnels
Objectif
Tester le bon fonctionnement de l'application d'un point de vue utilisateur.
Caractéristiques clés
- Ils devraient être les moins nombreux de l'ensemble des tests applicatifs.
- Ils devraient être représentatifs des cinématiques d'action des utilisateurs.
- Ils devraient s'exécuter sur un environnement identique à la production.
- Ils sont souvent complexes à mettre en place/maintenir.
- Ils ont une couverture fonctionnelle très large mais ne permettent pas d'identifier directement les sources de dysfonctionnement.
- Ils devraient couvrir les procédures faisant intervenir les dépendances externes de l'application (serveur de courriel, API externes, base de données...).
Cycle de vie
Les tests fonctionnels devraient être implémentés une fois que l'itération a atteint une certaine stabilité en terme d'interface utilisateur.
Ils ne devraient être modifiés que lorsque le client demande une évolution de l'interface utilisateur et/ou des cinématiques d'action.
Exemple: Tests fonctionnels Web avec NightmareJS
Voir l'exemple tests-fonctionnels
Exercice: Simuler un utilisateur avec le client web KiwiIRC
Via NightmareJS et avec le client KiwiIRC, se connecter au serveur irc.freenode.net
, rejoindre le canal #cadoles
en tapant la commande /join <canal>
puis envoyer un message sur le canal nouvellement rejoint.
Les tests "techniques"
Les tests techniques ont pour objectif de valider le bon fonctionnement de l'infrastructure exécutant l'application, c'est à dire que l'application et son infrastructure seront capables de répondre au contexte d'usage en production.
Tests de charge
Objectif
Les tests de charge doivent permettre de valider le niveau de stabilité de l'infrastructure et de l'application par rapport à la volumétrie d'utilisateurs en production et son contexte technique d'utilisation.
Pour ce faire, la mise en place d'une stratégie concrète de métrologie est obligatoire.
Caractéristiques clés
- Ils nécessitent la mise en place d'une infrastructure identique (ou au plus proche) à la production.
- Les données produites sont souvent peu fiables car les tests de charge véritablement représentatifs d'un comportement d'un utilisateur sont complexes à mettre en place.
- Ils doivent couvrir toutes les briques techniques de l'infrastructure et de l'application: disques, réseau, mémoire vive, CPU, serveur HTTPS, serveur applicatif, base de données...
Cycle de vie
Les tests de charge devraient être effectués avant tout primo déploiement d'une application et réactualisés lorsque le contexte d'usage de celle ci change (exemple: bascule d'un usage local à un usage national).
Les tests de charge devraient être exécutés régulièrement afin de vérifier qu'aucun "goulot d'étranglement" n'a été introduit dans l'itération en cours.
Exemple: Utilisation de Siege pour vérifier les temps de réponse d'une application Web
https://www.joedog.org/siege-home/
Tests de sécurité
Objectif
Valider la conformité de l'infrastructure et de l'application d'un point de vue sécurité. Découvrir des vulnérabilités dans la conception de l'application.
Caractéristiques clés
- Ils sont complexes à mettre en place
- Ils doivent fonctionner sur un environnement identique à la production.
- Ils ne peuvent remplacer un audit de sécurité "manuel".
- Ils sont souvent peu efficaces pour détecter les failles qui nécessites un premier niveau d'accréditation.
Cycle de vie
Ils doivent être mis en place au plus tôt dans le cycle de développement.
Suivant le type de cible, ils peuvent être exécutés de différentes manières: à chaque fin d'itération, de manière règulière, etc...
Ils doivent être réactualisés lors de tout changement d'infrastructure et de socle technologique de l'application.
Exemple: Vérification de sécurité pour les dépendances PHP
https://github.com/sensiolabs/security-checker
Exemple: Audit automatisé sur les applications Web avec w3af
https://github.com/andresriancho/w3af/
Le cycle (simplifié) d'intégration continue
Automatisation, virtualisation et conteneurisation
Principes généraux sur les conteneurs
- Les conteneurs sont un mécanisme de virtualisation basée sur l'isolation des processus au niveau du système d'exploitation.
- Ils bénéficient d'une très faible perte de performances brutes par rapport à de la virtualisation classique utilisation des mécanismes d'émulation.
Présentation de Docker
Manipuler des images de conteneur
Exécuter un conteneur
Gérer les conteneurs
Partager des répertoires
Configurer le réseau d'un conteneur
Construire une image
Publier une image sur le Hub
Utiliser docker-compose
Le fichier docker-compose.yml
version: '2' # Version du fichier docker-compose
services: # Déclaration des services
faketools: # Service 'faketools'
build: # Création de l'image du conteneur depuis un dossier du projet
context: .
dockerfile: containers/faketools/Dockerfile
args: # Déclaration d'arguments de construction de l'image
HTTP_PROXY: ${HTTP_PROXY}
HTTPS_PROXY: ${HTTPS_PROXY}
http_proxy: ${http_proxy}
https_proxy: ${https_proxy}
ports: # Déclaration des ports à exposer sur la machine hôte
- "8080:8080"
- "8081:8081"
- "2525:2525"
- "3389:3389"
- "8443:8443"
- "2222:22"
volumes: # Déclaration des montages de volumes
- ./data:/faketools/data
environment: # Déclaration de variables d'environnement
HTTP_PROXY: ${HTTP_PROXY}
HTTPS_PROXY: ${HTTPS_PROXY}
http_proxy: ${http_proxy}
https_proxy: ${https_proxy}
Exercice
Créer un environnement de développement docker-compose
à partir de l'image ubuntu:xenial
pour une application Symfony3 et une base de données MySQL avec l'image mysql:latest
.
Le répertoire des sources locales devra être partagé avec le conteneur d'application et le port du conteneur exposé sur la machine hôte.
Les serveurs d'intégration continue
https://docs.gitlab.com/ce/ci/ https://jenkins.io/ https://drone.io/
Exemple: Gitlab et Gitlab Runner
Le fichier .gitlab-ci.yml
# Déclaration de variables utilisables dans le fichier
variables:
MY_VAR: Hello World
# Nom de l'image Docker à utiliser pour les différentes phases du pipeline
image: <image_docker>
# Listes de commandes à exécuter dans le conteneur
# avant le lancement des différents jobs
before_script:
- echo ${MY_VAR}
# Déclaration des phases du pipeline
stages:
- test
- dist
# Déclaration d'un job
unit-tests:
stage: test # Ce job est attaché à la phase "test"
script: # Commandes à exécuter dans le conteneur
- ./script/test
# Déclaration d'un autre job
dist:
stage: dist # Ce job est attaché à la phase "dist"
only: # Ce job ne doit s'exécuter que lorsque la branche est "master"
refs:
- master
script:
- ./script/dist
artifacts: # Artefacts à conserver à l'issue de la phase
paths:
- dist/*.tar.gz
Exécuter un pipeline en local
gitlab-runner exec docker [job]
La fonctionnalité est dépréciée depuis la version 10.0. Voir la discussion https://gitlab.com/gitlab-org/gitlab-runner/issues/2797 pour le futur remplacement.
Exercice: Mise en application générale
Sélectionner une application existante (ou laissez le formateur vous proposer un sujet) et:
- Mettre en place l'exécution des tests unitaires sur le projet et écrire une première suite de tests.
- Mettre en place les tests d'intégration sur le projet et écrire une première suite de tests.
- Mettre en place les tests fonctionnels et écrire une première suite de tests.
- Concevoir un "pipeline" d'intégration continue avec Gitlab et Gitlab Runner et tester une activation complète du pipeline.