Aller au contenu
DevOps 9 min de lecture

Tests automatisés avec Cypress : guide complet pour sites web

Mettre en place des tests end-to-end avec Cypress : installation, premiers tests, sélecteurs robustes, fixtures, intercept réseau, CI/CD et bonnes pratiques. Guide pratique pour développeurs frontend et QA.

CypressTestsE2ECI/CDAutomatisationJavaScriptQA

Les tests manuels ont une limite : ils ne scalent pas. Quand votre site grossit, retester manuellement toutes les fonctionnalités à chaque déploiement devient impossible. Cypress est la solution E2E (end-to-end) qui simule les actions d’un utilisateur réel dans le navigateur — clicks, formulaires, navigation, assertions — de façon reproductible et intégrable dans votre CI/CD.

Pourquoi Cypress en 2025

Cypress n’est pas le seul outil E2E (Playwright, Selenium existent), mais il a trois avantages décisifs pour les équipes web :

  • Exécution dans le navigateur : les tests s’exécutent dans le même contexte que l’application — pas de problème de synchronisation réseau ou de timing
  • Time-travel debugging : chaque étape est capturée en screenshot, vous pouvez revenir en arrière et voir l’état du DOM à n’importe quel moment
  • Developer experience : syntaxe claire, rechargement à chaud, pas de configuration complexe pour démarrer

Installation et configuration

# Dans votre projet existant
npm install cypress --save-dev

# Premier lancement (ouvre l'interface Cypress)
npx cypress open

# Ou en mode headless (pour CI)
npx cypress run

Cypress génère automatiquement la structure :

cypress/
├── e2e/              # Vos fichiers de test
├── fixtures/         # Données de test statiques (JSON)
├── support/
│   ├── commands.ts   # Commandes custom réutilisables
│   └── e2e.ts        # Configuration globale (hooks, imports)
cypress.config.ts     # Configuration principale

Configuration de base (cypress.config.ts) :

import { defineConfig } from "cypress";

export default defineConfig({
    e2e: {
        baseUrl: "http://localhost:4321", // URL de votre app en dev
        viewportWidth: 1440,
        viewportHeight: 900,
        
        // Screenshots automatiques en cas d'échec
        screenshotOnRunFailure: true,
        
        // Vidéo de l'exécution
        video: true,
        videosFolder: "cypress/videos",
        
        // Timeout par défaut (4s)
        defaultCommandTimeout: 6000,
        
        setupNodeEvents(on, config) {
            // Plugins Node.js si nécessaire
        }
    }
});

Premier test : navigation et contenu

// cypress/e2e/accueil.cy.ts
describe("Page d'accueil", () => {
    beforeEach(() => {
        cy.visit("/"); // baseUrl + "/"
    });

    it("affiche le titre principal et le texte d'accroche", () => {
        cy.get("h1").should("be.visible").and("contain.text", "Amine MEGDICHE");
        cy.get("[data-testid='hero-tagline']").should("be.visible");
    });

    it("la navigation principale contient les liens attendus", () => {
        const liens = ["Services", "Réalisations", "Blog", "Contact"];
        liens.forEach(lien => {
            cy.get("nav").contains(lien).should("be.visible");
        });
    });

    it("le badge de disponibilité est visible", () => {
        cy.contains("Disponible au forfait").should("be.visible");
    });

    it("le bouton CTA principal redirige vers le contact", () => {
        cy.get("[data-testid='hero-cta']").click();
        cy.url().should("include", "/contact");
    });
});

Attribut data-testid : le sélecteur recommandé

Ne ciblez jamais les classes CSS (cy.get(".btn-primary")), les IDs de style (cy.get("#header-nav")), ou les textes (cy.contains("Connexion")). Ils changent souvent et rendent les tests fragiles.

<!-- Dans votre HTML/Astro -->
<a href="/contact" data-testid="hero-cta" class="btn btn-primary">
    Me contacter
</a>
// Dans vos tests
cy.get("[data-testid='hero-cta']").click(); // ✅ Stable
cy.get(".btn-primary").click();              // ❌ Fragile (classe peut changer)

Tests de formulaire

Le formulaire de contact est le flux le plus critique d’un site vitrine :

// cypress/e2e/contact.cy.ts
describe("Formulaire de contact", () => {
    beforeEach(() => {
        cy.visit("/contact");
    });

    it("valide les champs obligatoires", () => {
        // Soumettre sans rien remplir
        cy.get("[data-testid='contact-form']").submit();
        
        // Les messages d'erreur doivent apparaître
        cy.get("[data-testid='error-nom']")
            .should("be.visible")
            .and("contain.text", "obligatoire");
        cy.get("[data-testid='error-email']")
            .should("be.visible");
    });

    it("valide le format de l'email", () => {
        cy.get("[data-testid='input-email']").type("ceci-nest-pas-un-email");
        cy.get("[data-testid='contact-form']").submit();
        cy.get("[data-testid='error-email']")
            .should("contain.text", "email valide");
    });

    it("soumet le formulaire avec succès", () => {
        // Intercepter la requête réseau pour ne pas vraiment envoyer l'email
        cy.intercept("POST", "https://formsubmit.co/*", {
            statusCode: 200,
            body: { message: "ok" }
        }).as("formSubmit");

        cy.get("[data-testid='input-nom']").type("Test Utilisateur");
        cy.get("[data-testid='input-email']").type("test@example.com");
        cy.get("[data-testid='input-message']").type("Ceci est un message de test.");
        
        cy.get("[data-testid='contact-form']").submit();
        
        cy.wait("@formSubmit");
        
        // Message de confirmation affiché
        cy.get("[data-testid='success-message']")
            .should("be.visible")
            .and("contain.text", "Message envoyé");
    });
});

Intercept réseau : tester sans vraiment appeler les APIs

cy.intercept() est l’une des fonctionnalités les plus puissantes de Cypress. Elle permet de simuler les réponses API sans toucher aux vraies APIs :

// Simuler une API lente
cy.intercept("GET", "/api/produits", (req) => {
    req.on("response", (res) => {
        res.setDelay(3000); // 3 secondes de délai simulé
    });
}).as("lenteProduits");

// Vérifier que le loader s'affiche pendant le chargement
cy.get("[data-testid='produits-loader']").should("be.visible");
cy.wait("@lenteProduits");
cy.get("[data-testid='produits-loader']").should("not.exist");
// Simuler une erreur 500
cy.intercept("POST", "/api/commandes", {
    statusCode: 500,
    body: { error: "Erreur interne" }
}).as("commandeEchouee");

cy.get("[data-testid='btn-commander']").click();
cy.wait("@commandeEchouee");

// L'application doit afficher un message d'erreur, pas planter
cy.get("[data-testid='error-banner']")
    .should("be.visible")
    .and("contain.text", "Une erreur est survenue");

Fixtures : données de test réutilisables

// cypress/fixtures/commande.json
{
    "id": "cmd-123",
    "numero": "CMD-2025-0042",
    "statut": "LIVREE",
    "total": 349.90,
    "client": {
        "nom": "Dupont",
        "email": "jean.dupont@example.com"
    },
    "lignes": [
        { "produit": "Article A", "quantite": 2, "prix": 99.95 },
        { "produit": "Article B", "quantite": 1, "prix": 150.00 }
    ]
}
// Utilisation dans un test
cy.fixture("commande").then((commande) => {
    cy.intercept("GET", `/api/commandes/${commande.id}`, commande).as("getCommande");
    
    cy.visit(`/commandes/${commande.id}`);
    cy.wait("@getCommande");
    
    cy.get("[data-testid='commande-numero']").should("contain", commande.numero);
    cy.get("[data-testid='commande-total']").should("contain", "349,90");
});

Commandes custom : éviter la répétition

// cypress/support/commands.ts
declare global {
    namespace Cypress {
        interface Chainable {
            login(email?: string, password?: string): Chainable<void>;
            remplirFormContact(nom: string, email: string, message: string): Chainable<void>;
        }
    }
}

Cypress.Commands.add("login", (
    email = "admin@example.com",
    password = "TestPassword123!"
) => {
    cy.session([email, password], () => {
        cy.request("POST", "/api/auth/login", { email, password })
            .then(response => {
                localStorage.setItem("access_token", response.body.token);
            });
    });
});

Cypress.Commands.add("remplirFormContact", (nom, email, message) => {
    cy.get("[data-testid='input-nom']").clear().type(nom);
    cy.get("[data-testid='input-email']").clear().type(email);
    cy.get("[data-testid='input-message']").clear().type(message);
});
// Utilisation dans les tests
it("un utilisateur connecté peut accéder au dashboard", () => {
    cy.login(); // Réutilise la session (performant)
    cy.visit("/dashboard");
    cy.get("[data-testid='dashboard-title']").should("be.visible");
});

Tests responsive : mobile et desktop

// cypress/e2e/responsive.cy.ts
const viewports = [
    { device: "Mobile", width: 375, height: 812 },
    { device: "Tablet", width: 768, height: 1024 },
    { device: "Desktop", width: 1440, height: 900 }
];

viewports.forEach(({ device, width, height }) => {
    describe(`Navigation - ${device}`, () => {
        beforeEach(() => {
            cy.viewport(width, height);
            cy.visit("/");
        });

        it("affiche le menu correctement", () => {
            if (width < 768) {
                // Mobile : le menu hamburger doit être visible
                cy.get("[data-testid='menu-burger']").should("be.visible");
                cy.get("[data-testid='nav-main']").should("not.be.visible");
                
                cy.get("[data-testid='menu-burger']").click();
                cy.get("[data-testid='nav-main']").should("be.visible");
            } else {
                // Desktop : la nav principale est directement visible
                cy.get("[data-testid='nav-main']").should("be.visible");
                cy.get("[data-testid='menu-burger']").should("not.exist");
            }
        });
    });
});

Intégration CI/CD (GitHub Actions)

# .github/workflows/cypress.yml
name: Tests E2E Cypress

on:
    push:
        branches: [main, develop]
    pull_request:
        branches: [main]

jobs:
    cypress:
        runs-on: ubuntu-latest
        steps:
            - uses: actions/checkout@v4
            
            - name: Setup Node.js
              uses: actions/setup-node@v4
              with:
                  node-version: "20"
                  cache: "npm"
            
            - name: Install dependencies
              run: npm ci
            
            - name: Build application
              run: npm run build
            
            - name: Start preview server
              run: npm run preview &
              env:
                  PORT: 4321
            
            - name: Wait for server
              run: npx wait-on http://localhost:4321 --timeout 30000
            
            - name: Run Cypress tests
              uses: cypress-io/github-action@v6
              with:
                  browser: chrome
                  record: true  # Enregistrement sur Cypress Cloud (optionnel)
              env:
                  CYPRESS_baseUrl: http://localhost:4321
                  CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
            
            - name: Upload screenshots on failure
              uses: actions/upload-artifact@v4
              if: failure()
              with:
                  name: cypress-screenshots
                  path: cypress/screenshots

Structure recommandée pour un site vitrine

cypress/e2e/
├── accueil.cy.ts          # Page d'accueil : hero, CTA, navigation
├── services.cy.ts         # Pages services : contenu, liens
├── contact.cy.ts          # Formulaire : validation, soumission
├── blog.cy.ts             # Blog : liste, filtres, lecture article
├── navigation.cy.ts       # Navigation globale, breadcrumbs, footer
├── accessibilite.cy.ts    # Focus clavier, aria, contraste
└── responsive.cy.ts       # Mobile, tablette, desktop

Bonnes pratiques

Tester le comportement, pas l’implémentation

// ✅ Ce que l'utilisateur voit
cy.get("[data-testid='panier-count']").should("have.text", "3");

// ❌ L'état interne du store React
cy.window().its("__reactStore.panier.items.length").should("equal", 3);

Un test = un scénario utilisateur Chaque test it() doit couvrir un seul flux utilisateur, de A à Z. Évitez les tests qui couvrent 10 actions différentes — ils sont difficiles à déboguer quand ils échouent.

Nettoyer l’état entre les tests

beforeEach(() => {
    cy.clearLocalStorage();
    cy.clearCookies();
});

Ne pas tester ce qui n’a pas de valeur Tester que la couleur d’un bouton est “#0057d9” n’a aucune valeur. Tester que le bouton est cliquable et déclenche l’action attendue : oui.

Résultats attendus

Sur un site vitrine de 10-15 pages avec Cypress bien configuré :

  • Suite complète : ~150 tests, exécution en 4-6 minutes
  • Couverture : 100% des flux critiques (navigation, formulaires, liens)
  • En CI : exécution à chaque push, échec visible en 6 minutes
  • Maintenance : 1-2 heures par mois si le site évolue

L’investissement initial (3-4 jours pour une suite complète) se rentabilise rapidement : chaque bug de régression détecté avant la mise en production vaut plusieurs heures de débogage en prod.

Amine MEGDICHE

Amine MEGDICHE

Développeur AEM & Java Full Stack — Freelance depuis 2013

Vous avez un projet sur ces sujets ?

Envoyez-moi un message pour qualifier votre besoin, vos contraintes techniques et le bon format d'intervention.

Cadrer mon besoin