前端测试避坑指南:从被坑到真香的实战经验

先坦白,我刚开始对测试的态度是:哎呀,项目小,跑一遍浏览器就好了。结果第一次上线就被用户喷“提交表单没反应”,我本地测了十遍都没事。后来才发现是测试环境没覆盖到边缘场景。

所以这篇东西,我不扯什么TDD、BDD的大道理,直接说怎么落地,包括你可能会被坑的细节。

开篇吐槽:这就是我当年踩坑时的状态——测试覆盖率100%,线上bug照样炸。

第一步:单元测试到底测什么?

很多人写单元测试,上来就是expect(add(1,2)).toBe(3),这玩意儿测了等于没测。单元测试的核心是测逻辑,不是测实现

我现在的做法是:只测纯函数和业务逻辑,不测UI渲染。比如一个工具函数:

javascript
// 为什么要这么写?因为isValidEmail是纯函数,输入输出明确,适合单元测试
// 但注意:不要测试内部的实现细节,比如用了什么正则,只测结果
// 注意:这里的正则仅作示例,生产环境建议使用 validator.isEmail 等成熟库

// utils/validation.js
export function isValidEmail(email) {
if (typeof email !== 'string') return false;
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email.trim());
}
`

测试这样写:

`javascript
// __tests__/validation.test.js
import { isValidEmail } from '../utils/validation';

describe('isValidEmail', () => {
it('应该正确识别有效邮箱', () => {
expect(isValidEmail('test@example.com')).toBe(true);
expect(isValidEmail('a@b.cn')).toBe(true);
});

it('应该拒绝无效输入', () => {
expect(isValidEmail('')).toBe(false);
expect(isValidEmail(null)).toB

<

p>e(false);
expect(isValidEmail('not-email')).toBe(false);
});

// 注意:函数内部已处理空格,但测试仍需覆盖此场景
it('应该自动trim输入', () => {
expect(isValidEmail(' test@example.com ')).toBe(true);
});
});
`

这个设计真的反人类:我第一次写测试时,连nullundefined都没测,结果线上一个接口返回了null,直接崩了。所以边界值一定要覆盖。

第二步:集成测试——别让mock毁了你

集成测试是个大坑。官方文档说“mock掉外部依赖”,但多少人mock完发现自己测的是个假系统?

我现在的原则:只mock不稳定或不可控的东西(如第三方API),自己的数据库和模块尽量真实调用

举个例子,测试一个登录功能:

`javascript
// 为什么要这么写?因为登录流程涉及多个模块交互,mock太多测不出来 true bug
// 注意:这里使用独立的测试数据库(如 test_db),并在 beforeAll/afterAll 中通过事务回滚或删除操作确保数据隔离

// __tests__/login.integration.test.js
import { loginUser } from '../services/auth';
import { createUser, deleteUser } from './testHelpers'; // 真实操作测试数据库

// testHelpers 示例实现(假设使用 knex):
// export async function createUser(data) {
// await knex('users').insert(data);
// }
// export async function deleteUser(email) {

// await knex('users').where({ email }).del();
// }

beforeAll(async () => {
// 在测试数据库里创建一个临时用户
await createUser({ email: 'test@example.com', password: '123456' });
});

afterAll(async () => {
await deleteUser('test@example.com');
});

it('应该能成功登录', async () => {
const result = await loginUser('test@example.com', '123456');
expect(result.token).toBeDefined();
expect(result.user.email).toBe('test@example.com');
});

it('应该拒绝错误密码', async () => {
await expect(loginUser('test@example.com', 'wrong')).rejects.toThrow('密码错误');
});
`

另一个坑:测试数据清理。我第一次跑集成测试时,忘记在afterAll里删除测试用户,结果第二次运行就报“用户已存在”。后来改用事务回滚,每次测试完自动回滚,干净利落。

第三步:e2e测试——从3.2秒降到0.8秒的优化

E2E测试最容易被吐槽“跑得慢”。我当初用Cypress写了个登录流程,跑一次要3.2秒,开发同事直接骂娘。

后来发现,慢是因为Cypress默认每次都从头加载整个应用。我改成只加载一次,然后复用登录状态

`javascript
// cypress/support/commands.js
// 为什么要这样写?因为每次都登录太慢了,而且暴露了测试账号在代码里
// 注意:如果跨 spec 文件复用 session,可添加 cacheAcrossSpecs: true

Cypress.Commands.add('login', () => {
cy.session('user', () => {
cy.visit('/login');
cy.get('[data-cy=email]').type('test@example.com');
cy.get('[data-cy=password]').type('123456');
cy.get('[data-cy=submit]').click();
cy.url().should('include', '/dashboard');
});
});
`

然后在测试里:

`javascript
// cypress/e2e/dashboard.cy.js
describe('Dashboard', () => {
beforeEach(() => {
cy.login(); // 复用session,第一次3秒,之后0.8秒
});

it('应该显示用户信息', () => {
cy.visit('/dashboard');
cy.get('[data-cy=username]').should('contain', 'Test User');
});
});
`

这个优化直接让e2e测试从“没人愿跑”变成“CI流水线标配”。

核心操作:看看这个优化前后的对比图,从3.2秒到0.8秒,差距就在这里。

还有个技巧:用data-cy属性而不是class选择器。因为class经常被重构,而data-cy是专门为测试留的钩子,不会动。

`html



`

另外,建议在 cypress.config.js 中配置 baseUrl,这样 cy.visit 调用更简洁。

第四步:覆盖率的陷阱

最后说个血泪史:有一次我覆盖率报告显示90%,但线上还是出bug了。为什么?因为覆盖率只告诉你“哪些代码被执行了”,不告诉你“执行得对不对”。

我的教训:别盲目追求100%覆盖率。重点覆盖:

  • 核心业务逻辑(比如订单计算、权限判断)
  • 边界情况(空值、超长输入、并发)
  • 错误处理(try-catch里的逻辑)
  • 比如这个:

    `javascript
    // 这个函数在覆盖率报告里是绿色的,但线上还是会崩
    function processOrder(order) {
    const total = order.items.reduce((sum, item) => sum + item.price, 0);
    // 如果order.items是undefined,这里直接报错
    return total;
    }
    `

    正确的测试应该包括:

    `javascript
    it('应该处理空订单', () => {
    expect(processOrder({ items: [] })).toBe(0);
    });

    it('应该处理undefined订单', () => {
    expect(processOrder({})).toBe(0); // 这里会报错,因为你忘了判空
    });

    总结前再来一张图:这是我在项目里整理的核心路径覆盖清单,照着写准没错。

    总结一下,你可以立刻用的三个点

  • 单元测试只测纯函数和业务逻辑,别测UI细节,边界值必须覆盖null、undefined、空值
  • 集成测试少mock,用真实数据库或事务回滚,避免测出假绿
  • E2E测试优化速度:用cy.session复用登录状态,用data-cy属性定位元素,别碰class
  • 最后说一句:测试不是为了应付KPI,是帮你自己省心。我第一次上线前跑完测试,晚上睡得特别踏实。从被坑到真香,就这一步。

    滚动至顶部