前端单元测试最佳实践(一)

发布于:2025-08-09 ⋅ 阅读:(15) ⋅ 点赞:(0)

引言
这是一个新的小系列,跟大家聊聊怎么让前端的单元测试变得“有用”,而不是那种写了等于没写的摆设。

使用DDD与TDD结合,打造前端有意义的单元测试

很多时候,我们的测试都停留在“点个按钮,看看页面变没变”,跟业务逻辑没啥关系。但如果把**领域驱动设计(DDD)测试驱动开发(TDD)**结合起来,前端也能写出靠谱的测试,既能保证功能没问题,还能少写点没用的代码。

这篇文章,我会用一个简单的例子,带大家从业务建模开始,一步步写用例(UseCase),用TDD驱动代码实现,最后用React Hook把前端界面和业务逻辑连起来。整个过程会让前后端解耦,测试也能真正验证业务价值。

用例这个概念,其实不是DDD中的一部分,但为了方便理解,我会用这个概念。


一、为什么我要写TDD?

先说说前端开发里常见的糟心事:

  1. 测试覆盖难:组件逻辑和UI渲染混在一起,想单独测试业务逻辑都费劲。
  2. 测试价值低:测来测去都是"按钮点击了没",抓不住业务逻辑的核心。
  3. 测试维护难:需求改了,测试用例没人删,时间一长全是过时的测试。

而TDD呢,先写测试,再写业务代码,能让我们只写有用的代码——每行代码都有测试撑腰,需求没要的就不写,干净又省心。

谦卑对象模式

这就不得不提一个设计模式,谦卑对象模式。谦卑对象模式(Humble Object Pattern)是一种设计模式,用于将复杂逻辑从难以测试的组件中分离出来,以提高代码的可测试性和可维护性。其核心思想是将与用户界面、外部系统或复杂依赖相关的代码(难以测试的部分)剥离,保留一个“谦卑”的对象,只包含简单逻辑或直接调用,而将主要业务逻辑放入易于测试的独立对象中。
比如:前端的GUI展示部分难以测试,所以应尽量保持简单,只负责渲染数据。数据处理和业务逻辑则单独拆分到易于测试的模块中,这样既降低出错率,也方便单元测试。

DDD能帮我们把业务抽出来,变成独立的“领域模型”和“用例”,让前端也有自己的逻辑层,跟界面分开。


二、实践路径:从业务建模到TDD

1. 先搞清楚业务:建个领域模型

第一步,咱们得从业务角度想想,核心是什么。比如说,我们要做一个任务管理系统,里面有“任务”,任务有标题、描述、状态(待办、进行中、已完成)。这些东西可以建个模型,叫Task,只管业务规则,不管界面长啥样。

class Task {
  constructor(id, title, description, status) {
    this.id = id;
    this.title = title;
    this.description = description;
    this.status = status;
  }

  // 业务规则:已完成的任务不能再改状态
  changeStatus(newStatus) {
    if (this.status === 'done' && newStatus !== 'done') {
      throw new Error('已完成的任务不能随便改状态哦');
    }
    this.status = newStatus;
  }
}

这个Task就是个纯业务对象,比如“已完成的任务不能改状态”这种规则就写在里面,跟页面没半毛钱关系。

2. 写用例(UseCase):定义业务操作

接下来,把具体的业务操作封装成UseCase。比如“创建任务”是个常见的操作,咱们可以写个CreateTaskUseCase,告诉系统怎么创建任务。

class CreateTaskUseCase {
  constructor(taskRepository) {
    this.taskRepository = taskRepository; // 模拟个仓库,存任务的地方
  }

  async execute(title, description) {
    const task = new Task('task-001', title, description, 'todo'); // 默认待办状态
    await this.taskRepository.save(task); // 存起来
    return task;
  }
}

这个用例就是前端跟后端交互的“合同”,告诉大家创建任务的步骤是什么。

3. 先写测试:TDD的“红灯”阶段

TDD的核心是先写测试,再写代码。咱们以CreateTaskUseCase为例,写个测试,确保它能正常工作:

test('创建任务时应该有正确的标题和默认状态', async () => {
  const taskRepository = { save: jest.fn() }; // 假装有个仓库
  const useCase = new CreateTaskUseCase(taskRepository);
  const task = await useCase.execute('买牛奶', '记得买全脂的');

  expect(task.title).toBe('买牛奶');
  expect(task.status).toBe('todo');
});

这时候运行测试,肯定挂,因为taskRepository和代码细节还没写呢。

4. 写最小代码:让测试“绿灯”

然后,咱们写刚好能通过测试的代码。把taskRepository模拟一下:

class MockTaskRepository {
  async save(task) {
    // 假装保存,啥也不干
  }
}

再跑测试,就通过了。TDD就是要先“红”再“绿”,每步都踏实。

5. 重构:让代码更好看

测试过了,就可以优化一下。比如把硬编码的'task-001'改成随机ID生成器,但前提是测试还得过。


三、用React Hook把业务和前端连起来

传统写法里,前端组件直接调API、改数据,乱七八糟。现在业务逻辑都在UseCase里了,组件只管“调用用例”和“显示结果”。

咱们写个useCreateTask Hook,把用例塞进去:

function useCreateTask() {
  const taskRepository = { save: async (task) => console.log('保存任务:', task) }; // 模拟仓库
  const createTaskUseCase = new CreateTaskUseCase(taskRepository);

  const createTask = async (title, description) => {
    const task = await createTaskUseCase.execute(title, description);
    console.log('任务创建成功:', task);
  };

  return { createTask };
}

然后在组件里用:

function TaskForm() {
  const { createTask } = useCreateTask();
  const [title, setTitle] = useState('');

  const handleSubmit = async () => {
    await createTask(title, '用户输入的任务');
    setTitle(''); // 清空输入框
  };

  return (
    <div>
      <input value={title} onChange={(e) => setTitle(e.target.value)} />
      <button onClick={handleSubmit}>创建任务</button>
    </div>
  );
}

组件只管界面和调用,具体怎么创建任务交给Hook和UseCase,干净又解耦。

测试的时候,咱们测UseCase或者Hook就行,不用管页面渲染。比如:

test('useCreateTask应该调用useCase', async () => {
  const mockUseCase = { execute: jest.fn().mockResolvedValue({ title: '测试任务' }) };
  const taskRepository = { save: jest.fn() };
  const useCase = new CreateTaskUseCase(taskRepository);
  mockUseCase.execute = useCase.execute;

  const { createTask } = useCreateTask();
  await createTask('测试任务', '描述');
  expect(mockUseCase.execute).toHaveBeenCalledWith('测试任务', '描述');
});

这测的是业务逻辑,不是按钮点了啥效果。


四、DDD+TDD的好处

用了这套组合拳,开发体验和代码质量都上去了:

  • 逻辑不乱了:业务规则都在模型和用例里,组件只管展示。
  • 测试靠谱了:测的是业务行为,不是界面效果,出了问题一眼就能看出来。
  • 代码干净了:TDD保证每行代码都有用,需求没了测试一删,代码也没了。
  • 回归省心了:以前改个需求要手动测半天,现在跑测试就知道行不行。

五、总结

用DDD把业务抽象成模型和用例,再用TDD推着实现,前端也能像后端一样,写出健壮、可测试、好维护的代码。React Hook把界面和逻辑桥接起来,既模块化又好测。这不只是换个写法,更是换个思路。试试看吧,绝对有收获!

还有一件事,最近接触的工作内容是react 所以这里用这个做举例,但是说真的,vite系列的vitest的单元测试真的顶级好用,同样做单元测试,vue3的单元测试可覆盖率也会很容易更高。


网站公告

今日签到

点亮在社区的每一天
去签到