Aller au contenu

Feuille de chaleur Cyprès

Cypress - Essais de bout en bout faciles

Cypress est un outil de test avant de nouvelle génération construit pour le web moderne. Il s'adresse aux développeurs de points de douleur clés et aux ingénieurs QA face lors des tests d'applications modernes. Cypress vous permet d'écrire tous les types de tests: Essais de bout en bout, Essais d'intégration, Essais unitaires.

Copier toutes les commandes Générer PDF

Sommaire

  • [Installation] (LINK_0)
  • [Pour commencer] (LINK_0)
  • [Commandes de base] (LINK_0)
  • [Sélectionneurs] (LINK_0)
  • [Assertions] (LINK_0)
  • [Essais de réseau] (LINK_0)
  • [Fichier des opérations] (LINK_0)
  • [Commandes personnalisées] (LINK_0)
  • [Configuration] (LINK_0)
  • [Objets de la page] (LINK_0)
  • [Essais API] (LINK_0)
  • [Essais visuels] (LINK_0)
  • [Intégration IC/CD] (LINK_0)
  • [Meilleures pratiques] (LINK_0)
  • [Débogage] (LINK_0)
  • [Performance] (LINK_0)
  • [Dépannage] (LINK_0)

Installation

Installation de base

# Install Cypress via npm
npm install --save-dev cypress

# Install Cypress via yarn
yarn add --dev cypress

# Open Cypress for the first time
npx cypress open

# Run Cypress in headless mode
npx cypress run

Configuration du projet

# Initialize new project with Cypress
mkdir my-cypress-project
cd my-cypress-project
npm init -y
npm install --save-dev cypress

# Open Cypress (creates folder structure)
npx cypress open
```_

### Structure du dossier
cypress/ ├── e2e/ # End-to-end test files │ └── spec.cy.js ├── fixtures/ # Test data │ └── example.json ├── support/ # Support files │ ├── commands.js # Custom commands │ └── e2e.js # Global configuration └── downloads/ # Downloaded files during tests
### Paquet.json Scripts
```json
{
  "scripts": {
    "cypress:open": "cypress open",
    "cypress:run": "cypress run",
    "cypress:run:chrome": "cypress run --browser chrome",
    "cypress:run:firefox": "cypress run --browser firefox",
    "cypress:run:edge": "cypress run --browser edge",
    "cypress:run:headed": "cypress run --headed",
    "cypress:run:record": "cypress run --record --key=your-record-key"
  }
}

Commencer

Premier essai

// cypress/e2e/first-test.cy.js
describe('My First Test', () => {
  it('Visits the Kitchen Sink', () => {
    cy.visit('https://example.cypress.io')
    cy.contains('type').click()
    cy.url().should('include', '/commands/actions')
    cy.get('.action-email')
      .type('fake@email.com')
      .should('have.value', 'fake@email.com')
  })
})

Structure d'essai de base

describe('Test Suite Name', () => {
  beforeEach(() => {
    // Runs before each test
    cy.visit('/login')
  })

  it('should do something', () => {
    // Test implementation
    cy.get('[data-cy=username]').type('user@example.com')
    cy.get('[data-cy=password]').type('password123')
    cy.get('[data-cy=submit]').click()
    cy.url().should('include', '/dashboard')
  })

  it('should do something else', () => {
    // Another test
  })
})

Crochets d'essai

describe('Hooks Example', () => {
  before(() => {
    // Runs once before all tests in the block
    cy.task('seedDatabase')
  })

  after(() => {
    // Runs once after all tests in the block
    cy.task('clearDatabase')
  })

  beforeEach(() => {
    // Runs before each test in the block
    cy.login('user@example.com', 'password123')
  })

  afterEach(() => {
    // Runs after each test in the block
    cy.clearCookies()
    cy.clearLocalStorage()
  })

  it('should test something', () => {
    // Test implementation
  })
})

Commandes de base

// Visit a URL
cy.visit('https://example.com')
cy.visit('/relative-path')
cy.visit('/', { timeout: 30000 })

// Navigate browser history
cy.go('back')
cy.go('forward')
cy.go(-1) // Go back one page
cy.go(1)  // Go forward one page

// Reload page
cy.reload()
cy.reload(true) // Force reload

Interaction des éléments

// Click elements
cy.get('button').click()
cy.get('button').click({ force: true }) // Force click
cy.get('button').dblclick() // Double click
cy.get('button').rightclick() // Right click

// Type text
cy.get('input').type('Hello World')
cy.get('input').type('user@example.com{enter}')
cy.get('input').type('{selectall}New Text')
cy.get('input').clear().type('New Text')

// Select options
cy.get('select').select('Option 1')
cy.get('select').select(['Option 1', 'Option 2']) // Multiple
cy.get('select').select('value', { force: true })

// Check/uncheck
cy.get('input[type="checkbox"]').check()
cy.get('input[type="checkbox"]').uncheck()
cy.get('input[type="radio"]').check()

// File upload
cy.get('input[type="file"]').selectFile('path/to/file.pdf')
cy.get('input[type="file"]').selectFile(['file1.jpg', 'file2.jpg'])

Renseignements sur les éléments

// Get elements
cy.get('.class-name')
cy.get('#id')
cy.get('[data-cy="element"]')
cy.get('button').first()
cy.get('button').last()
cy.get('button').eq(2) // Third button (0-indexed)

// Find within elements
cy.get('.parent').find('.child')
cy.get('.parent').within(() => {
  cy.get('.child').click()
})

// Filter elements
cy.get('button').filter('.active')
cy.get('button').not('.disabled')

// Contains text
cy.contains('Submit')
cy.contains('button', 'Submit')
cy.get('button').contains('Submit')

Traverse

// Parent/child relationships
cy.get('.child').parent()
cy.get('.element').parents('.ancestor')
cy.get('.parent').children()
cy.get('.parent').children('.specific-child')

// Siblings
cy.get('.element').siblings()
cy.get('.element').next()
cy.get('.element').prev()
cy.get('.element').nextAll()
cy.get('.element').prevAll()

// Closest
cy.get('.element').closest('.container')

Attendre

// Wait for element
cy.get('.loading').should('not.exist')
cy.get('.content').should('be.visible')

// Wait for specific time (not recommended)
cy.wait(1000)

// Wait for network requests
cy.intercept('GET', '/api/users').as('getUsers')
cy.wait('@getUsers')

// Wait for multiple requests
cy.wait(['@getUsers', '@getPosts'])

Sélecteurs

Sélecteurs CSS

// Basic selectors
cy.get('button')           // Tag
cy.get('.btn')            // Class
cy.get('#submit')         // ID
cy.get('[type="submit"]') // Attribute

// Combinators
cy.get('form button')     // Descendant
cy.get('form > button')   // Direct child
cy.get('label + input')   // Adjacent sibling
cy.get('h2 ~ p')         // General sibling

// Pseudo-selectors
cy.get('button:first')
cy.get('button:last')
cy.get('button:nth-child(2)')
cy.get('input:checked')
cy.get('input:disabled')
cy.get('a:contains("Click me")')

Attributs de données (Recommandé)

// HTML
// <button data-cy="submit-btn">Submit</button>
// <input data-testid="username" />
// <div data-test="user-profile">...</div>

// Cypress tests
cy.get('[data-cy="submit-btn"]')
cy.get('[data-testid="username"]')
cy.get('[data-test="user-profile"]')

// Custom command for data-cy
Cypress.Commands.add('dataCy', (value) => {
  return cy.get(`[data-cy=${value}]`)
})

// Usage
cy.dataCy('submit-btn').click()

XPath (avec plugin)

# Install cypress-xpath plugin
npm install --save-dev cypress-xpath
// cypress/support/e2e.js
require('cypress-xpath')

// Usage in tests
cy.xpath('//button[contains(text(), "Submit")]')
cy.xpath('//input[@placeholder="Username"]')
cy.xpath('//div[@class="container"]//button[1]')

Sélecteurs complexes

// Multiple attributes
cy.get('input[type="email"][required]')

// Partial attribute matching
cy.get('[class*="btn"]')      // Contains
cy.get('[class^="btn"]')      // Starts with
cy.get('[class$="primary"]')  // Ends with

// Multiple selectors
cy.get('button, input[type="submit"]')

// Chaining selectors
cy.get('.form-group')
  .find('input')
  .filter('[required]')
  .first()

Hypothèses

Doivent être considérées comme

// Existence
cy.get('button').should('exist')
cy.get('.loading').should('not.exist')

// Visibility
cy.get('button').should('be.visible')
cy.get('.modal').should('not.be.visible')

// Text content
cy.get('h1').should('contain', 'Welcome')
cy.get('h1').should('have.text', 'Welcome to our site')
cy.get('p').should('include.text', 'partial text')

// Attributes
cy.get('input').should('have.attr', 'placeholder', 'Enter email')
cy.get('button').should('have.class', 'btn-primary')
cy.get('input').should('have.value', 'test@example.com')

// CSS properties
cy.get('button').should('have.css', 'background-color', 'rgb(0, 123, 255)')
cy.get('div').should('have.css', 'display', 'none')

// Length
cy.get('li').should('have.length', 5)
cy.get('li').should('have.length.greaterThan', 3)
cy.get('li').should('have.length.lessThan', 10)

Hypothèses attendues

// Using expect (Chai)
cy.get('input').then(($input) => {
  expect($input).to.have.value('expected value')
  expect($input[0].validity.valid).to.be.true
})

// Multiple assertions
cy.get('button').then(($btn) => {
  expect($btn).to.have.class('btn')
  expect($btn).to.contain('Submit')
  expect($btn).to.be.visible
})

// Custom assertions
cy.get('.user-list').then(($list) => {
  const users = $list.find('.user-item')
  expect(users).to.have.length.above(0)
  expect(users.first()).to.contain('John Doe')
})

Options d'URL

// URL should include
cy.url().should('include', '/dashboard')
cy.url().should('eq', 'https://example.com/dashboard')

// URL should match pattern
cy.url().should('match', /\/users\/\d+/)

// Location assertions
cy.location('pathname').should('eq', '/dashboard')
cy.location('search').should('eq', '?tab=profile')
cy.location('hash').should('eq', '#section1')

Assertions personnalisées

// Add custom assertion
chai.Assertion.addMethod('beValidEmail', function() {
  const obj = this._obj
  const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/

  this.assert(
    emailRegex.test(obj),
    'expected #{this} to be a valid email',
    'expected #{this} not to be a valid email',
    true
  )
})

// Usage
cy.get('input[type="email"]')
  .invoke('val')
  .should('beValidEmail')

Essais en réseau

Demandes d'interception

// Basic intercept
cy.intercept('GET', '/api/users').as('getUsers')
cy.visit('/users')
cy.wait('@getUsers')

// Intercept with response modification
cy.intercept('GET', '/api/users', { fixture: 'users.json' }).as('getUsers')

// Intercept with dynamic response
cy.intercept('GET', '/api/users', (req) => {
  req.reply({
    statusCode: 200,
    body: {
      users: [
        { id: 1, name: 'John Doe' },
        { id: 2, name: 'Jane Smith' }
      ]
    }
  })
}).as('getUsers')

// Intercept POST requests
cy.intercept('POST', '/api/users', {
  statusCode: 201,
  body: { id: 3, name: 'New User' }
}).as('createUser')

Demande d'assertions

// Wait and assert request
cy.intercept('GET', '/api/users').as('getUsers')
cy.visit('/users')
cy.wait('@getUsers').then((interception) => {
  expect(interception.response.statusCode).to.eq(200)
  expect(interception.response.body).to.have.property('users')
})

// Assert request was made
cy.intercept('POST', '/api/users').as('createUser')
cy.get('[data-cy="create-user"]').click()
cy.wait('@createUser').its('request.body').should('deep.include', {
  name: 'John Doe',
  email: 'john@example.com'
})

// Assert request headers
cy.wait('@getUsers').its('request.headers').should('have.property', 'authorization')

Conditions du réseau

// Simulate slow network
cy.intercept('GET', '/api/users', (req) => {
  req.reply((res) => {
    res.delay(2000) // 2 second delay
    res.send({ fixture: 'users.json' })
  })
}).as('getUsers')

// Simulate network failure
cy.intercept('GET', '/api/users', { forceNetworkError: true }).as('getUsersError')

// Simulate server error
cy.intercept('GET', '/api/users', {
  statusCode: 500,
  body: { error: 'Internal Server Error' }
}).as('getUsersError')

Interceptes multiples

// Multiple endpoints
cy.intercept('GET', '/api/users').as('getUsers')
cy.intercept('GET', '/api/posts').as('getPosts')
cy.intercept('GET', '/api/comments').as('getComments')

cy.visit('/dashboard')

// Wait for all requests
cy.wait(['@getUsers', '@getPosts', '@getComments'])

// Or wait individually
cy.wait('@getUsers')
cy.wait('@getPosts')
cy.wait('@getComments')

Opérations de fichiers

Téléchargement de fichier

// Upload single file
cy.get('input[type="file"]').selectFile('cypress/fixtures/example.pdf')

// Upload multiple files
cy.get('input[type="file"]').selectFile([
  'cypress/fixtures/file1.jpg',
  'cypress/fixtures/file2.jpg'
])

// Upload with drag and drop
cy.get('.dropzone').selectFile('cypress/fixtures/example.pdf', {
  action: 'drag-drop'
})

// Upload from fixtures
cy.fixture('example.pdf', 'base64').then(fileContent => {
  cy.get('input[type="file"]').selectFile({
    contents: Cypress.Buffer.from(fileContent, 'base64'),
    fileName: 'example.pdf',
    mimeType: 'application/pdf'
  })
})

Téléchargement de fichier

// Download file and verify
cy.get('[data-cy="download-btn"]').click()
cy.readFile('cypress/downloads/report.pdf').should('exist')

// Verify downloaded file content
cy.get('[data-cy="download-csv"]').click()
cy.readFile('cypress/downloads/data.csv')
  .should('contain', 'Name,Email,Age')
  .and('contain', 'John Doe,john@example.com,30')

// Download with custom filename
cy.get('[data-cy="download-btn"]').click()
cy.task('downloadFile', {
  url: 'https://example.com/file.pdf',
  directory: 'cypress/downloads',
  filename: 'custom-name.pdf'
})

Travailler avec des installations

// Load fixture data
cy.fixture('users.json').then((users) => {
  cy.intercept('GET', '/api/users', { body: users }).as('getUsers')
})

// Use fixture in test
cy.fixture('user-data.json').as('userData')
cy.get('@userData').then((data) => {
  cy.get('[data-cy="name"]').type(data.name)
  cy.get('[data-cy="email"]').type(data.email)
})

// Fixture with alias
cy.fixture('config.json').as('config')
cy.get('@config').its('apiUrl').should('eq', 'https://api.example.com')

Lecture/écriture de fichiers

// Read file
cy.readFile('cypress/fixtures/data.json').then((data) => {
  expect(data).to.have.property('users')
})

// Write file
cy.writeFile('cypress/fixtures/output.json', {
  timestamp: Date.now(),
  testResults: 'passed'
})

// Append to file
cy.writeFile('cypress/fixtures/log.txt', 'New log entry\n', { flag: 'a+' })

Commandes personnalisées

Commandes personnalisées de base

// cypress/support/commands.js

// Simple command
Cypress.Commands.add('login', (email, password) => {
  cy.visit('/login')
  cy.get('[data-cy="email"]').type(email)
  cy.get('[data-cy="password"]').type(password)
  cy.get('[data-cy="submit"]').click()
})

// Command with options
Cypress.Commands.add('login', (email, password, options = {}) => {
  const { rememberMe = false } = options

  cy.visit('/login')
  cy.get('[data-cy="email"]').type(email)
  cy.get('[data-cy="password"]').type(password)

  if (rememberMe) {
    cy.get('[data-cy="remember-me"]').check()
  }

  cy.get('[data-cy="submit"]').click()
})

// Usage
cy.login('user@example.com', 'password123')
cy.login('user@example.com', 'password123', { rememberMe: true })

Commandes personnalisées avancées

// Command that returns value
Cypress.Commands.add('getLocalStorage', (key) => {
  return cy.window().then((win) => {
    return win.localStorage.getItem(key)
  })
})

// Command with chaining
Cypress.Commands.add('dataCy', (value) => {
  return cy.get(`[data-cy=${value}]`)
})

// Usage with chaining
cy.dataCy('submit-btn').click()
cy.dataCy('username').type('john@example.com')

// Command with retry logic
Cypress.Commands.add('waitForElement', (selector, timeout = 10000) => {
  cy.get(selector, { timeout }).should('be.visible')
})

// API command
Cypress.Commands.add('apiLogin', (email, password) => {
  cy.request({
    method: 'POST',
    url: '/api/auth/login',
    body: { email, password }
  }).then((response) => {
    window.localStorage.setItem('authToken', response.body.token)
  })
})

Commandes d'écrasement

// Overwrite existing command
Cypress.Commands.overwrite('visit', (originalFn, url, options) => {
  const domain = Cypress.config('baseUrl')

  if (domain) {
    const combinedUrl = `${domain}${url}`
    return originalFn(combinedUrl, options)
  }

  return originalFn(url, options)
})

// Overwrite type command to clear first
Cypress.Commands.overwrite('type', (originalFn, element, text, options) => {
  if (options && options.clearFirst !== false) {
    element.clear()
  }

  return originalFn(element, text, options)
})

Support de script de type

// cypress/support/commands.ts
declare global {
  namespace Cypress {
    interface Chainable {
      login(email: string, password: string, options?: { rememberMe?: boolean }): Chainable<void>
      dataCy(value: string): Chainable<JQuery<HTMLElement>>
      apiLogin(email: string, password: string): Chainable<void>
    }
  }
}

Cypress.Commands.add('login', (email: string, password: string, options = {}) => {
  // Implementation
})

Configuration

Configuration de la cyprès

// cypress.config.js
const { defineConfig } = require('cypress')

module.exports = defineConfig({
  e2e: {
    // Base URL for cy.visit() and cy.request()
    baseUrl: 'http://localhost:3000',

    // Viewport settings
    viewportWidth: 1280,
    viewportHeight: 720,

    // Timeouts
    defaultCommandTimeout: 10000,
    requestTimeout: 10000,
    responseTimeout: 30000,
    pageLoadTimeout: 30000,

    // Test files
    specPattern: 'cypress/e2e/**/*.cy.{js,jsx,ts,tsx}',
    excludeSpecPattern: ['**/examples/*', '**/temp/*'],

    // Support file
    supportFile: 'cypress/support/e2e.js',

    // Fixtures
    fixturesFolder: 'cypress/fixtures',

    // Screenshots and videos
    screenshotsFolder: 'cypress/screenshots',
    videosFolder: 'cypress/videos',
    video: true,
    screenshotOnRunFailure: true,

    // Browser settings
    chromeWebSecurity: false,

    // Setup function
    setupNodeEvents(on, config) {
      // implement node event listeners here
    },
  },

  // Component testing
  component: {
    devServer: {
      framework: 'react',
      bundler: 'webpack',
    },
  },
})

Variables d'environnement

// cypress.config.js
module.exports = defineConfig({
  e2e: {
    env: {
      apiUrl: 'https://api.example.com',
      username: 'testuser',
      password: 'testpass'
    }
  }
})

// cypress.env.json
{
  "apiUrl": "https://api.example.com",
  "username": "testuser",
  "password": "testpass"
}

// Usage in tests
cy.visit(Cypress.env('apiUrl'))
const username = Cypress.env('username')

Environnements multiples

// cypress.config.js
module.exports = defineConfig({
  e2e: {
    setupNodeEvents(on, config) {
|  |  | const environment = config.env.environment |  | 'development' |  |  |

      const environments = {
        development: {
          baseUrl: 'http://localhost:3000',
          apiUrl: 'http://localhost:3001'
        },
        staging: {
          baseUrl: 'https://staging.example.com',
          apiUrl: 'https://api-staging.example.com'
        },
        production: {
          baseUrl: 'https://example.com',
          apiUrl: 'https://api.example.com'
        }
      }

      config.baseUrl = environments[environment].baseUrl
      config.env.apiUrl = environments[environment].apiUrl

      return config
    }
  }
})

// Run with specific environment
// npx cypress run --env environment=staging

Configuration du navigateur

// cypress.config.js
module.exports = defineConfig({
  e2e: {
    // Browser launch options
    setupNodeEvents(on, config) {
      on('before:browser:launch', (browser = {}, launchOptions) => {
        if (browser.name === 'chrome') {
          launchOptions.args.push('--disable-dev-shm-usage')
          launchOptions.args.push('--no-sandbox')
          launchOptions.args.push('--disable-gpu')
        }

        if (browser.name === 'firefox') {
          launchOptions.preferences['media.navigator.permission.disabled'] = true
        }

        return launchOptions
      })
    }
  }
})

Page Objets

Objet de page de base

// cypress/support/pages/LoginPage.js
class LoginPage {
  visit() {
    cy.visit('/login')
    return this
  }

  fillEmail(email) {
    cy.get('[data-cy="email"]').type(email)
    return this
  }

  fillPassword(password) {
    cy.get('[data-cy="password"]').type(password)
    return this
  }

  submit() {
    cy.get('[data-cy="submit"]').click()
    return this
  }

  login(email, password) {
    this.fillEmail(email)
    this.fillPassword(password)
    this.submit()
    return this
  }

  // Getters for elements
  get emailInput() {
    return cy.get('[data-cy="email"]')
  }

  get passwordInput() {
    return cy.get('[data-cy="password"]')
  }

  get submitButton() {
    return cy.get('[data-cy="submit"]')
  }

  get errorMessage() {
    return cy.get('[data-cy="error-message"]')
  }
}

export default LoginPage

Utilisation des objets de page

// cypress/e2e/login.cy.js
import LoginPage from '../support/pages/LoginPage'

describe('Login Tests', () => {
  const loginPage = new LoginPage()

  beforeEach(() => {
    loginPage.visit()
  })

  it('should login successfully', () => {
    loginPage
      .login('user@example.com', 'password123')

    cy.url().should('include', '/dashboard')
  })

  it('should show error for invalid credentials', () => {
    loginPage
      .login('invalid@email.com', 'wrongpassword')

    loginPage.errorMessage
      .should('be.visible')
      .and('contain', 'Invalid credentials')
  })
})

Objet de page avancé

// cypress/support/pages/DashboardPage.js
class DashboardPage {
  constructor() {
    this.selectors = {
      userMenu: '[data-cy="user-menu"]',
      userProfile: '[data-cy="user-profile"]',
      notifications: '[data-cy="notifications"]',
      searchInput: '[data-cy="search"]',
      sidebar: '[data-cy="sidebar"]',
      mainContent: '[data-cy="main-content"]'
    }
  }

  visit() {
    cy.visit('/dashboard')
    this.waitForLoad()
    return this
  }

  waitForLoad() {
    cy.get(this.selectors.mainContent).should('be.visible')
    cy.get('.loading-spinner').should('not.exist')
    return this
  }

  openUserMenu() {
    cy.get(this.selectors.userMenu).click()
    return this
  }

  search(query) {
    cy.get(this.selectors.searchInput)
      .clear()
      .type(`${query}{enter}`)
    return this
  }

  // Navigation methods
  navigateToProfile() {
    this.openUserMenu()
    cy.get(this.selectors.userProfile).click()
    return this
  }

  // Assertion methods
  shouldShowWelcomeMessage(username) {
    cy.contains(`Welcome, ${username}`).should('be.visible')
    return this
  }

  shouldHaveNotificationCount(count) {
    cy.get(this.selectors.notifications)
      .find('.badge')
      .should('contain', count)
    return this
  }
}

export default DashboardPage

Objet de page avec intégration d'API

// cypress/support/pages/UsersPage.js
class UsersPage {
  visit() {
    cy.intercept('GET', '/api/users').as('getUsers')
    cy.visit('/users')
    cy.wait('@getUsers')
    return this
  }

  createUser(userData) {
    cy.intercept('POST', '/api/users').as('createUser')

    cy.get('[data-cy="create-user-btn"]').click()
    cy.get('[data-cy="name"]').type(userData.name)
    cy.get('[data-cy="email"]').type(userData.email)
    cy.get('[data-cy="save"]').click()

    cy.wait('@createUser')
    return this
  }

  deleteUser(userId) {
    cy.intercept('DELETE', `/api/users/${userId}`).as('deleteUser')

    cy.get(`[data-cy="user-${userId}"]`)
      .find('[data-cy="delete-btn"]')
      .click()

    cy.get('[data-cy="confirm-delete"]').click()
    cy.wait('@deleteUser')
    return this
  }

  shouldShowUser(userData) {
    cy.get('[data-cy="users-table"]')
      .should('contain', userData.name)
      .and('contain', userData.email)
    return this
  }
}

export default UsersPage

Essai de l'API

Essais d'API de base

describe('API Tests', () => {
  it('should get users', () => {
    cy.request('GET', '/api/users')
      .then((response) => {
        expect(response.status).to.eq(200)
        expect(response.body).to.have.property('users')
        expect(response.body.users).to.be.an('array')
      })
  })

  it('should create user', () => {
    const newUser = {
      name: 'John Doe',
      email: 'john@example.com'
    }

    cy.request('POST', '/api/users', newUser)
      .then((response) => {
        expect(response.status).to.eq(201)
        expect(response.body).to.have.property('id')
        expect(response.body.name).to.eq(newUser.name)
        expect(response.body.email).to.eq(newUser.email)
      })
  })
})

Tests API avec authentification

describe('Authenticated API Tests', () => {
  let authToken

  before(() => {
    // Login and get token
    cy.request({
      method: 'POST',
      url: '/api/auth/login',
      body: {
        email: 'admin@example.com',
        password: 'password123'
      }
    }).then((response) => {
      authToken = response.body.token
    })
  })

  it('should get protected resource', () => {
    cy.request({
      method: 'GET',
      url: '/api/admin/users',
      headers: {
        'Authorization': `Bearer ${authToken}`
      }
    }).then((response) => {
      expect(response.status).to.eq(200)
      expect(response.body).to.have.property('users')
    })
  })

  it('should fail without token', () => {
    cy.request({
      method: 'GET',
      url: '/api/admin/users',
      failOnStatusCode: false
    }).then((response) => {
      expect(response.status).to.eq(401)
    })
  })
})

Assistants de test API

// cypress/support/api-helpers.js
Cypress.Commands.add('apiLogin', (email, password) => {
  return cy.request({
    method: 'POST',
    url: '/api/auth/login',
    body: { email, password }
  }).then((response) => {
    window.localStorage.setItem('authToken', response.body.token)
    return response.body.token
  })
})

Cypress.Commands.add('apiRequest', (method, url, body = null) => {
  const token = window.localStorage.getItem('authToken')

  return cy.request({
    method,
    url,
    body,
    headers: token ? { 'Authorization': `Bearer ${token}` } : {}
  })
})

// Usage
cy.apiLogin('admin@example.com', 'password123')
cy.apiRequest('GET', '/api/users')
cy.apiRequest('POST', '/api/users', { name: 'John', email: 'john@example.com' })

Validation du schéma

// Install ajv for JSON schema validation
// npm install --save-dev ajv

// cypress/support/schema-validation.js
import Ajv from 'ajv'

const ajv = new Ajv()

Cypress.Commands.add('validateSchema', (data, schema) => {
  const validate = ajv.compile(schema)
  const valid = validate(data)

  if (!valid) {
    throw new Error(`Schema validation failed: ${JSON.stringify(validate.errors)}`)
  }
})

// Usage
const userSchema = {
  type: 'object',
  properties: {
    id: { type: 'number' },
    name: { type: 'string' },
    email: { type: 'string', format: 'email' }
  },
  required: ['id', 'name', 'email']
}

cy.request('GET', '/api/users/1')
  .then((response) => {
    cy.validateSchema(response.body, userSchema)
  })

Essais visuels

Essai de capture d'écran

describe('Visual Tests', () => {
  it('should match homepage screenshot', () => {
    cy.visit('/')
    cy.screenshot('homepage')
  })

  it('should match element screenshot', () => {
    cy.visit('/dashboard')
    cy.get('[data-cy="user-profile"]').screenshot('user-profile')
  })

  it('should match full page screenshot', () => {
    cy.visit('/products')
    cy.screenshot('products-page', { capture: 'fullPage' })
  })
})

Régression visuelle avec percès

# Install Percy
npm install --save-dev @percy/cypress
// cypress/support/e2e.js
import '@percy/cypress'

// Usage in tests
describe('Visual Regression Tests', () => {
  it('should match visual snapshot', () => {
    cy.visit('/')
    cy.percySnapshot('Homepage')
  })

  it('should match responsive snapshots', () => {
    cy.visit('/dashboard')
    cy.percySnapshot('Dashboard - Desktop', {
      widths: [1280]
    })
    cy.percySnapshot('Dashboard - Mobile', {
      widths: [375]
    })
  })
})

Commandes visuelles personnalisées

// cypress/support/commands.js
Cypress.Commands.add('compareSnapshot', (name, options = {}) => {
  const { threshold = 0.1, clip } = options

  cy.screenshot(name, { clip })

  // Custom comparison logic would go here
  // This is a simplified example
  cy.task('compareImages', {
    actualImage: `cypress/screenshots/${name}.png`,
    expectedImage: `cypress/snapshots/${name}.png`,
    threshold
  })
})

// Usage
cy.visit('/login')
cy.compareSnapshot('login-page', { threshold: 0.05 })

Essais réactifs

describe('Responsive Tests', () => {
  const viewports = [
    { width: 320, height: 568 },   // iPhone SE
    { width: 375, height: 667 },   // iPhone 8
    { width: 768, height: 1024 },  // iPad
    { width: 1024, height: 768 },  // iPad Landscape
    { width: 1280, height: 720 },  // Desktop
  ]

  viewports.forEach((viewport) => {
    it(`should display correctly at ${viewport.width}x${viewport.height}`, () => {
      cy.viewport(viewport.width, viewport.height)
      cy.visit('/')
      cy.screenshot(`homepage-${viewport.width}x${viewport.height}`)

      // Test responsive behavior
      if (viewport.width < 768) {
        cy.get('[data-cy="mobile-menu"]').should('be.visible')
        cy.get('[data-cy="desktop-menu"]').should('not.be.visible')
      } else {
        cy.get('[data-cy="mobile-menu"]').should('not.be.visible')
        cy.get('[data-cy="desktop-menu"]').should('be.visible')
      }
    })
  })
})

Intégration CI/CD

Actions GitHub

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

on: [push, pull_request]

jobs:
  cypress-run:
    runs-on: ubuntu-latest

    strategy:
      matrix:
        browser: [chrome, firefox, edge]

    steps:
      - name: Checkout
        uses: actions/checkout@v3

      - name: Setup Node.js
        uses: actions/setup-node@v3
        with:
          node-version: '18'
          cache: 'npm'

      - name: Install dependencies
        run: npm ci

      - name: Start application
        run: npm start &

      - name: Wait for application
        run: npx wait-on http://localhost:3000

      - name: Run Cypress tests
        uses: cypress-io/github-action@v5
        with:
          browser: ${{ matrix.browser }}
          record: true
          parallel: true
        env:
          CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

      - name: Upload screenshots
        uses: actions/upload-artifact@v3
        if: failure()
        with:
          name: cypress-screenshots-${{ matrix.browser }}
          path: cypress/screenshots

      - name: Upload videos
        uses: actions/upload-artifact@v3
        if: always()
        with:
          name: cypress-videos-${{ matrix.browser }}
          path: cypress/videos

Intégration Docker

# Dockerfile.cypress
FROM cypress/included:12.17.0

WORKDIR /app

COPY package*.json ./
RUN npm ci

COPY . .

# Run tests
CMD ["npx", "cypress", "run"]
# docker-compose.yml
version: '3.8'
services:
  app:
    build: .
    ports:
      - "3000:3000"
    environment:
      - NODE_ENV=test

  cypress:
    build:
      context: .
      dockerfile: Dockerfile.cypress
    depends_on:
      - app
    environment:
      - CYPRESS_baseUrl=http://app:3000
    volumes:
      - ./cypress/videos:/app/cypress/videos
      - ./cypress/screenshots:/app/cypress/screenshots

Essais parallèles

// cypress.config.js
module.exports = defineConfig({
  e2e: {
    // Enable parallel testing
    setupNodeEvents(on, config) {
      // Split tests across multiple machines
      if (config.env.SPLIT_TESTS) {
|  |  | const totalMachines = config.env.TOTAL_MACHINES |  | 1 |  |  |
|  |  | const currentMachine = config.env.CURRENT_MACHINE |  | 0 |  |  |

        // Custom logic to split test files
        config.specPattern = getSpecsForMachine(currentMachine, totalMachines)
      }

      return config
    }
  }
})

function getSpecsForMachine(current, total) {
  // Implementation to split specs across machines
  const allSpecs = [
    'cypress/e2e/auth/*.cy.js',
    'cypress/e2e/dashboard/*.cy.js',
    'cypress/e2e/users/*.cy.js'
  ]

  return allSpecs.filter((_, index) => index % total === current)
}

Déclaration des essais

# Install mochawesome reporter
npm install --save-dev mochawesome mochawesome-merge mochawesome-report-generator
// cypress.config.js
module.exports = defineConfig({
  e2e: {
    reporter: 'mochawesome',
    reporterOptions: {
      reportDir: 'cypress/reports',
      overwrite: false,
      html: false,
      json: true
    }
  }
})
{
  "scripts": {
    "test:report": "cypress run && npm run merge:reports && npm run generate:report",
    "merge:reports": "mochawesome-merge cypress/reports/*.json > cypress/reports/merged-report.json",
    "generate:report": "marge cypress/reports/merged-report.json --reportDir cypress/reports --inline"
  }
}

Meilleures pratiques

Organisation des essais

// Good: Descriptive test names
describe('User Authentication', () => {
  it('should allow user to login with valid credentials', () => {
    // Test implementation
  })

  it('should display error message for invalid credentials', () => {
    // Test implementation
  })

  it('should redirect to dashboard after successful login', () => {
    // Test implementation
  })
})

// Good: Group related tests
describe('Shopping Cart', () => {
  describe('Adding Items', () => {
    it('should add item to cart')
    it('should update cart count')
    it('should show item in cart sidebar')
  })

  describe('Removing Items', () => {
    it('should remove item from cart')
    it('should update cart total')
    it('should show empty cart message when no items')
  })
})

Meilleures pratiques de sélection

// ❌ Bad: Fragile selectors
cy.get('.btn.btn-primary.submit-button')
cy.get('div > form > div:nth-child(2) > input')
cy.get('button:contains("Submit")')

// ✅ Good: Stable selectors
cy.get('[data-cy="submit-button"]')
cy.get('[data-testid="email-input"]')
cy.get('[aria-label="Submit form"]')

// ✅ Good: Custom commands for common patterns
Cypress.Commands.add('getByTestId', (testId) => {
  return cy.get(`[data-testid="${testId}"]`)
})

cy.getByTestId('email-input').type('user@example.com')

En attente des meilleures pratiques

// ❌ Bad: Arbitrary waits
cy.wait(3000)
cy.get('button').click()

// ✅ Good: Wait for specific conditions
cy.get('[data-cy="loading"]').should('not.exist')
cy.get('[data-cy="submit-button"]').should('be.enabled').click()

// ✅ Good: Wait for network requests
cy.intercept('POST', '/api/users').as('createUser')
cy.get('[data-cy="submit"]').click()
cy.wait('@createUser')

Gestion des données d'essai

// ✅ Good: Use fixtures for test data
cy.fixture('users.json').then((users) => {
  const testUser = users.validUser
  cy.login(testUser.email, testUser.password)
})

// ✅ Good: Generate dynamic test data
const generateUser = () => ({
  name: `Test User ${Date.now()}`,
  email: `test${Date.now()}@example.com`,
  password: 'password123'
})

// ✅ Good: Clean up test data
afterEach(() => {
  cy.task('cleanupTestData')
})

Gestion des erreurs

// ✅ Good: Handle expected errors
cy.get('[data-cy="submit"]').click()
cy.get('[data-cy="error-message"]')
  .should('be.visible')
  .and('contain', 'Email is required')

// ✅ Good: Retry flaky operations
Cypress.Commands.add('retryableClick', (selector, maxRetries = 3) => {
  let attempts = 0

  const clickWithRetry = () => {
    attempts++

    return cy.get(selector).click().then(() => {
      // Verify click was successful
      return cy.get(selector).should('have.class', 'clicked')
    }).catch((error) => {
      if (attempts < maxRetries) {
        cy.wait(1000)
        return clickWithRetry()
      }
      throw error
    })
  }

  return clickWithRetry()
})

Déboguement

Déboguer les commandes

// Pause test execution
cy.pause()

// Debug specific element
cy.get('[data-cy="button"]').debug()

// Log values
cy.get('[data-cy="input"]').then(($el) => {
  console.log('Element value:', $el.val())
})

// Take screenshot at specific point
cy.screenshot('debug-point-1')

// Log network requests
cy.intercept('**', (req) => {
  console.log('Request:', req.method, req.url)
})

Outils de navigateur Dev

// Open DevTools programmatically
cy.window().then((win) => {
  win.debugger
})

// Inspect element in DevTools
cy.get('[data-cy="element"]').then(($el) => {
  debugger // Will pause in DevTools
  console.log($el[0]) // Inspect element
})

// Access application state
cy.window().its('store').then((store) => {
  console.log('Redux state:', store.getState())
})

Commandes de débogage personnalisées

// cypress/support/commands.js
Cypress.Commands.add('logElement', (selector) => {
  cy.get(selector).then(($el) => {
    console.log('Element:', $el[0])
    console.log('Text:', $el.text())
    console.log('Value:', $el.val())
    console.log('Classes:', $el.attr('class'))
  })
})

Cypress.Commands.add('debugState', () => {
  cy.window().then((win) => {
    if (win.store) {
      console.log('Redux State:', win.store.getState())
    }
    if (win.localStorage) {
      console.log('LocalStorage:', win.localStorage)
    }
    if (win.sessionStorage) {
      console.log('SessionStorage:', win.sessionStorage)
    }
  })
})

// Usage
cy.logElement('[data-cy="user-profile"]')
cy.debugState()

Tester les stratégies de débogage

// Strategy 1: Isolate failing test
it.only('should debug this specific test', () => {
  // Test implementation
})

// Strategy 2: Add detailed logging
it('should complete user flow', () => {
  cy.log('Step 1: Visit login page')
  cy.visit('/login')

  cy.log('Step 2: Fill credentials')
  cy.get('[data-cy="email"]').type('user@example.com')
  cy.get('[data-cy="password"]').type('password123')

  cy.log('Step 3: Submit form')
  cy.get('[data-cy="submit"]').click()

  cy.log('Step 4: Verify redirect')
  cy.url().should('include', '/dashboard')
})

// Strategy 3: Break down complex tests
it('should login user', () => {
  cy.login('user@example.com', 'password123')
})

it('should navigate to profile', () => {
  cy.login('user@example.com', 'password123')
  cy.get('[data-cy="profile-link"]').click()
  cy.url().should('include', '/profile')
})

Rendement

Efficacité des essais

// Optimize test setup
describe('Performance Optimized Tests', () => {
  // Use beforeEach for common setup
  beforeEach(() => {
    cy.login('user@example.com', 'password123')
  })

  // Avoid unnecessary visits
  it('should test multiple features on same page', () => {
    cy.visit('/dashboard')

    // Test multiple things on the same page
    cy.get('[data-cy="user-name"]').should('contain', 'John Doe')
    cy.get('[data-cy="notification-count"]').should('contain', '3')
    cy.get('[data-cy="recent-activity"]').should('be.visible')
  })
})

// Use API for setup when possible
Cypress.Commands.add('seedData', () => {
  return cy.request('POST', '/api/test/seed', {
    users: 10,
    posts: 50,
    comments: 100
  })
})

// Usage
beforeEach(() => {
  cy.seedData()
})

Exécution parallèle

# Run tests in parallel (requires Cypress Dashboard)
npx cypress run --record --parallel

# Split tests manually
npx cypress run --spec "cypress/e2e/auth/**/*"
npx cypress run --spec "cypress/e2e/dashboard/**/*"
npx cypress run --spec "cypress/e2e/users/**/*"

Optimisation de la mémoire

// cypress.config.js
module.exports = defineConfig({
  e2e: {
    // Optimize memory usage
    numTestsKeptInMemory: 5,

    // Disable video for faster execution
    video: false,

    // Reduce screenshot quality
    screenshotOnRunFailure: true,

    setupNodeEvents(on, config) {
      // Clear browser cache between tests
      on('before:browser:launch', (browser, launchOptions) => {
        if (browser.name === 'chrome') {
          launchOptions.args.push('--disable-dev-shm-usage')
          launchOptions.args.push('--memory-pressure-off')
        }
        return launchOptions
      })
    }
  }
})

Dépannage

Questions communes

// Issue: Element not found
// Solution: Wait for element or check selector
cy.get('[data-cy="button"]', { timeout: 10000 }).should('exist')

// Issue: Element not clickable
// Solution: Ensure element is visible and enabled
cy.get('[data-cy="button"]')
  .should('be.visible')
  .and('not.be.disabled')
  .click()

// Issue: Flaky tests
// Solution: Add proper waits and assertions
cy.intercept('GET', '/api/data').as('getData')
cy.visit('/page')
cy.wait('@getData')
cy.get('[data-cy="content"]').should('be.visible')

// Issue: Cross-origin errors
// Solution: Disable web security (not recommended for production)
// cypress.config.js
{
  chromeWebSecurity: false
}

Configuration de débogage

// cypress.config.js
module.exports = defineConfig({
  e2e: {
    // Enable debug mode
    setupNodeEvents(on, config) {
      // Log all events
      on('task', {
        log(message) {
          console.log(message)
          return null
        }
      })

      // Debug failed tests
      on('after:spec', (spec, results) => {
        if (results && results.stats.failures) {
          console.log('Failed spec:', spec.relative)
          console.log('Failures:', results.stats.failures)
        }
      })
    }
  }
})

// Usage in tests
cy.task('log', 'Debug message here')

Erreur de récupération

// Retry failed commands
Cypress.Commands.add('retryCommand', (command, maxRetries = 3) => {
  let attempts = 0

  const executeCommand = () => {
    attempts++

    return command().catch((error) => {
      if (attempts < maxRetries) {
        cy.wait(1000)
        return executeCommand()
      }
      throw error
    })
  }

  return executeCommand()
})

// Usage
cy.retryCommand(() => cy.get('[data-cy="flaky-element"]').click())

Résumé

Cypress est un puissant cadre de test de bout en bout qui fournit:

  • Expérience développeur: API intuitive avec prévisualisation du navigateur en temps réel
  • Attente automatique: attente intelligente pour les éléments et les requêtes réseau
  • ** Voyages dans le temps** : Essais de débogage avec des instantanés à chaque étape
  • Contrôle du réseau: Intercepter et modifier les requêtes réseau
  • Véritables tests de navigateur: Tests exécutés dans des navigateurs réels pour des résultats précis
  • Rich Ecosystem: vaste plugin écosystème et intégrations
  • CI/CD Ready: Support intégré pour l'intégration continue
  • Essais visuels: Captures d'écran et capacités de tests de régression visuelle

Cypress excelle dans les tests d'applications web modernes avec son accent sur l'expérience du développeur, des outils de débogage complets et une exécution de test fiable. Son architecture et son ensemble de fonctionnalités uniques en font un excellent choix pour les équipes qui cherchent à mettre en œuvre des workflows de test de bout en bout robustes.