阿里云主机折上折
  • 微信号
Current Site:Index > Unit Testing & E2E Testing: Jest + Cypress Combo

Unit Testing & E2E Testing: Jest + Cypress Combo

Author:Chuan Chen 阅读数:10940人阅读 分类: 前端综合

Unit testing and E2E testing are two core methods for ensuring frontend quality. The combination of Jest, a popular unit testing framework in the JavaScript ecosystem, and Cypress, a modern E2E testing tool, can cover the entire testing spectrum from function-level validation to user interaction.

Unit Testing: Core Capabilities of Jest

Jest has become the preferred testing framework for React and other frontend projects due to its zero-configuration setup, snapshot testing, and powerful mocking capabilities. Its core advantages include:

  1. Fast Feedback: Hot-reload testing via --watch mode
  2. Isolated Testing: Automatically creates independent sandbox environments for each test file
  3. Snapshot Comparison: Version control for UI component outputs

A typical test case structure looks like this:

// utils.test.js
import { formatDate } from './dateUtils';

describe('Date Formatting Utility', () => {
  test('should correctly handle ISO format dates', () => {
    const input = '2023-05-15T08:30:00Z';
    expect(formatDate(input)).toBe('May 15, 2023');
  });

  test('null values should return default placeholder', () => {
    expect(formatDate(null)).toBe('--');
  });
});

Example of mocking in practice:

// api.test.js
jest.mock('./apiClient');

describe('User Data Fetching', () => {
  it('should handle API errors', async () => {
    require('./apiClient').getUser.mockRejectedValue(new Error('Timeout'));
    const { fetchUser } = require('./userService');
    
    await expect(fetchUser(123)).rejects.toThrow('Request timeout');
  });
});

E2E Testing: Cypress in Action

Cypress addresses three major pain points of traditional E2E testing tools:

  • Real-time test reloading
  • Automatic waiting mechanism
  • Time-travel debugging

Basic test scenario example:

// login.spec.js
describe('Login Flow', () => {
  beforeEach(() => {
    cy.visit('/login');
  });

  it('should prevent login with invalid credentials', () => {
    cy.get('#username').type('test@demo.com');
    cy.get('#password').type('wrong123');
    cy.get('form').submit();
    cy.contains('.error', 'Invalid credentials').should('be.visible');
  });

  it('should redirect to dashboard after successful login', () => {
    cy.intercept('POST', '/api/login', { fixture: 'login.success.json' });
    cy.get('#username').type('admin@company.com');
    cy.get('#password').type('correct-password');
    cy.get('form').submit();
    cy.url().should('include', '/dashboard');
  });
});

Advanced feature implementation:

// network.spec.js
it('should display loading state', () => {
  cy.intercept('GET', '/api/products', {
    delay: 2000,
    fixture: 'products.json'
  }).as('getProducts');

  cy.visit('/products');
  cy.get('.loading-indicator').should('exist');
  cy.wait('@getProducts');
  cy.get('.product-list').should('have.length', 5);
});

Combined Strategy: Layered Testing System

  1. Pyramid Model:

    • Base: 70% Jest unit tests
    • Middle: 20% integration tests
    • Top: 10% Cypress E2E tests
  2. CI/CD Integration:

# .github/workflows/test.yml
jobs:
  test:
    steps:
      - run: npm test       # Jest unit tests
      - run: npm run cy:run -- --headless # Cypress headless mode
  1. Shared Logic Reuse:
// Test data shared between Jest and Cypress
// test-utils/constants.js
export const TEST_USER = {
  email: 'test@cypress.com',
  password: 'Test1234!'
};

// In Jest tests
import { TEST_USER } from '../test-utils/constants';

// In Cypress tests
import { TEST_USER } from '../../test-utils/constants';

Debugging Tips and Performance Optimization

Jest Debugging Solution:

// Configure launch.json in VSCode
{
  "type": "node",
  "request": "launch",
  "name": "Debug Jest Tests",
  "program": "${workspaceFolder}/node_modules/jest/bin/jest",
  "args": ["--runInBand", "${fileBasename}"]
}

Cypress Performance Optimization:

// cypress.config.js
module.exports = {
  e2e: {
    experimentalMemoryManagement: true,
    numTestsKeptInMemory: 5 // Reduce memory usage
  }
}

Parallel testing configuration example:

# Split test tasks
npm run jest -- --shard=1/3
npm run jest -- --shard=2/3
npm run jest -- --shard=3/3

Common Issue Solutions

Jest and CSS Module Conflicts:

// jest.config.js
module.exports = {
  moduleNameMapper: {
    '\\.(css|less)$': 'identity-obj-proxy'
  }
}

Cypress Element Locating Strategy:

// Avoid fragile locators
cy.get('[data-testid="submit-button"]').click(); // Better than cy.get('.btn-primary').click();

// Custom commands
Cypress.Commands.add('login', (email, password) => {
  cy.get('[data-testid=email]').type(email);
  cy.get('[data-testid=password]').type(password);
  cy.get('[data-testid=submit]').click();
});

Test Data Management:

// fixtures/products.json
{
  "list": [
    {
      "id": 1,
      "name": "Test Product",
      "price": 99.9
    }
  ]
}

// Usage in tests
cy.intercept('GET', '/api/products', { fixture: 'products.json' });

Test Coverage and Reporting

Jest coverage configuration:

// package.json
{
  "scripts": {
    "test:coverage": "jest --coverage"
  },
  "jest": {
    "collectCoverageFrom": [
      "src/**/*.{js,jsx}",
      "!**/node_modules/**",
      "!**/vendor/**"
    ]
  }
}

Cypress coverage integration:

// cypress.config.js
module.exports = {
  env: {
    codeCoverage: {
      url: '/__coverage__'
    }
  }
}

// Plugin configuration
module.exports = (on, config) => {
  require('@cypress/code-coverage/task')(on, config);
  return config;
};

Merge reports example:

# Merge Jest and Cypress coverage data
nyc merge .nyc_output && nyc report --reporter=lcov

Testing Adaptation for Modern Frontend Frameworks

React component testing solution:

// Button.test.jsx
import { render, screen, fireEvent } from '@testing-library/react';
import Button from './Button';

test('button click triggers callback', () => {
  const handleClick = jest.fn();
  render(<Button onClick={handleClick}>Submit</Button>);
  
  fireEvent.click(screen.getByText('Submit'));
  expect(handleClick).toHaveBeenCalledTimes(1);
});

Vue Composition API testing:

// useCounter.test.js
import { ref } from 'vue';
import { useCounter } from './useCounter';

test('counter should increment', () => {
  const count = ref(0);
  const { increment } = useCounter(count);
  
  increment();
  expect(count.value).toBe(1);
});

Next.js page testing:

// home.cy.js
describe('Home Page Test', () => {
  it('should prefetch data', () => {
    cy.intercept('GET', '/api/posts', { fixture: 'posts.json' });
    cy.visit('/');
    cy.get('[data-testid=post-list]').should('have.length', 5);
  });
});

本站部分内容来自互联网,一切版权均归源网站或源作者所有。

如果侵犯了你的权益请来信告知我们删除。邮箱:cc@cccx.cn

Front End Chuan

Front End Chuan, Chen Chuan's Code Teahouse 🍵, specializing in exorcising all kinds of stubborn bugs 💻. Daily serving baldness-warning-level development insights 🛠️, with a bonus of one-liners that'll make you laugh for ten years 🐟. Occasionally drops pixel-perfect romance brewed in a coffee cup ☕.