IMDW320 / IINF170 - 29/03/2024 - Récapitulatif CI/CD/tests

Cette journée va faire l’objet d’une évaluation : vous devrez pousser des commits au fur et à mesure de votre avancée, sur un dépôt que vous aurez forké.

Objectifs

  • Réviser les notions vues depuis le premier cours :

    • Pipelines GitLab CI
    • Tests
  • Intégrer différents types de tests dans un pipeline GitLab CI

  • Aborder un exemple de pipeline plus complexe, au travers d’un “monorepo” frontend + backend

Modalités

Un dépôt vous est fourni, avec du code backend, frontend, une partie des outils configurés.

Vous allez écrire un pipeline GitLab CI presque complet, intégrant le linting, différents types de tests, etc.

Quelques précisions :

  • Pour simplifier, et ne pas vous “surcharger” avec TypeScript, le code est en JavaScript.
  • Il y aura des tests de composants React, mais ils resteront simples - et pas (ou peu) de code React à modifier (ou alors de façon très guidée).
  • Lorsque ce sera possible, les étapes seront relativement indépendantes les unes des autres, mais si vous bloquez sur une étape, demandez-moi.

En résumé, Je n’attends pas de vous que vous sachiez absolument tout faire, mais :

  1. Que vous essayiez,
  2. Que vous posiez des questions si vous n’y arrivez pas.

Point de départ

On vous donne un dépôt avec un backend et un frontend : https://gitlab.com/bhubert/ipi-cicd-todoapp

Il s’agit d’une application de “todo-list”. Pas très original certes, mais adapté pour réviser les notions de tests…

  1. Forkez ce dépôt sur GitLab
  2. Clonez-le

❗️❗️N’ALLEZ PAS PLUS LOIN (pour l’instant)❗️❗️

Vue d’ensemble du pipeline

Voici à quoi ressemblera le pipeline dans son état final :

Pipeline dependency graph

L’ordre des stages (étapes) diffère un peu de ce qu’on avait pu voir.

Les étapes de build ne sont plus découpées en deux parties (compilation TypeScript dans une étape, puis build d’une image Docker dans la suivante) : on build directement les images Docker.

La raison à cela est que l’étape de tests end-to-end (test_e2e) nécessite des images Docker.

Dans l’interface de GitLab, le pipeline ressemblera à ceci (ici en cours d’exécution) :

Pipeline dependency graph in GitLab

Notez le fait qu’on ait coché “Show dependencies” pour montrer les dépendances entre les jobs.

  • Cette vue d’ensemble est accessible, depuis la barre latérale, dans le menu Build > Pipelines puis en cliquant sur l’id d’un pipeline.
  • Un autre outil qui pourra vous servir est Build > Pipeline editor. Il vous permettra, en collant le code YAML de votre pipeline, de vérifier qu’il ne comporte pas d’erreur : il est en effet frustrant qu’un pipeline échoue à cause d’une erreur de syntaxe.

Étapes à implémenter - dans cet ordre - dans le pipeline

Le plan détaillé sera fourni étape par étape.

  1. install → Installation des dépendances du projet
  2. lint_backend → Exécution d’ESLint dans la partie backend du dépôt
  3. lint_frontend → Exécution d’ESLint dans la partie frontend du dépôt
  4. test_backend → Exécution des tests backend (tests d’intégration Express)
  5. test_frontend → Exécution des tests frontend (tests de composants React + tests unitaires)
  6. build_backend_image → Construction de l’image Docker du backend
  7. build_frontend_image → Construction de l’image Docker du frontend
  8. test_e2e → Exécution des tests end-to-end, utilisant les images Docker
  9. push_images → Push des images Docker vers un registry (par ex. Docker Hub)

Un “squelette” incomplet de pipeline vous est fourni dans le dépôt, et reproduit ci-dessous.

Il va s’agir de remplir les différentes étapes.

# Définit une image Docker à utiliser pour les jobs,
# si une image n'est pas spécifiée pour un job.
# Pas obligatoire !! On peut définir une image par stage
# default:
#   image: node:lts-alpine

# Définit les étapes de pipeline.
# Chaque étape est exécutée dans l'ordre défini.
stages:
  - install
  - lint_backend
  - lint_frontend
  # - test_backend
  # - test_frontend
  # - build_backend_image
  # - build_frontend_image
  # - test_e2e
  # - push_images

# ICI => cache (pour les node_modules)

install:
  stage: install
  script:
    - echo "Installing dependencies with npm..."
    - sleep 5

lint_backend:
  stage: lint_backend
  script:
    - echo "Linting backend..."
    - sleep 5
  needs:
    - install

lint_frontend:
  stage: lint_frontend
  script:
    - echo "Linting frontend..."
    - sleep 5
  needs:
    - install
# TODO => ajouter
# - test_backend
# - test_frontend
# - build_backend_image
# - build_frontend_image

# Désactivé pour le moment
# LAISSÉ car il démontre par exemple l'utilisation
# de dépendances multiples, et de services
# test_e2e:
#   image: docker:latest
#   services:
#     - docker:dind
#   stage: test_e2e
#   script:
#     - echo "Running end-to-end tests..."
#     - sleep 5
#   needs:
#     - build_backend_image
#     - build_frontend_image

# Désactivé pour le moment
# push_images:
#   stage: push_images
#   script:
#     - echo "Pushing Docker images to registry..."
#     - cat toto.txt
#     - sleep 5
#   needs:
#     - test_e2e

0. Configuration du projet en local

Cette étape ne concerne pas vraiment le pipeline CI !

Workspaces

Dans plusieurs dépôts que je vous ai fournis précédemment, il fallait installer les dépendances séparément dans les dossiers backend et frontend.

Dans ce dépôt, vous n’avez à le faire qu’une seule fois, à la racine du dépôt. C’est dû à la présence de la propriété workspaces dans le package.json de la racine. Cela permet de centraliser tous les node_modules à la racine du dépôt.

Installation des dépendances dans votre dépôt cloné

Une fois placé dans votre dossier ipi-cicd-todoapp, vous pouvez lancer npm install.

Premier lancement de l’application

Ouvrez deux fenêtres/onglets de terminal, une sous backend, une sous frontend, et lancez dans chaque dossier npm run dev. Cela lance le script dev du package.json. (si besoin, voir rappels sur npm dans les annexes).

Le frontend est accessible à l’URL http://localhost:5173.

Si vous le visitez en l’état, vous devriez obtenir :

    • un message en bas au centre, en vert : Message from server: Hello World! → cela signifie que le frontend communique bien avec le backend.
    • plus gênant, au centre, en rouge : Error fetching tasks: Request failed with status code 500.

Si vous examinez la console d’où vous avez lancé le backend, vous en aurez l’explication : l’app a besoin de se connecter à une base de données MySQL

Nous allons tout de suite voir comment adresser cela.

Créer une base de données MySQL

Trois options sont possibles :

  1. Avec Docker
  2. En natif sur votre machine
  3. Sur un serveur distant

L’option Docker est de loin la meilleure, les autres ne doivent être envisagées que si Docker ne marche pas chez vous.

1. Avec Docker

Dans un autre terminal, vous pouvez d’abord pull la dernière version de l’image MySQL :

docker pull mysql:latest

Puis lancez-la comme ceci :

docker run -d --name mysql -p 3306:3306 -e MYSQL_ROOT_PASSWORD=root_password -e MYSQL_DATABASE=app_todolist_test -e MYSQL_USER=app_todolist_test -e MYSQL_PASSWORD=app_todolist_test mysql:latest

Quelques explications sur les arguments et de docker run :

  • -ddaemonize, lancer en tâche de fond (pour éviter de bloquer la console)

  • -p 3306:3306 → établit une correspondance entre un port sur l’hôte (avant le :) et un port sur le conteneur (MySQL écoute par défaut sur 3306). Avec ce “mapping”, ce sera comme si vous utilisiez un MySQL “standard”.

  • --name mysql → nommer le conteneur mysql - cela sera pratique pour s’y référer, par ex. avec docker stop, docker exec, etc.

  • -e VARIABLE=VALEUR → passer une variable d’environnement au conteneur. La doc de l’image officielle MySQL en mentionne plusieurs, ici nous utilisons :

    • MYSQL_ROOT_PASSWORD → mot de passe admin de MySQL
    • MYSQL_DATABASE, MYSQL_USER, MYSQL_PASSWORD → respectivement nom d’une base de données que MySQL va créer, username et password d’un utilisateur qui aura les droits d’accès à cette base de données
  • mysql:latest → image à partir de laquelle démarrer le conteneur

Après le lancement, vous pouvez lancer docker logs mysql -f pour suivre la procédure de démarrage, puis interrompre (Ctrl-C) quand vous voyez que la BDD est prête :

2024-03-28T19:26:03.838165Z 0 [System] [MY-010931] [Server] /usr/sbin/mysqld: ready for connections. Version: '8.3.0'  socket: '/var/run/mysqld/mysqld.sock'  port: 3306  MySQL Community Server - GPL.
2. Natif

Concerne MySQL installé sur votre machine Windows, macOS ou Linux.

À ne faire de préférence que si vous avez déjà installé - je préfère ne pas avoir à faire du dépannage d’installation 😬.

Lancez soit la console mysql dans un terminal, ou MySQL Shell, ou tout autre outil vous permettant d’envoyer des requêtes à votre BDD.

Vous pouvez coller dans cette console le contenu du fichier backend/sql/create-db-test.sql.

Cela créera un utilisateur avec les mêmes paramètres que pour l’option Docker.

3. BDD externe

Me demander : j’ai créé un certain nombre de comptes sur une VM hébergée chez moi, à usage temporaire, le temps de cette journée.

Les BDD sont déjà créées avec des noms d’utilisateurs et mots de passe uniques, que je ne peux évidemment pas les publier ici !

Configuration du backend pour la connexion à MySQL

Une fois votre BDD en route, il vous faut :

  • aller sous backend,
  • copier le fichier sample.env en tant que .env
  • attribuer des valeurs aux variables DB_NAME, DB_USER, DB_PASS ; laisser les variables DB_HOST et DB_PORT vides, cela concerne ceux à qui j’aurai donné un accès à une BDD externe.

Sinon mettez directement ça dans .env :

DB_HOST=localhost
DB_NAME=app_todolist_test
DB_USER=app_todolist_test
DB_PASS=app_todolist_test

Exécution des migrations puis de l’app

Exécutez les migrations :

npx db-migrate up -e test

Relancez le backend :

npm run dev

Cette fois-ci, le frontend devrait pouvoir communiquer avec le backend, et le gros message d’erreur en rouge devrait avoir disparu !

Prenez un peu de temps à interagir avec l’application pour voir comment elle fonctionne.

1. Étape install du pipeline

Vous pouvez, ici comme à chaque étape, supprimer les sleep 5, qui font office de “remplissage”.

Il va vous falloir modifier la stage install du pipeline .gitlab-ci.yml, de façon à installer les dépendances du projet.

Il y a juste une commande à ajouter dans la partie script du pipeline !

Par contre, il y a d’autres modifications à apporter.

  • D’une part, il faut choisir quelle image va servir à lancer le conteneur qui exécute la stage. Ce choix peut être fait :

    • stage par stage, en ajoutant la propriété image: nom-de-l-image:tag,
    • ou en indiquant, tout en haut du pipeline, cette propriété : ce sera alors l’image par défaut pour toutes les stages qui n’ont pas la propriété image.
    • pour la stage install, comme pour les 4 suivantes, je vous recommande fortement d’utiliser l’image node:lts-alpine (Alpine = image Linux légère, LTS = long term support).
  • D’autre part, il arrive que des stages aient besoin de fichiers produits par des stages précédentes. Le résultat de la stage présente est la création d’un dossier node_modules. Les 4 stages suivantes auront besoin de les récupérer.

Vous pouvez référer au support de cours de la 2ème journée pour vous inspirer.

❗️ Avant chaque git add/commit/push votre fichier .gitlab-ci.yml, je vous conseille de copier-coller son contenu dans l’éditeur de pipeline, que vous pouvez trouver, depuis la home page de votre dépôt, sous Build > Pipeline Editor dans la barre latérale.

Quand vous collez, GitLab “mouline” un peu, puis vous indique si le pipeline est valide, ou s’il comporte des erreurs.

Par contre, pas la peine de sauvegarder le fichier dans l’interface de GitLab : on s’en sert juste pour vérifier, mais on fait la modification localement et on pousse.

2. Étape lint_backend

Nous avions vu - rapidement - ESLint lors de la première journée. Nous allons revoir son installation et sa configuration, pour le backend.

❗️ Placez-vous dans le dossier backend.

Assistant de configuration d’ESLint

Lancez cette commande, qui démarre l’assistant de configuration d’ESLint, en ligne de commande :

npm init @eslint/config

L’assistant va vous poser plusieurs questions, et il est important de bien choisir les réponses !

How would you like to use ESLint?

La dernière option est la meilleure, exception faite de mini-projets à la durée de vie très limitée. Nous détaillerons cela plus loin.

  To check syntax only
  To check syntax and find problems
❯ To check syntax, find problems, and enforce code style
What type of modules does your project use?

Garder la première option. Traditionnellement, les projets Node.js utilisaient CommonJS, mais ici, nous avons configuré babel, qui permet d’utiliser la syntaxe import/export.

Celle-ci commence à être prise en charge nativement par Node.js dans les dernières versions, avec quelques restrictions.

❯ JavaScript modules (import/export)
  CommonJS (require/exports)
  None of these
Which framework does your project use?

Dernière option car nous parlons ici de code backend !!

  React
  Vue.js
❯ None of these
Does your project use TypeScript? › No / Yes

Valider directement No : pour simplifier, nous sommes restés sur du code JavaScript.

Where does your code run?

Attention ici : appuyez sur espace pour désélectionner Browser, descendez et rappuyez sur espace pour sélectionner Node.

  Browser
✔ Node
How would you like to define a style for your project?

Valider la 1ère option.

❯ Use a popular style guide
  Answer questions about your style

Que signifie cette question ?

On a le choix entre :

  • ou choisir un ensemble de règles qui a déjà été créé par une équipe de développement (chez Google, Airbnb ou autre), et qui a été publiée et adoptée par d’autres équipes (d’où popular style guide): c’est probablement mieux de se fier à des “pros”.
  • définir notre propre ensemble de règles (par ex. entourer les chaînes de caractères avec des guillemets simples ou doubles, mettre des point-virgules en fin de ligne, une indentation à deux espaces, etc.)
Which style guide do you want to follow?

Valider la 1ère option : Airbnb.

❯ Airbnb: https://github.com/airbnb/javascript
  Standard: https://github.com/standard/standard
  Google: https://github.com/google/eslint-config-google
  XO: https://github.com/xojs/eslint-config-xo

La style guide Airbnb est stricte, mais permet de détecter bon nombre d’erreurs subtiles. Elle a également de bons (à mon avis) choix par défaut :

  • indentation à deux espaces,
  • point-virgules en fin de lignes,
  • etc.
What format do you want your config file to be in?

Valider la 1ère option : JavaScript - même si ici, je dirais “peu importe” !

❯ JavaScript
  YAML
  JSON
Installer les dépendances

Valider le choix par défaut Yes.

Checking peerDependencies of eslint-config-airbnb-base@latest
Local ESLint installation not found.
The config that you've selected requires the following dependencies:

eslint-config-airbnb-base@latest eslint@^7.32.0 || ^8.2.0 eslint-plugin-import@^2.25.2
? Would you like to install them now? › No / Yes
Which package manager do you want to use?

À nouveau le choix par défaut car le dépôt a été configuré avec npm.

❯ npm
  yarn
  pnpm

C’est terminé pour l’assistant de configuration !

Il reste quelques réglages à effectuer, mais vous pouvez remarquer que dans votre IDE, un fichier .eslintrc.js est apparu à la racine du backend.

Faites git add .eslintrc.js package.json puis commitez en indiquant dans votre message “configuration initiale d’ESLint” (en anglais ou français, peu importe). Ne poussez pas encore !

Autres réglages pour ESLint

D’abord, créez un fichier .eslintignore sous backend, avec ce contenu :

dist
migrations

Cela va ignorer les builds générés dans dist par la commande npm run build (qui appelle Babel). On va ignorer également les migrations, produites par l’outil db-migrate avec une ancienne syntaxe JavaScript.

Maintenant, modifiez .eslintrc.js pour :

  • sous node: true, ajouter jest: true, pour qu’ESLint ne remonte pas d’erreur quand il rencontre les describe, it, beforeEach, etc. de Jest
  • remplacez extends: 'airbnb-base' par extends: ['airbnb-base', 'prettier'] ; cela permettra à Prettier et ESLint de “cohabiter pacifiquement”. Sinon, quand on formate son code avec Prettier, ESLint peut se plaindre, car certaines règles de Prettier peuvent contredire celles de la style guide choisie précédemment (ici Airbnb). Ici, on met 'prettier' en dernier, pour que les règles de Prettier prennent le pas sur certaines de la style guide.

Au final, votre config doit ressembler à ça :

module.exports = {
  env: {
    es2021: true,
    node: true,
    jest: true,
  },
  extends: ["airbnb-base", "prettier"],
  overrides: [
    {
      env: {
        node: true,
      },
      files: [".eslintrc.{js,cjs}"],
      parserOptions: {
        sourceType: "script",
      },
    },
  ],
  parserOptions: {
    ecmaVersion: "latest",
    sourceType: "module",
  },
  rules: {},
};

L’ajout qu’on a fait à extends implique d’avoir un paquet correspondant installé. Lancez :

npm i -D eslint-config-prettier

Après cette modification, ESLint va utiliser les règles des modules eslint-config-airbnb-base et eslint-config-prettier.

Faites git add .eslintrc.js .eslintignore puis commitez. Ne poussez pas encore !

Modification du package.json pour lancer ESLint

Lancer manuellement ESLint se fait avec npx eslint --ext .js .. Pour simplifier cela, ajoutez un script lint à package.json :

  "scripts": {
    ...
    "lint": "eslint --ext .js .",
    ...
  }

Testez avec npm run lint pour voir si ESLint vous remonte des erreurs !

Ne vous inquiétez pas des lignes avec warning en jaune. La convention de codage Airbnb indique qu’il vaut mieux éviter les console.log. Des modules “loggers” tels que debug ou winston sont plus appropriés pour afficher des informations dans la console. Mais pour l’instant, on fera sans !

Modification du pipeline

Modifiez l’étape lint_backend du pipeline pour linter le code backend.

Attention, dans la partie script de cette stage :

  • il faut d’abord se placer dans backend
  • puis lancer le script de lint avec la commande testée à l’instant

À nouveau, vérifiez votre pipeline dans GitLab CI. Puis commitez, et cette fois, poussez !

C’est la fin de cette étape. À noter : il est possible de rendre automatique le linting, avant même de commiter, avec les outils Husky et Lint-Staged. On le fera si on a le temps. Si vous êtes en avance, cherchez comment faire (avec la subtilité : on utilise un “npm workspace”, ce qui aura probablement une incidence).

3. Étape lint_frontend

Cela va être beaucoup plus rapide que dans l’étape précédente, car ESLint est déjà pré-configuré par l’outil Vite, avec lequel on a initialisé l’application frontend.

Un script lint est même déjà fourni dans package.json !

Il ne vous reste plus qu’à modifier le pipeline, à le vérifier, à le commiter et pousser.

4. Étape test_backend

Cette étape est dépendante de l’étape lint_backend, ce que vous pouvez matérialiser avec la propriété needs.

On ne va pas ici faire de configuration : cela a déjà été fait pour vous.

On va en revanche se focaliser sur les tests, et appliquer les notions de tests unitaires et tests d’intégration vues lors de la 1ère journée consacrée aux tests.

Plus de détails vous sont donnés ci-dessous, mais il vous est proposé d’écrire :

  • 1 test unitaire pour la fonction castDoneToBoolean définie dans backend/src/helpers/cast-done-to-boolean.js

  • au moins 1 test d’intégration (mais le plus est le mieux), parmi ceux-ci :

    • ajouter un test de succès pour le endpoint DELETE /api/items/:id (le plus facile).
    • ajouter un cas de test de succès pour le endpoint POST /api/items,
    • écrire un cas de test d’échec pour le endpoint PUT /api/items/:id

Même si vous vous sentez confiant, commencez par vous tenir au test unitaire, et à un seul test d’intégration ; vous pourrez passer à la section suivante (Exécution des tests dans la stage test_backend), et revenir à l’écriture de tests plus tard.

Pour exécuter les tests, lancez :

  • soit npm test : lance tous les tests, une seule fois
  • soit npx jest --watchAll : lance tous les tests, et reste en attente de modifications pour les relancer ! À ne pas utiliser dans une CI car ça bloque le pipeline !

Test unitaire - castDoneToBoolean

Il s’agit de tester que la fonction castDoneToBoolean convertit l’attribut done d’une tâche de la base de données en booléen.

La fonction est documentée dans le fichier backend/src/helpers/cast-done-to-boolean.js.

Le test est à écrire sous backend/test/unit/cast-done-to-boolean.test.js.

Vous pouvez vous inspirer, pour la structure du test et sa relation avec la fonction à tester, d’une fonction qu’on a mise dans la base de code pour faire une démonstration :

  • backend/src/helpers/add-full-name.js
  • backend/test/unit/add-full-name.test.js

Si on passe à la fonction castDoneToBoolean l’objet suivant :

{
  "id": 5,
  "name": "Écrire un pipeline CI",
  "done": 0,
  "createdAt": "2024-03-29T04:50:39.340Z",
  "updatedAt": "2024-03-29T04:50:39.340Z"
}

On s’attend à ce qu’elle renvoie :

{
  "id": 5,
  "name": "Écrire un pipeline CI",
  "done": false,
  "createdAt": "2024-03-29T04:50:39.340Z",
  "updatedAt": "2024-03-29T04:50:39.340Z"
}

Tests d’intégration

Tous les tests d’intégration sont à ajouter à test/integration/item-endpoints.test.js

Endpoint DELETE /api/items/:id - cas succès
  • S’inspirer du test PUT /api/items/:id pour voir comment on crée une tâche
  • Envoyer une requête DELETE sans données
  • S’attendre à recevoir un code succès 204 (l’opération a réussi mais aucune donnée n’est renvoyée)
  • S’attendre à ce que res.body soit un objet vide.
Endpoint POST /api/items - cas succès
  • S’inspirer à la fois du test fourni (cas d’échec) sur cette même route, et du test fourni pour la route suivante ci après en PUT
  • Envoyer une requête POST avec un object contenant name.
  • S’attendre à recevoir un code succès 201 avec un objet de forme similaire à celui renvoyé par la route PUT.
Endpoint et PUT /api/items/:id
  • S’inspirer des autres tests pour voir comment on peut envoyer une requête avec des données et comment faire des assertions sur la réponse.
  • Envoyer une requête PUT sans données
  • S’attendre à recevoir un code erreur 400 avec un objet contenant un message d’erreur.
À compléter ?

Si, après l’exploration des étapes suivantes, vous souhaitez aller plus loin, vous pouvez écrire un test pour un endpoint DELETE /api/items/all visant à supprimer toutes les tâches de la BDD, puis écrire l’implémentation correspondante dans app.js.

Exécution des tests dans la stage test_backend

Pour cette stage, utiliser l’image node:lts-alpine

Il va maintenant s’agir d’exécuter les tests dans la stage test_backend.

C’est moins aisé qu’il n’y paraît ! Un simple npm test ne va pas suffire…

Comme les tests sont en majorité des tests d’intégration, il faut nous disposer d’une vraie base de données MySQL.

Ici intervient la notion de services. Vous pouvez consulter cette section de la documentation de GitLab : Use CI/CD > Services > MySQL service.

Les deux premiers blocs de code de la doc peuvent vous inspirer :

D’abord, à l’intérieur de votre stage test_backend, ajoutez une propriété services.

Votre stage ressemble alors à ceci :

test_backend:
  services:
    - mysql:latest

Pour la durée de vie de cette stage, on va lancer un serveur MySQL, basé sur l’image Docker officielle. Il va falloir configurer ce serveur avec des variables d’environnement. Si vous avez utilisé Docker pour lancer MySQL sur votre machine, c’est exactement la même chose.

Ajoutez un bloc variables sous services en y définissant les variables :

  variables:
    MYSQL_ROOT_PASSWORD: VotreMotDePasseRoot
    MYSQL_DATABASE: nom_de_la_bdd
    MYSQL_USER: nom_utilisateur
    MYSQL_PASSWORD: MotDePasseUtilisateur

Les valeurs que vous mettez importent peu : les considérations de sécurité sont ici moindres, puisqu’on est en environnement de test, sur des BDD éphémères.

Vous pouvez ici “hardcoder” les valeurs que vous voulez. Utiliser les variables d’environnement via l’interface de GitLab (Settings > CI/CD > Variables) est une autre option, mais semble plus lourde à mettre en place.

Il faut également ajouter des variables pour db-migrate et pour l’app Node dans ce bloc variables : les DB_* telles que vous les aviez dans votre backend/.env ou backend/sample.env. Vous devrez utiliser ces valeurs :

  • DB_HOSTmysql (le service est un conteneur avec le même nom que l’image)
  • DB_NAME → même valeur que MYSQL_DATABASE
  • DB_USER → même valeur que MYSQL_USER
  • DB_PASS → même valeur que MYSQL_PASSWORD

❗️ Il faut également exécuter les migrations avant de lancer les tests

Donc, dans le script de la stage, il faut lancer npx db-migrate up -e test, avant de lancer npm test.

5. Étape test_frontend

Cette étape est dépendante de l’étape lint_frontend, ce que vous pouvez matérialiser avec la propriété needs.

À nouveau, on ne va pas ici faire de configuration.

Écriture des tests

On va revenir sur les tests de composants - mais pas de panique :

  • Vous n’avez pas de composants React à écrire ou à modifier
  • Les tests requis sont simples

L’app frontend comporte beaucoup de composants.

On a fourni des tests pour votre inspiration :

  • le composant TodoItem, dans le fichier frontend/src/components/__tests__/TodoItem.test.jsx
  • le composant AppTitle, dans le fichier frontend/src/components/__tests__/AppTitle.test.jsx

Vous êtes invités à en écrire deux (dans un premier temps) :

  • pour le composant TodoListError - qui reçoit simplement un message d’erreur et l’affiche en rouge (c’est le composant qui indiquait “Network Error” avant la configuration de l’app backend)

  • pour le composant Footer :

    • il peut y avoir un cas de “succès” (on lui passe une propriété message - chaîne de caractères)
    • et un cas d’erreur (on lui passe une propriété error, qui elle-même est un objet comprenant une propriété message)

Si et seulement si vous avez suffisamment avancé sur le reste, vous pouvez étoffer la suite de tests :

  • tester le composant AddTodo : cas plus complexe car met en oeuvre des interactions utilisateurs simulées, ainsi que le fait le test de TodoItem
  • dans TodoItem, changer le formatage de la propriété createdAt, pour l’afficher à un format plus lisible, par exemple : 11/07/2024, 11:32. Vous pouvez pour cela écrire une fonction utilitaire dans un fichier séparé, et tester cette fonction.

Exécution des tests de composants dans la stage test_frontend

Cette stage est plus simple que son pendant backend : il n’y a qu’à lancer les tests avec la commande habituelle !

6. Étape build_backend_image

Ici, contrairement aux étapes précédentes, il va falloir utiliser l’image docker:latest.

Il va également falloir ajouter un block services avec docker:dind.

La stage va ressembler à ceci (rajouter également needs pour qu’elle s’exécute après test_backend).

build_backend_image:
  stage: build_backend_image
  image: docker:latest
  services:
    - docker:dind
  script:
    - echo "Build image Docker backend"

Maintenant, que va-t-on mettre dans script ?

De façon évidente, une commande de type docker build.

En fait, cette stage est relativement courte :

  1. invocation de docker build,
  2. sauvegarde de l’image Docker en vue d’une réutilisation dans les stages suivantes

La difficulté réside dans l’écriture du Dockerfile, d’autant qu’il va être un peu différent de ceux qu’on a pu écrire jusqu’ici.

Dans les cours précédents, on avait réparti le build sur deux stages :

  • une stage utilisant l’image node et compilant des sources TypeScript avec tsc, pour obtenir un dossier dist contenant des fichiers .js.
  • une stage de build Docker dans laquelle on récupérait ce dossier dist : on faisait un COPY dist dist.

Cette fois, même si on n’utilise pas TypeScript, on doit transpiler du code JavaScript moderne - avec la syntaxe import/export ES6 - en code JavaScript exécutable directement par node.

On va faire cela au travers du script dev, qui appelle babel (outil utilisé également par les systèmes de build pour React, etc.).

Mais cela va être fait lors du build de l’image Docker.

Pour résumer, le Dockerfile doit :

  • se baser sur une image nodenode:lts-alpine

  • copier les fichiers :

    • package.json
    • babel.config.json → pour l’outil babel,
    • database.json → pour l’outil de migration de bases de données db-migrate,
    • src → code source de l’app,
    • migrations → migrations exécutées par db-migrate pour construire la BDD
  • lancer npm run build

  • enfin, exécuter l’application

Traditionnellement, le lancement d’une application Node en production se fait via npm start, qui invoque node avec, comme argument, le point d’entrée de l’application.

Si vous regardez le package.json, vous pouvez voir :

  • le script build : babel -d dist src va transpiler les .js du dossier src et stocker les .js résultants dans dist.
  • le script start : node dist/index.js va exécuter le point d’entrée de l’application, dist/index.js.

Normalement, vous devriez donc avoir une CMD qui conclut votre Dockerfile comme ceci :

CMD ["npm", "start"]

Ou, ce qui revient au même :

CMD ["node", "dist/index.js"]

Mais ce n’est pas si simple, car il faut - comme dans la stage test_backend avant de lancer les tests - exécuter les migrations avant de lancer l’app.

Pour ce faire, il y a plusieurs options :

  • chaîner des commandes dans un même script npm
  • utiliser un module npm qui permet d’invoquer plusieurs scripts npm dans un script
  • chaîner plusieurs commandes dans le CMD du Dockerfile
  • utiliser un script shell

Un exemple pour chaque option :

Chaîner des commandes dans un script npm :

{
  "scripts": {
    "hello-goodbye": "echo Hello && echo Goodbye"
  }
}

On pourrait lancer hello-goodbye à la fin d’un Dockerfile comme ceci :

CMD npm run hello-goodbye

OU

CMD ["npm", "run", "hello-goodbye"]

Utiliser un module npm

Un module comme npm-run-all permet d’exécuter plusieurs scripts en parallèle ou en séquentiel - par défaut, en séquentiel.

{
  "scripts": {
    "hello": "echo Hello",
    "goodbye": "echo Goodbye",
    "hello-goodbye": "npm-run-all hello goodbye"
  },
  "dependencies": {
    "npm-run-all": "4.1.5"
  }
}

Dans le Dockerfile : même chose que dans l’exemple précédent

7. Étape build_frontend_image

Dockerfile front

# Stage 1 - Build React app into static files
FROM node:lts-alpine AS builder

WORKDIR /app

COPY package.json vite.config.js index.html ./
RUN npm install

COPY src ./src

RUN npm run build

# Stage 2 - Copy into nginx and serve static files
FROM nginx:stable-alpine

COPY --from=builder /app/dist /usr/share/nginx/html
# Contenu de ce fichier ci-dessous
COPY docker/nginx.conf /etc/nginx/nginx.conf

EXPOSE 80

CMD ["nginx", "-g", "daemon off;"]

Config nginx à stocker sous frontend/docker/nginx.conf, et à copier sous /etc/nginx/nginx.conf dans le Dockerfile

# Ref: https://cli.vuejs.org/guide/deployment.html#docker-nginx

user nginx;
worker_processes 1;
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;

events {
    worker_connections 1024;
}

http {
    include /etc/nginx/mime.types;
    default_type application/octet-stream;
    log_format main
    '$remote_addr - $remote_user [$time_local] "$request" '
    '$status $body_bytes_sent "$http_referer" '
    '"$http_user_agent" "$http_x_forwarded_for"';
    access_log /var/log/nginx/access.log main;
    sendfile on;
    keepalive_timeout 65;
    server {
        listen 80;
        server_name localhost;

        # Proxy to API backend
        location /api {
            proxy_pass http://backend:5000;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
        }

        # Serve the React app build
        location / {
            root /usr/share/nginx/html;
            index index.html;
            try_files $uri $uri/ /index.html;
        }
        error_page 500 502 503 504 /50x.html;
        location = /50x.html {
            root /usr/share/nginx/html;
        }
    }
}

Pour tester les deux images back + front

# docker-compose.yml

# Docker Compose for full-stack Express+React+MySQL
# Backend image is ipi-recap-back and exposing 5000
# It needs vars DB_HOST, DB_USER, DB_PASS, DB_NAME
# Frontend image is ipi-recap-front and exposing 80 (nginx proxying to backend:5000)
# We'll need 3 containers here: backend, frontend and db

---
version: '3.7'

services:
  mysql:
    image: mysql:8
    container_name: mysql
    environment:
      MYSQL_ROOT_PASSWORD: root
      MYSQL_DATABASE: ${DB_NAME}
      MYSQL_USER: ${DB_USER}
      MYSQL_PASSWORD: ${DB_PASS}
    ports:
      - "3306:3306"
    networks:
      - mynetwork

  backend:
    image: ipi-recap-back
    container_name: backend
    environment:
      NODE_ENV: ${NODE_ENV}
      DB_HOST: mysql
      DB_USER: ${DB_USER}
      DB_PASS: ${DB_PASS}
      DB_NAME: ${DB_NAME}
    ports:
      - "5000:5000"
    networks:
      - mynetwork
    depends_on:
      - mysql
    restart: unless-stopped

  frontend:
    image: ipi-recap-front
    container_name: recapfront
    ports:
      - "8000:80"
    networks:
      - mynetwork
    restart: unless-stopped

networks:
  mynetwork:
    driver: bridge

6 à 7. SUPPORT INCOMPLET 😐

Étape 8 - tests e2e

Vous pouvez utiliser les mêmes commandes en local que d’habitude pour lancer les tests.

npm run wdio

Explorez des idées de cas de test (modification d’une tâche, suppression, etc).

Demandez moi, si vous vous ennuyez…

Vous pouvez explorer :

  • Husky
  • Penser à d’autres cas de tests end-to-end
  • Les builds Docker multi-stage

Annexes

Rappels sur npm

Cette section donne des rappels sur npm, l’outil de gestion de paquets fourni avec Node.js.

npm fournit un grand nombre de commandes (npm --help pour en avoir un aperçu). Parmi celles qu’on utilise souvent :

  • init permet d’initialiser le fichier package.json pour un nouveau projet, ou, suivi d’un nom de paquet, de configurer l’installation d’une bibliothèque (exemples : @eslint/config, wdio).

  • install

    • sans argument, installe tous les paquets référencés dans package.json (sous dependencies et devDependencies) : ils sont téléchargés depuis le registre NPM, décompressés et stockés sous node_modules.
    • suivi d’un ou plusieurs nom(s) de paquet(s) permet de les installer et de les ajouter au `package.json.
  • uninstall est son inverse.

En outre, npm permet d’exécuter des scripts qu’on peut exécuter dans un répertoire contenant un fichier package.json. Prenons l’exemple de ce package.json minimaliste :

{
  "scripts": {
    "echo": "echo This is a message",
    "start": "node index.js"
  }
}
  • On peut exécuter les scripts avec npm run <nom script> - npm run echo et npm run start fonctionneront.
  • **Uniquement pour certains scripts bien définis - par ex. start, test - on peut lancer plus simplement npm start ou npm test.