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.
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
Développeur AEM & Java Full Stack — Freelance depuis 2013