阿里云主机折上折
  • 微信号
您当前的位置:网站首页 > 单元测试 & E2E测试:Jest + Cypress 组合拳

单元测试 & E2E测试:Jest + Cypress 组合拳

作者:陈川 阅读数:10939人阅读 分类: 前端综合

单元测试和E2E测试是前端质量保障的两大核心手段。Jest作为JavaScript生态中流行的单元测试框架,与Cypress这一现代E2E测试工具的结合,能覆盖从函数级验证到用户交互的全流程测试场景。

单元测试:Jest的核心能力

Jest以其零配置、快照测试和强大的Mock功能成为React等前端项目的首选测试框架。它的核心优势体现在:

  1. 快速反馈:通过--watch模式实现热更新测试
  2. 隔离测试:自动为每个测试文件创建独立沙箱环境
  3. 快照比对:对UI组件输出进行版本控制

典型测试用例结构如下:

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

describe('日期格式化工具', () => {
  test('应该正确处理ISO格式日期', () => {
    const input = '2023-05-15T08:30:00Z';
    expect(formatDate(input)).toBe('2023年5月15日');
  });

  test('空值应该返回默认占位符', () => {
    expect(formatDate(null)).toBe('--');
  });
});

Mock功能的实际应用示例:

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

describe('用户数据获取', () => {
  it('应该处理API错误', async () => {
    require('./apiClient').getUser.mockRejectedValue(new Error('Timeout'));
    const { fetchUser } = require('./userService');
    
    await expect(fetchUser(123)).rejects.toThrow('请求超时');
  });
});

E2E测试:Cypress实战

Cypress解决了传统E2E测试工具的三大痛点:

  • 实时重载测试运行
  • 自动等待机制
  • 时间旅行调试

基础测试场景示例:

// login.spec.js
describe('登录流程', () => {
  beforeEach(() => {
    cy.visit('/login');
  });

  it('应该阻止无效凭证登录', () => {
    cy.get('#username').type('test@demo.com');
    cy.get('#password').type('wrong123');
    cy.get('form').submit();
    cy.contains('.error', '凭证无效').should('be.visible');
  });

  it('成功登录后应跳转仪表盘', () => {
    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');
  });
});

高级功能实践:

// network.spec.js
it('应该显示加载状态', () => {
  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);
});

组合策略:分层测试体系

  1. 金字塔模型

    • 底层:70% Jest单元测试
    • 中层:20% 集成测试
    • 顶层:10% Cypress E2E测试
  2. CI/CD集成

# .github/workflows/test.yml
jobs:
  test:
    steps:
      - run: npm test       # Jest单元测试
      - run: npm run cy:run -- --headless # Cypress无头模式
  1. 共享逻辑复用
// 在Jest和Cypress间共享的测试数据
// test-utils/constants.js
export const TEST_USER = {
  email: 'test@cypress.com',
  password: 'Test1234!'
};

// 在Jest测试中
import { TEST_USER } from '../test-utils/constants';

// 在Cypress测试中
import { TEST_USER } from '../../test-utils/constants';

调试技巧与性能优化

Jest调试方案

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

Cypress性能优化

// cypress.config.js
module.exports = {
  e2e: {
    experimentalMemoryManagement: true,
    numTestsKeptInMemory: 5 // 减少内存占用
  }
}

并行测试配置示例:

# 分割测试任务
npm run jest -- --shard=1/3
npm run jest -- --shard=2/3
npm run jest -- --shard=3/3

常见问题解决方案

Jest与CSS模块冲突

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

Cypress元素定位策略

// 避免脆弱的定位方式
cy.get('[data-testid="submit-button"]').click(); // 优于 cy.get('.btn-primary').click();

// 自定义命令
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();
});

测试数据管理

// fixtures/products.json
{
  "list": [
    {
      "id": 1,
      "name": "测试商品",
      "price": 99.9
    }
  ]
}

// 测试中使用
cy.intercept('GET', '/api/products', { fixture: 'products.json' });

测试覆盖率与报告

Jest覆盖率配置:

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

Cypress覆盖率集成:

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

// 插件配置
module.exports = (on, config) => {
  require('@cypress/code-coverage/task')(on, config);
  return config;
};

合并报告示例:

# 合并Jest和Cypress的覆盖率数据
nyc merge .nyc_output && nyc report --reporter=lcov

现代前端框架的测试适配

React组件测试方案:

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

test('按钮点击触发回调', () => {
  const handleClick = jest.fn();
  render(<Button onClick={handleClick}>提交</Button>);
  
  fireEvent.click(screen.getByText('提交'));
  expect(handleClick).toHaveBeenCalledTimes(1);
});

Vue组合式API测试:

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

test('计数器应该递增', () => {
  const count = ref(0);
  const { increment } = useCounter(count);
  
  increment();
  expect(count.value).toBe(1);
});

Next.js页面测试:

// home.cy.js
describe('首页测试', () => {
  it('应该预取数据', () => {
    cy.intercept('GET', '/api/posts', { fixture: 'posts.json' });
    cy.visit('/');
    cy.get('[data-testid=post-list]').should('have.length', 5);
  });
});

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

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

前端川

前端川,陈川的代码茶馆🍵,专治各种不服的Bug退散符💻,日常贩卖秃头警告级的开发心得🛠️,附赠一行代码笑十年的摸鱼宝典🐟,偶尔掉落咖啡杯里泡开的像素级浪漫☕。‌