从单元测试的角度看React组件设计

305 阅读3分钟

关于单元测试的共识

  1. 先开发再补单元测试?

行不通,因为代码在编写时并没有考虑可测试性。如果在开发时编写单元测试,会反过来影响代码的设计,使你的设计更容易被测试。而容易被测试的代码一般也是更好维护的。

其次,开发上线后不可能再安排时间回来写单元测试了。

  1. 稳定的功能才能做单元测试?

稳定和自动化测试本身就是相互矛盾的。如果一个功能非常稳定永远不用修改,那就没有编写单元测试的必要了。修改一个功能10%的代码,我们需要保证另外90%的代码稳定,即使每隔几个月就要修改10%的代码(可以称之为需求频繁改动),写单元测试依然有必要。

如果90%的代码需要频繁改动,那确实没有写单元测试的必要。所以主要还是看代码的寿命。

与集成测试不同,是否做单元测试并不需要以功能为单位来做决策。 通常一个功能在设计上会抽象出很多小的模块或函数,可以对其中相对稳定的部分做单元测试。即使是频繁变更的功能,大概率也能找到相对稳定的模块。如果找不到,那很有可能是代码设计上出了问题。

编写易于测试的代码

单一职责

编程基本意识,略。

抽离外部依赖

确定测试单元:在测试单元内不应包含外部依赖,应该将依赖作为参数。测试则采用mock数据

分辨需要提取的外部依赖:不稳定的外部依赖模块。如接口、CDN资源等,这类依赖缺少版本控制,存在不确定性。而指定版本的外部依赖则可以认为是稳定的,如npm包。

抽离存在副作用的函数

副作用”是指除函数返回值之外的任何变更,包括 state 的更改或者其他行为。一些常见的副作用是:

  • 在控制台打印日志
  • 保存文件
  • 设置异步定时器
  • 发送 AJAX HTTP 请求
  • 修改存在于函数之外的某些 state,或改变函数的参数
  • 生成随机数或唯一随机 ID(例如 Math.random()Date.now()

React组件设计原则

要对一个React组件做单元测试时,一开始可能会觉得无从下手。我们先将React组件抽象为如下数据流:

image.png

测试什么?

  • Action --> state:动作触发之后,组件中所有state的状态是否正确
  • Props --> view:对于pure component,props与view之间的关系是否一一对应。
  • Function:流程图中涉及到的函数
抽象state

使用react自带的reducer或redux将state组织起来,便于测试Action --> state是否正确

相关文档:

使用redux:

redux.js.org/tutorials/f…

redux.js.org/tutorials/f…

使用 React reducer:beta.reactjs.org/reference/r…

减少状态

推荐: state(A) --> fun(A) --> B --> view

不推荐:state(A) --> fun(A) --> state(B) --> view

UI&交互组件 与 业务分开

用redux可以实现这一点。这种方式是将逻辑放在redux里面了

image.png

提升通用性

案例:

重构前

export const getNavList = (id: number, departmentList: IDepartment[]): IDepartment[] => {
  const newList: IDepartment[] = [];
  let parent = departmentList.find((department) => id === department.id);
  while (parent) {
    newList.unshift(parent);
    parent = departmentList.find((department) => department.id === parent!.parentId);
  }
  return newList;
};

重构后

export const getParents = <T extends { id: unknown; parentId: unknown }>(
  id: unknown,
  totalData: T[],
): T[] => {
  const newList: T[] = [];
  let parent = totalData.find((department) => id === department.id);
  while (parent) {
    newList.unshift(parent);
    parent = totalData.find((department) => department.id === parent!.parentId);
  }
  return newList;
};

心得

  • 最大的难度和价值不在于编写单元测试本身,而在于编写易于测试的代码