> 技术文档 > 前端单元测试最佳实践(一)_React组件测试方法

前端单元测试最佳实践(一)_React组件测试方法

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

使用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的单元测试可覆盖率也会很容易更高。