Aller au contenu

TP : Tests unitaires, d'intégration et E2E avec OWASP Juice Shop et GitLab CI

Ce TP vous fait pratiquer une stratégie de tests automatisés autour d'une application réelle : OWASP Juice Shop

L'objectif n'est pas de réaliser un audit de sécurité, mais de comprendre comment organiser des tests unitaires, des tests d'intégration et des tests end-to-end dans une pipeline GitLab CI/CD

Objectifs du TP

  • Identifier ce que chaque niveau de test doit prouver
  • Écrire des tests unitaires rapides sur du code isolé
  • Écrire des tests d'intégration contre l'API HTTP de Juice Shop
  • Écrire des tests E2E sur un parcours utilisateur avec Playwright
  • Automatiser l'exécution dans GitLab CI
  • Publier des rapports de tests au format JUnit XML dans GitLab
  • Conserver les artifacts utiles au diagnostic en cas d'échec

Contexte

OWASP Juice Shop est une application web volontairement vulnérable utilisée pour la formation

Dans ce TP, elle sert uniquement d'application cible pour apprendre à tester :

  • une fonction isolée utilisée par notre outillage de test
  • des endpoints HTTP de Juice Shop
  • un parcours utilisateur réel dans le navigateur

Vous allez créer un dépôt séparé contenant votre outillage de test

Structure cible :

juice-shop-testing-tp/
├── src/
│   └── juice-shop-client.js
├── tests/
│   ├── unit/
│   │   └── juice-shop-client.test.js
│   ├── integration/
│   │   └── product-search.test.js
│   └── e2e/
│       └── search.spec.js
├── .gitlab-ci.yml
├── package.json
└── playwright.config.js

Partie 1 : Préparer Juice Shop en local

Exercice 1 : Démarrer l'application cible

Lancez Juice Shop avec Docker :

docker run --rm --name juice-shop -p 3000:3000 bkimminich/juice-shop

Dans un navigateur, ouvrez :

http://localhost:3000

Travail demandé :

  1. Vérifiez que la page d'accueil se charge
  2. Fermez les éventuelles fenêtres d'accueil ou de cookies
  3. Recherchez un produit depuis l'interface
  4. Ouvrez les outils développeur du navigateur
  5. Dans l'onglet réseau, repérez l'appel HTTP déclenché par la recherche

Partie 2 : Initialiser le projet de tests

Exercice 2 : Créer le dépôt de travail

Créez un nouveau projet :

mkdir juice-shop-testing-tp
cd juice-shop-testing-tp
npm init -y
npm install --save-dev jest jest-junit wait-on @playwright/test@1.58.2
npx playwright install chromium

La version de @playwright/test est volontairement alignée avec l'image Docker utilisée plus loin dans GitLab CI

Si vous changez cette version, pensez à changer aussi l'image mcr.microsoft.com/playwright

Créez les dossiers :

mkdir -p src tests/unit tests/integration tests/e2e reports

Ajoutez ces scripts dans package.json :

{
  "scripts": {
    "test:unit": "jest tests/unit --ci --reporters=default --reporters=jest-junit",
    "test:integration": "jest tests/integration --ci --reporters=default --reporters=jest-junit",
    "test:e2e": "playwright test",
    "test": "npm run test:unit && npm run test:integration && npm run test:e2e",
    "wait:juice-shop": "wait-on http://localhost:3000"
  }
}

Ajoutez un .gitignore :

node_modules/
reports/
test-results/
playwright-report/
coverage/

Travail demandé :

  1. Initialisez un dépôt Git
  2. Créez un dépôt GitLab
  3. Poussez votre projet vide sur GitLab
  4. Vérifiez que les scripts npm apparaissent avec npm run

Partie 3 : Tests unitaires

Exercice 3 : Créer un petit client de test isolable

Dans les tests unitaires, vous ne devez pas démarrer Juice Shop

Vous allez tester une fonction pure qui prépare les requêtes de recherche envoyées ensuite à l'application

Créez src/juice-shop-client.js :

function normalizeSearchTerm(term) {
  return String(term).trim().replace(/\s+/g, ' ')
}

function buildProductSearchPath(term) {
  const normalizedTerm = normalizeSearchTerm(term)

  return `/rest/products/search?q=${encodeURIComponent(normalizedTerm)}`
}

function productNamesFromSearchResponse(responseBody) {
  if (!responseBody || !Array.isArray(responseBody.data)) {
    return []
  }

  return responseBody.data.map((product) => product.name)
}

module.exports = {
  normalizeSearchTerm,
  buildProductSearchPath,
  productNamesFromSearchResponse
}

Créez tests/unit/juice-shop-client.test.js

Travail demandé :

  1. Testez que normalizeSearchTerm supprime les espaces au début et à la fin
  2. Testez que plusieurs espaces internes deviennent un seul espace
  3. Testez que buildProductSearchPath encode correctement une recherche contenant un espace
  4. Testez que productNamesFromSearchResponse retourne uniquement les noms de produits
  5. Testez que productNamesFromSearchResponse retourne une liste vide si la réponse est invalide

Exemple de départ :

const {
  normalizeSearchTerm,
  buildProductSearchPath,
  productNamesFromSearchResponse
} = require('../../src/juice-shop-client')

test('normalise un terme de recherche', () => {
  expect(normalizeSearchTerm('  Apple   Juice  ')).toBe('Apple Juice')
})

Lancez les tests :

npm run test:unit

Partie 4 : Tests d'intégration

Exercice 4 : Tester l'API HTTP de Juice Shop

Un test d'intégration accepte une dépendance réelle

Ici, votre test va appeler Juice Shop par HTTP

Assurez-vous que Juice Shop tourne localement :

docker run --rm --name juice-shop -p 3000:3000 bkimminich/juice-shop

Dans un second terminal, vérifiez que l'application est prête :

npx wait-on http://localhost:3000

Créez tests/integration/product-search.test.js :

const {
  buildProductSearchPath,
  productNamesFromSearchResponse
} = require('../../src/juice-shop-client')

const baseUrl = process.env.JUICE_SHOP_URL || 'http://localhost:3000'

async function getStatus(path) {
  const response = await fetch(`${baseUrl}${path}`)

  return response.status
}

async function getJson(path) {
  const response = await fetch(`${baseUrl}${path}`)
  const body = await response.json()

  return {
    status: response.status,
    body
  }
}

Travail demandé :

  1. Testez que la page d'accueil répond avec un statut HTTP 200
  2. Testez que la recherche de produit sur Apple répond avec un statut HTTP 200
  3. Vérifiez que la réponse contient une propriété data
  4. Vérifiez qu'au moins un nom de produit contient Apple
  5. Testez qu'une recherche improbable retourne une liste vide ou ne contient pas le produit recherché

Exemple de test attendu :

test('recherche un produit via l API Juice Shop', async () => {
  const result = await getJson(buildProductSearchPath('Apple'))
  const productNames = productNamesFromSearchResponse(result.body)

  expect(result.status).toBe(200)
  expect(productNames.some((name) => name.includes('Apple'))).toBe(true)
})

Lancez les tests :

npm run test:integration

Partie 5 : Tests E2E avec Playwright

Exercice 5 : Configurer Playwright

Créez playwright.config.js :

module.exports = {
  testDir: './tests/e2e',
  use: {
    baseURL: process.env.JUICE_SHOP_URL || 'http://localhost:3000',
    trace: 'retain-on-failure',
    screenshot: 'only-on-failure'
  },
  reporter: [
    ['list'],
    ['junit', { outputFile: process.env.PLAYWRIGHT_JUNIT_OUTPUT_NAME || 'test-results/e2e.xml' }],
    ['html', { open: 'never' }]
  ]
}

Le format JUnit XML n'est pas réservé à Java

C'est un format de rapport de tests que GitLab sait afficher dans ses merge requests et dans l'onglet des tests de pipeline

Exercice 6 : Écrire un parcours utilisateur

Créez tests/e2e/search.spec.js

Travail demandé :

  1. Ouvrez la page d'accueil de Juice Shop
  2. Fermez les éventuels dialogues d'accueil ou de cookies
  3. Recherchez Apple
  4. Vérifiez qu'un produit contenant Apple est visible
  5. Recherchez un terme improbable
  6. Vérifiez que le résultat précédent n'est plus affiché ou que l'application indique l'absence de résultat

Exemple de point de départ :

const { expect, test } = require('@playwright/test')

async function closeOptionalDialogs(page) {
  await page.getByRole('button', { name: /dismiss/i }).click().catch(() => {})
  await page.getByRole('button', { name: /close/i }).click().catch(() => {})
}

test('un utilisateur peut rechercher un produit', async ({ page }) => {
  await page.goto('/')
  await closeOptionalDialogs(page)

  await page.getByRole('button', { name: /search/i }).click()
  await page.getByPlaceholder(/search/i).fill('Apple')
  await page.keyboard.press('Enter')

  await expect(page.getByText(/Apple/i)).toBeVisible()
})

Selon la version de Juice Shop, certains libellés ou sélecteurs peuvent varier

Si le test ne trouve pas un élément, inspectez la page avec Playwright Inspector ou les outils développeur du navigateur, puis adaptez le sélecteur

Lancez les tests :

npm run test:e2e

Partie 6 : Automatisation avec GitLab CI

Exercice 7 : Créer la pipeline

Créez .gitlab-ci.yml à la racine du dépôt

Base de travail :

stages:
  - test
  - e2e

default:
  image: node:22-bookworm
  cache:
    key:
      files:
        - package-lock.json
    paths:
      - .npm/
  before_script:
    - npm ci --cache .npm --prefer-offline

unit_tests:
  stage: test
  variables:
    JEST_JUNIT_OUTPUT_DIR: reports/unit
    JEST_JUNIT_OUTPUT_NAME: unit.xml
  script:
    - mkdir -p reports/unit
    - npm run test:unit
  artifacts:
    when: always
    paths:
      - reports/unit/
    reports:
      junit: reports/unit/unit.xml

integration_tests:
  stage: test
  services:
    - name: bkimminich/juice-shop:latest
      alias: juice-shop
  variables:
    JUICE_SHOP_URL: http://juice-shop:3000
    JEST_JUNIT_OUTPUT_DIR: reports/integration
    JEST_JUNIT_OUTPUT_NAME: integration.xml
  script:
    - mkdir -p reports/integration
    - npx wait-on "$JUICE_SHOP_URL"
    - npm run test:integration
  artifacts:
    when: always
    paths:
      - reports/integration/
    reports:
      junit: reports/integration/integration.xml

e2e_tests:
  stage: e2e
  image: mcr.microsoft.com/playwright:v1.58.2-noble
  services:
    - name: bkimminich/juice-shop:latest
      alias: juice-shop
  variables:
    JUICE_SHOP_URL: http://juice-shop:3000
    PLAYWRIGHT_JUNIT_OUTPUT_NAME: test-results/e2e.xml
  before_script:
    - npm ci --cache .npm --prefer-offline
  script:
    - npx wait-on "$JUICE_SHOP_URL"
    - npm run test:e2e
  artifacts:
    when: always
    paths:
      - playwright-report/
      - test-results/
    reports:
      junit: test-results/e2e.xml
  needs:
    - unit_tests
    - integration_tests
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH

Travail demandé :

  1. Commitez le fichier .gitlab-ci.yml
  2. Poussez votre branche sur GitLab
  3. Ouvrez Build > Pipelines
  4. Vérifiez que unit_tests et integration_tests s'exécutent dans le stage test
  5. Vérifiez que e2e_tests s'exécute ensuite
  6. Ouvrez les artifacts de chaque job
  7. Ouvrez les rapports de tests affichés par GitLab

Partie 7 : Faire échouer puis diagnostiquer

Exercice 8 : Lire un échec de test

Modifiez volontairement un test unitaire pour attendre une mauvaise valeur

Exemple :

expect(normalizeSearchTerm('  Apple   Juice  ')).toBe('Orange Juice')

Travail demandé :

  1. Poussez la modification sur GitLab
  2. Identifiez le job en échec
  3. Lisez les logs du job
  4. Ouvrez le rapport de tests GitLab
  5. Corrigez le test
  6. Poussez la correction
  7. Vérifiez que la pipeline redevient verte

Exercice 9 : Diagnostiquer un échec E2E

Modifiez volontairement le sélecteur de recherche dans le test Playwright

Travail demandé :

  1. Poussez la modification
  2. Attendez l'échec du job e2e_tests
  3. Téléchargez les artifacts playwright-report et test-results
  4. Ouvrez le rapport HTML Playwright
  5. Identifiez la ligne exacte du test en échec
  6. Corrigez le sélecteur
  7. Vérifiez que la pipeline repasse au vert

Partie 8 - Bonus

Bonus 1 : Ajouter la couverture

Ajoutez la couverture sur les tests unitaires

Objectifs :

  • générer un dossier coverage/
  • produire un rapport Cobertura
  • publier la couverture dans GitLab avec coverage_report
  • définir un seuil minimal avec Jest

Bonus 2 : Ajouter un test d'authentification

Écrivez un test d'intégration qui vérifie le comportement de la route de connexion

Objectifs :

  • tester une connexion invalide
  • vérifier le statut HTTP attendu
  • vérifier que le message d'erreur est exploitable
  • ne pas tester une faille de sécurité dans ce TP

Bonus 3 : Planifier les tests E2E

Ajoutez une règle GitLab CI pour exécuter une suite E2E plus complète uniquement la nuit

Expliquez le compromis entre rapidité du feedback et niveau de confiance

Critères de réussite

  • Juice Shop est utilisé comme application cible
  • les tests unitaires ne démarrent pas Juice Shop
  • les tests d'intégration appellent Juice Shop par HTTP
  • les tests E2E valident un parcours utilisateur dans le navigateur
  • la pipeline GitLab CI sépare les tests rapides et les tests E2E
  • les rapports de tests au format JUnit XML sont publiés dans GitLab
  • les artifacts utiles sont conservés pour diagnostiquer les échecs

☕️ Si tu souhaites soutenir mon travail, tu peux m'offrir un café ici.