Cypress 基础测试 #

测试的基本结构 #

describe 和 it #

Cypress 使用 Mocha 的 BDD 语法来组织测试:

javascript
describe('测试套件名称', () => {
  it('测试用例名称', () => {
    // 测试代码
  });
});

第一个测试 #

javascript
// cypress/e2e/login.cy.js
describe('登录功能', () => {
  it('用户可以使用正确的凭据登录', () => {
    cy.visit('/login');
    
    cy.get('#username').type('testuser');
    cy.get('#password').type('password123');
    cy.get('button[type="submit"]').click();
    
    cy.get('.welcome-message').should('contain', '欢迎回来');
  });
});

context 块 #

contextdescribe 的别名,用于更清晰地表达测试上下文:

javascript
describe('购物车功能', () => {
  context('当购物车为空时', () => {
    it('应该显示空购物车提示', () => {
      cy.visit('/cart');
      cy.get('.empty-cart').should('be.visible');
    });
  });

  context('当购物车有商品时', () => {
    it('应该显示商品列表', () => {
      cy.visit('/cart');
      cy.get('.cart-items').should('have.length.gt', 0);
    });
  });
});

测试生命周期 #

钩子函数 #

javascript
describe('用户管理', () => {
  // 所有测试之前执行一次
  before(() => {
    cy.log('初始化测试环境');
    cy.request('POST', '/api/test/setup');
  });

  // 所有测试之后执行一次
  after(() => {
    cy.log('清理测试环境');
    cy.request('POST', '/api/test/teardown');
  });

  // 每个测试之前执行
  beforeEach(() => {
    cy.log('每个测试前的准备');
    cy.visit('/dashboard');
  });

  // 每个测试之后执行
  afterEach(() => {
    cy.log('每个测试后的清理');
    cy.clearCookies();
    cy.clearLocalStorage();
  });

  it('测试1', () => {
    cy.get('.dashboard').should('exist');
  });

  it('测试2', () => {
    cy.get('.sidebar').should('exist');
  });
});

执行顺序 #

text
before()           ─── 执行一次
    │
    ├── beforeEach()  ─── 测试1前
    │   └── it('测试1')
    │   └── afterEach()  ─── 测试1后
    │
    ├── beforeEach()  ─── 测试2前
    │   └── it('测试2')
    │   └── afterEach()  ─── 测试2后
    │
after()            ─── 执行一次

访问页面 #

cy.visit() #

javascript
// 访问相对路径(基于 baseUrl)
cy.visit('/login');

// 访问绝对路径
cy.visit('https://example.com/login');

// 带查询参数
cy.visit('/search?q=cypress');

// 带 options
cy.visit('/dashboard', {
  timeout: 30000,
  onBeforeLoad(win) {
    // 页面加载前的操作
    win.localStorage.setItem('token', 'test-token');
  },
  onLoad(win) {
    // 页面加载后的操作
    console.log('页面加载完成');
  }
});

页面加载验证 #

javascript
describe('首页测试', () => {
  it('应该成功加载首页', () => {
    cy.visit('/');
    
    // 验证标题
    cy.title().should('include', 'My App');
    
    // 验证 URL
    cy.url().should('include', '/');
    
    // 验证关键元素
    cy.get('h1').should('be.visible');
  });
});

测试组织 #

嵌套 describe #

javascript
describe('用户中心', () => {
  describe('个人资料', () => {
    describe('头像上传', () => {
      it('应该支持上传 JPG 格式', () => {});
      it('应该支持上传 PNG 格式', () => {});
      it('应该拒绝不支持的格式', () => {});
    });

    describe('信息修改', () => {
      it('应该允许修改用户名', () => {});
      it('应该允许修改邮箱', () => {});
    });
  });

  describe('账户设置', () => {
    it('应该允许修改密码', () => {});
    it('应该允许删除账户', () => {});
  });
});

测试命名规范 #

javascript
// ✅ 好的命名 - 描述行为和预期结果
it('用户输入正确的用户名和密码后应该成功登录', () => {});
it('用户输入错误的密码后应该显示错误提示', () => {});
it('购物车为空时应该显示"去购物"按钮', () => {});

// ❌ 不好的命名 - 太模糊
it('测试登录', () => {});
it('测试1', () => {});
it('works', () => {});

跳过测试 #

skip 方法 #

javascript
describe('支付功能', () => {
  it('应该支持信用卡支付', () => {
    // 正常执行
  });

  // 跳过单个测试
  it.skip('应该支持 PayPal 支付', () => {
    // 这个测试会被跳过
  });

  // 跳过整个 describe
  describe.skip('银行转账支付', () => {
    it('应该支持银行卡支付', () => {});
  });
});

条件跳过 #

javascript
describe('国际化测试', () => {
  const supportedLocales = ['en', 'zh', 'ja'];
  const testLocale = Cypress.env('locale') || 'en';

  it('应该正确显示翻译文本', () => {
    if (!supportedLocales.includes(testLocale)) {
      this.skip();
    }
    cy.visit(`/${testLocale}`);
    cy.get('.translated-text').should('exist');
  });
});

仅运行特定测试 #

only 方法 #

javascript
describe('用户模块', () => {
  it('测试A', () => {
    // 不会执行
  });

  // 只运行这个测试
  it.only('测试B - 正在调试', () => {
    cy.visit('/');
    cy.get('.element').click();
  });

  it('测试C', () => {
    // 不会执行
  });

  // 只运行这个 describe
  describe.only('登录功能', () => {
    it('登录测试', () => {
      // 会执行
    });
  });
});

测试模式 #

AAA 模式 #

Arrange(准备)- Act(执行)- Assert(断言):

javascript
it('应该成功添加商品到购物车', () => {
  // Arrange - 准备测试数据
  const product = { id: 1, name: '商品A', price: 100 };

  // Act - 执行操作
  cy.visit('/products');
  cy.get(`[data-product-id="${product.id}"]`).click();
  cy.get('.add-to-cart').click();

  // Assert - 验证结果
  cy.get('.cart-count').should('contain', '1');
  cy.get('.cart-total').should('contain', '¥100');
});

Given-When-Then 模式 #

javascript
describe('用户登录', () => {
  it('用户使用正确凭据登录成功', () => {
    // Given - 前置条件
    cy.visit('/login');
    const user = { username: 'testuser', password: 'password123' };

    // When - 执行动作
    cy.get('#username').type(user.username);
    cy.get('#password').type(user.password);
    cy.get('button[type="submit"]').click();

    // Then - 验证结果
    cy.url().should('include', '/dashboard');
    cy.get('.user-name').should('contain', user.username);
  });
});

测试数据 #

使用变量 #

javascript
describe('表单测试', () => {
  // 在 describe 作用域定义测试数据
  const formData = {
    name: '张三',
    email: 'zhangsan@example.com',
    phone: '13800138000'
  };

  it('应该正确提交表单', () => {
    cy.visit('/contact');
    
    cy.get('#name').type(formData.name);
    cy.get('#email').type(formData.email);
    cy.get('#phone').type(formData.phone);
    
    cy.get('form').submit();
    
    cy.get('.success-message').should('be.visible');
  });
});

使用别名 #

javascript
describe('API 数据测试', () => {
  beforeEach(() => {
    // 使用别名存储数据
    cy.request('GET', '/api/users/1').as('userResponse');
  });

  it('应该显示用户信息', function() {
    // 使用 this 访问别名
    const user = this.userResponse.body;
    
    cy.visit(`/users/${user.id}`);
    cy.get('.user-name').should('contain', user.name);
  });
});

重试机制 #

自动重试 #

Cypress 会自动重试断言:

javascript
it('等待元素出现', () => {
  cy.visit('/');
  
  // 自动重试直到元素出现或超时
  cy.get('.loading').should('not.exist');
  cy.get('.data-loaded').should('be.visible');
});

自定义重试 #

javascript
// 自定义重试逻辑
it('自定义重试示例', () => {
  cy.visit('/');
  
  cy.get('.status', { timeout: 10000 })  // 10秒超时
    .should('have.class', 'active');
});

// 使用 retry 选项
Cypress.Commands.add('waitForCondition', (condition, options = {}) => {
  const timeout = options.timeout || 5000;
  const interval = options.interval || 100;
  
  return cy.wrap(null, { timeout }).should(() => {
    expect(condition()).to.be.true;
  });
});

调试技巧 #

cy.pause() #

javascript
it('调试测试', () => {
  cy.visit('/login');
  
  cy.get('#username').type('testuser');
  cy.pause();  // 执行到这里会暂停
  
  cy.get('#password').type('password');
  cy.get('button').click();
});

cy.debug() #

javascript
it('调试示例', () => {
  cy.visit('/');
  
  cy.get('.item').then(($item) => {
    cy.debug();  // 打开开发者工具调试
    console.log($item.text());
  });
});

cy.log() #

javascript
it('日志输出', () => {
  cy.visit('/');
  
  cy.log('开始测试');
  cy.get('.button').click();
  cy.log('按钮已点击');
  
  cy.get('.result').then(($el) => {
    cy.log(`结果: ${$el.text()}`);
  });
});

测试文件组织 #

按功能模块组织 #

text
cypress/e2e/
├── auth/
│   ├── login.cy.js
│   ├── register.cy.js
│   └── logout.cy.js
├── products/
│   ├── list.cy.js
│   ├── detail.cy.js
│   └── search.cy.js
└── checkout/
    ├── cart.cy.js
    └── payment.cy.js

按用户流程组织 #

text
cypress/e2e/
├── user-journey/
│   ├── guest-browsing.cy.js
│   ├── user-registration.cy.js
│   ├── purchase-flow.cy.js
│   └── support-ticket.cy.js
└── admin/
    ├── user-management.cy.js
    └── product-management.cy.js

完整示例 #

登录功能测试 #

javascript
// cypress/e2e/auth/login.cy.js
describe('登录功能', () => {
  const user = {
    username: 'testuser',
    password: 'Password123!'
  };

  beforeEach(() => {
    cy.visit('/login');
  });

  context('表单验证', () => {
    it('应该显示登录表单', () => {
      cy.get('#username').should('be.visible');
      cy.get('#password').should('be.visible');
      cy.get('button[type="submit"]').should('be.visible');
    });

    it('空表单提交应该显示错误', () => {
      cy.get('button[type="submit"]').click();
      cy.get('.error-message').should('contain', '请填写用户名');
    });

    it('无效邮箱格式应该显示错误', () => {
      cy.get('#username').type('invalid-email');
      cy.get('#password').type('password');
      cy.get('button[type="submit"]').click();
      cy.get('.error-message').should('contain', '邮箱格式不正确');
    });
  });

  context('登录流程', () => {
    it('正确的凭据应该登录成功', () => {
      cy.get('#username').type(user.username);
      cy.get('#password').type(user.password);
      cy.get('button[type="submit"]').click();

      cy.url().should('include', '/dashboard');
      cy.get('.welcome-message').should('contain', '欢迎');
    });

    it('错误的密码应该显示错误提示', () => {
      cy.get('#username').type(user.username);
      cy.get('#password').type('wrongpassword');
      cy.get('button[type="submit"]').click();

      cy.get('.error-message').should('contain', '用户名或密码错误');
    });

    it('记住登录状态', () => {
      cy.get('#username').type(user.username);
      cy.get('#password').type(user.password);
      cy.get('#remember-me').check();
      cy.get('button[type="submit"]').click();

      cy.url().should('include', '/dashboard');
      cy.getCookie('remember_token').should('exist');
    });
  });

  context('登录后操作', () => {
    beforeEach(() => {
      cy.login(user.username, user.password);
    });

    it('应该能够访问受保护的页面', () => {
      cy.visit('/profile');
      cy.get('.profile-page').should('be.visible');
    });

    it('应该能够成功登出', () => {
      cy.get('.logout-button').click();
      cy.url().should('include', '/login');
    });
  });
});

运行测试 #

交互模式 #

bash
# 打开 Cypress Test Runner
npx cypress open

# 选择测试文件运行
# 可以看到实时执行过程

命令行模式 #

bash
# 运行所有测试
npx cypress run

# 运行指定文件
npx cypress run --spec "cypress/e2e/login.cy.js"

# 运行匹配的文件
npx cypress run --spec "cypress/e2e/auth/**/*.cy.js"

# 指定浏览器
npx cypress run --browser chrome

# 无头模式
npx cypress run --headless

下一步 #

现在你已经掌握了 Cypress 基础测试的编写方法,接下来学习 选择器 了解更多元素定位方式!

最后更新:2026-03-28