React 中 使用 Jest And Enzyme 进行单元测试整理

为什么要进行测试

  • 测试可以确保得到预期的结果
  • 作为现有代码行为的描述
  • 促使开发者写可测试的代码,一般可测试的代码可读性也会高一点
  • 如果依赖的组件有修改,受影响的组件能在测试中发现错误

jest

  • 易用性:基于 Jasmine,提供断言库,支持多种测试风格
  • 适应性:Jest 是模块化、可扩展和可配置的
  • 沙箱和快照:Jest 内置了 JSDOM,能够模拟浏览器环境,并且并行执行
  • 快照测试:Jest 能够对 React 组件树进行序列化,生成对应的字符串快照,通过比较字符串提供高性能的 UI 检测
  • Mock 系统:Jest 实现了一个强大的 Mock 系统,支持自动和手动 mock
  • 支持异步代码测试:支持 Promise 和 async/await
  • 自动生成静态分析结果:内置 Istanbul,测试代码覆盖率,并生成对应的报告

jest global api

  • describe(name, fn):描述块,讲一组功能相关的测试用例组合在一起
  • it(name, fn, timeout):别名 test,用来放测试用例
  • afterAll(fn, timeout):所有测试用例跑完以后执行的方法
  • beforeAll(fn, timeout):所有测试用例执行之前执行的方法
  • afterEach(fn):在每个测试用例执行完后执行的方法
  • beforeEach(fn):在每个测试用例执行之前需要执行的方法

jest 断言

  • toBeCalled() :确保调用模拟函数
  • toBeCalledTimes(number) : 确保模拟函数被调用准确的次数。
  • toBeCalledWith(args) : 确保使用特定参数调用模拟函数。
  • expect(value):要测试一个值进行断言的时候,要使用 expect 对值进行包裹
  • toBe(value):使用 Object.is 来进行比较,如果进行浮点数的比较,要使用 toBeCloseTo
  • not:用来取反
  • toEqual(value):用于对象的深比较
  • toMatch(regexpOrString):用来检查字符串是否匹配,可以是正则表达式或者字符串
  • toContain(item):用来判断 item 是否在一个数组中,也可以用于字符串的判断
  • toBeNull(value):只匹配 null
  • toBeUndefined(value):只匹配 undefined
  • toBeDefined(value):与 toBeUndefined 相反
  • toBeTruthy(value):匹配任何使 if 语句为真的值
  • toBeFalsy(value):匹配任何使 if 语句为假的值
  • toBeGreaterThan(number): 大于
  • toBeGreaterThanOrEqual(number):大于等于
  • toBeLessThan(number):小于
  • toBeLessThanOrEqual(number):小于等于
  • toBeInstanceOf(class):判断是不是 class 的实例
  • anything(value):匹配除了 null 和 undefined 以外的所有值
  • resolves:用来取出 promise 为 fulfilled 时包裹的值,支持链式调用
  • rejects:用来取出 promise 为 rejected 时包裹的值,支持链式调用
  • toHaveBeenCalled():用来判断 mock function 是否被调用过
  • toHaveBeenCalledTimes(number):用来判断 mock function 被调用的次数
  • assertions(number):验证在一个测试用例中有 number 个断言被调用
  • extend(matchers):自定义一些断言

jest 方法

  • simulate(event, mock):模拟事件,用来触发事件,event 为事件名称,mock 为一个 event object
  • instance():返回组件的实例
  • find(selector):根据选择器查找节点,selector 可以是 CSS 中的选择器,或者是组件的构造函数,组件的 display name 等
  • at(index):返回一个渲染过的对象
  • get(index):返回一个 react node,要测试它,需要重新渲染
  • contains(nodeOrNodes):当前对象是否包含参数重点 node,参数类型为 react 对象或对象数组
  • text():返回当前组件的文本内容
  • html(): 返回当前组件的 HTML 代码形式
  • props():返回根组件的所有属性
  • prop(key):返回根组件的指定属性
  • state():返回根组件的状态
  • setState(nextState):设置根组件的状态
  • setProps(nextProps):设置根组件的属性

enzyme

实现了 jQuery 风格的方式进行 DOM 处理

提供了三种渲染方式render,mount,shallow,分别存在以下区别:

  • shallow:浅渲染,是对官方的 Shallow Renderer 的封装。将组件渲染成虚拟 DOM 对象,只会渲染第一层,子组件将不会被渲染出来,使得效率非常高。不需要 DOM 环境, 并可以使用 jQuery 的方式访问组件的信息
/*
    TodoList:要测试的组件
    props:自定义的测试数据
*/
const wrapper = shallow(<TodoList {...props} />);
  • render:静态渲染,它将 React 组件渲染成静态的 HTML 字符串,然后使用 Cheerio 这个库解析这段字符串,并返回一个 Cheerio 的实例对象,可以用来分析组件的 html 结构
/*
    ...同上
*/
const wrapper = render(<TodoList {...props} />);
  • mount:完全渲染,它将组件渲染加载成一个真实的 DOM 节点,用来测试 DOM API 的交互和组件的生命周期。用到了 jsdom 来模拟浏览器环境
// ...
const wrapper = mount(<TodoList {...props} />);

enzyme 配置

import Enzyme, { shallow } from "enzyme";

import Adapter from "enzyme-adapter-react-16"; //适应React-16
Enzyme.configure({ adapter: new Adapter() }); //适应React-16,初始化

export function shallowInit(node, options) {
  return shallow(node, options);
}


// 或者是以下这样的一个配置


const props = {
  list: ['first', 'second'],
  deleteTodo: jest.fn(),
};

const setup = () => {
  const wrapper = shallow(<TodoList {...props} />);
  return {
    props,
    wrapper,
  };
};

const setupByRender = () => {
  const wrapper = render(<TodoList {...props} />);
  return {
    props,
    wrapper,
  };
};

const setupByMount = () => {
  const wrapper = mount(<TodoList {...props} />);
  return {
    props,
    wrapper,
  };

实际操作

// func.js
export const sum = (x, y) => {
  return x + y;
};

// 回调
export const getDataCallback = (fn) => {
  setTimeout(() => {
    fn({ name: "callback" });
  }, 1000);
};

// promise
export const getDataPromise = (fn) => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve({ name: "callback" });
    }, 1000);
  });
};

匹配器

相当,不相等,包含,等等,匹配的关系

import { sum } from "./func";

// describe:分组
describe("test describe first", () => {
  // 测试sum方法结果是否等于3
  it("test func sun", () => {
    expect(sum(1, 2)).toBe(3);
  });

  it("test 1+1=2", () => {
    expect(1 + 1).toBe(2);
  });

  it("对象比较", () => {
    expect({ name: 1 }).toEqual({ name: 1 });
  });
});

it("测试不相等", () => {
  expect(1 + 1).not.toBe(3); // 1+1不等3
  expect(3).toBeLessThan(5); // 3<5
});

it("测试包含", () => {
  expect("hello").toContain("h");
  expect("hello").toMatch(/h/);
});

测试 DOM

it("测试删除DOM", () => {
  document.body.innerHTML = `<div><button></button></div>`;

  let button = document.querySelector("button");
  expect(button).not.toBe(null);

  // 自己写的移除的DOM方法
  removeNode(button);

  button = document.querySelector("button");
  expect(button).toBe(null);
});

异步

import { getDataCallback, getDataPromise } from "./func";

it("测试回调函数", (done) => {
  getDataCallback((data) => {
    expect(data).toEqual({ name: "callback" });
    done(); // 标识调用完成
  });
});

it("测试promise", () => {
  return getDataPromise().then((data) => {
    expect(data).toEqual({ name: "callback" });
  });
});

it("测试promise", async () => {
  const data = await getDataPromise();
  expect(data).toEqual({ name: "callback" });
});

对组件节点进行测试

// 判断组件是否有Button这个组件,因为不需要渲染子节点,所以使用shallow方法进行组件的渲染,因为props的list有两项,所以预期应该有两个Button组件。
it("should has Button", () => {
  const { wrapper } = setup();
  expect(wrapper.find("Button").length).toBe(2);
});

// 判断组件是否有button这个元素,因为button是Button组件里的元素,所有使用render方法进行渲染,预期也会找到连个button元素。
it("should render 2 item", () => {
  const { wrapper } = setupByRender();
  expect(wrapper.find("button").length).toBe(2);
});

// 判断组件的内容,使用mount方法进行渲染,然后使用forEach判断.item-text的内容是否和传入的值相等
it("should render item equal", () => {
  const { wrapper } = setupByMount();
  wrapper.find(".item-text").forEach((node, index) => {
    expect(node.text()).toBe(wrapper.props().list[index]);
  });
});

// 使用simulate来触发click事件,因为deleteTodo被mock了,所以可以用deleteTodo方法时候被调用来判断click事件是否被触发。
it("click item to be done", () => {
  const { wrapper } = setupByMount();
  wrapper.find("Button").at(0).simulate("click");
  expect(props.deleteTodo).toBeCalled();
});

测试组件生命周期

//使用spy替身的时候,在测试用例结束后,要对spy进行restore,不然这个spy会一直存在,并且无法对相同的方法再次进行spy。
it("calls componentDidMount", () => {
  // 个人理解: 组件原型身上复制一个生命周期[componentDidMount]分身出来
  const componentDidMountSpy = jest.spyOn(
    TodoList.prototype,
    "componentDidMount"
  );
  const { wrapper } = setup(); // setup是shallow模式创建的(往上翻)
  expect(componentDidMountSpy).toHaveBeenCalled();
  componentDidMountSpy.mockRestore();
});
// 使用spyOn来mock 组件的componentDidMount,替身函数要在组件渲染之前,所有替身函数要定义在setup执行之前,并且在判断以后要对替身函数restore,不然这个替身函数会一直存在,且被mock的那个函数无法被再次mock。

测试组件的内部函数

使用 instance 函数来取得组件的实例,并用 spyOn 方法来 mock 实例上的内部方法,然后用这个实例去调用那个内部方法,就可以用替身来判断这个内部函数是否被调用。

it("calls component handleTest", () => {
  // class中使用箭头函数来定义方法
  const { wrapper } = setup();
  // 需要对实例进行mock
  const spyFunction = jest.spyOn(wrapper.instance(), "handleTest");
  wrapper.instance().handleTest();
  expect(spyFunction).toHaveBeenCalled();
  spyFunction.mockRestore();
});

it("calls component handleTest2", () => {
  //在constructor使用bind来定义方法
  // 对组件的prototype进行mock
  const spyFunction = jest.spyOn(TodoList.prototype, "handleTest2");
  const { wrapper } = setup();
  wrapper.instance().handleTest2();
  expect(spyFunction).toHaveBeenCalled();
  spyFunction.mockRestore();
});

// 其实对生命周期或者内部函数的测试,可以通过一些state的改变进行判断,因为这些函数的调用一般都会对组件的state进行一些操作。

本文整理于以下 3 个链接,如有侵权,

https://github.com/divcssjs/blog/issues [留言删除]

https://blog.csdn.net/wu_xianqiang/article/details/102636926

https://blog.csdn.net/wangshang1320/article/details/101054926

https://blog.csdn.net/qq_52879678/article/details/116999359