4

Jest:给你的 React 项目加上单元测试

 1 year ago
source link: https://www.51cto.com/article/721289.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

Jest:给你的 React 项目加上单元测试

作者:前端西瓜哥 2022-10-26 08:00:49
单元测试(Unit Testing),指的是对程序中的模块(最小单位)进行检查和验证。比如一个函数、一个类、一个组件,它们都是模块。
538de3831d1e710e41115297390818ebd86d60.png

大家好,我是前端西瓜哥。

Jest 是一款轻量的 JavaScript 测试框架,它的卖点是简单好用,由 facebook 出品。本文就简单讲讲如何使用 Jest 对 React 组件进行测试。

为什么需要单元测试?

单元测试(Unit Testing),指的是对程序中的模块(最小单位)进行检查和验证。比如一个函数、一个类、一个组件,它们都是模块。

使用单元测试的优点:

  • 更好地交付高质量代码。代码不可能没有 bug,测试能帮你找出来;
  • 更容易重构。我们不愿意去重构代码,不去还技术债,很大原因是测试覆盖率不足,害怕遗漏一些边边角角的逻辑,导致线上发生重大事故;
  • 可以用测试描述模块功能。注释和文档容易忘记修改,但测试用例的描述永远是准确的,因为不对就无法通过测试;
  • 可测试性好的代码,往往可维护性更好。比如某个模块很难测试,是因为它和其他模块高度耦合,此时你需要替换为依赖注入的方式来管理模块依赖。

Jest 判定测试脚本

Jest 需要 确认哪些是测试文件,默认判断测试文件的逻辑是:

  • __tests__​ 文件夹下的 .js  .jsx、.ts 、.tsx 为后缀的文件。
  • test.js​ 、spec.js 或其他文件后缀  .jsx、.ts 、.tsx。

可以通过设置  Jest 配置文件的 testMatch 或 testRegex 选项进行修改,或者 package.json 下的 "jest" 属性。

Jest 基本使用

我们先写一个简单的函数,作为被测试的模块。

function sum(a, b) {
  return a + b;
}
export default sum;

然后我们用 Jest 来做测试。

import sum from './sum';

test('1 + 1 应该等于 2', () => {
  expect(sum(1, 1)).toBe(2);
});

然后执行 jest 命令,得到测试结果。

图片

test 方法创建了一个测试的作用域,该方法有三个参数:

  1. 测试的描述。
  2. 我们写测试代码的函数。
  3. 测试超时时间,默认为 5 秒,有些测试是异步的,我们需要等待。

test 方法有一个别名叫做 it,二者的功能是一致的,只是语义不同。通常用 test,但在某些情况下更适合用 it。这种情况就是 it 可以和描述语句拼成一句话的时候,比如:

it('should be true', () => { /* 测试内容 */});

it 方法和后面的 should be true 拼成了一句主语为 it 的句子,语义更好。

我们通常使用 expect 来测试一个模块的逻辑是否符合预期。expect 会将模块返回的结果封装成一个对象,然后提供非常丰富的方法做测试。

比如 toBe 就可以做 Object.is 的对比测试。

// sum(1, 1) 的结果是否为 2
expect(sum(1, 1)).toBe(2);

expect 的实现思路大致为:

function expect(value) {
  return {
    toBe(comparedValue) {
      if (Object.is(value, comparedValue)) {
        // 记录测试成功
      } else {
        // 记录测试失败
      }
    },
    // 其他 API
    toBeTruthy() { /* ... */ },
    // ...
  }
}

利用了闭包。

还有一些其他的 toXX API,我们称为 matcher。比如:

  • toEqual:对对象进行深递归的 Object.is 对比。
  • toBeTruthy:是否为真值。
  • not:对结果取反,比如expect(val).not.beBe(otherVal) 表示两值不相等才通过测试。
  • toContain:数组中是否含有某个元素。
  • toBeLessThan:是否小于某个值,可以做性能测试,执行某个函数几千次,时间不能高于某个值。

更多 API 可以看文档:

​https://jestjs.io/docs/expect。​

你可以用 describe 方法将多个相关的 test 组合起来,这样能让你的测试用例更好地被组织,测试报告输出也更有条理。

describe('一个有多个属性的对象的测试', () => {
  test('test 1', async () => {
    expect(obj.a).toBeTruthy();
  });

  test('test 2', async () => {
    expect(obj.b).toBeTruthy();
  });
});

describe 里面可以嵌套 describe,即组里面还可以有组。

如果使用异步测试,需要将 Promise 作为返回值。

test('请求测试', () => {
  return getData().then(res {
    expect(res.data.success).toBe(true);
  })
})

或使用 async / await。

test('请求测试', async () => {
  const res = await getData();
  expect(res.data.success).toBe(true);
})

也支持回调函数风格的测试,你需要调用函数传入的 done 函数来表明测试完成:

test('异步测试', done => {
  setTimeout(() {
    expect('前端西瓜哥').toBeTruthy();
    done();
  }, 2000);
});

生命周期函数

beforeAll,在当前文件的正式开始测试前执行一次,适合做一些每次 test 前都要做的初始化操作,比如数据库的清空以及初始化。

beforeEach,在当前文件的每个 test 执行前都调用一次。

afterAll,在当前文件所有测试结束后执行一次,适合做一些收尾工作,比如将数据库清空。

afterEach,在当前文件的每个 test 执行完后都调用一次。

React Testing Library

本文不讲解安装和配置,我们先用 CreateReactApp 来搭建项目,并使用 TypeScript 模板。

yarn create react-app jest-app --template typescript

执行单元测试的命令为:

yarn test

CreateReactApp 内置了 Jest,但 Jest 本身并不支持 React 组件的测试 API,需要使用另外一个内置的 React Testing Library 库来测试  React 组件。

React Testing Library 是 以用户为角度 的测试库,能够模拟浏览器的 DOM,将 React 组件挂载上去后,我们使用其提供的一些模拟用户操作的 API 进行测试。

React Testing Library 的哲学是:

测试的写法越是接近应用被使用的方式,我们就越有自信将其交付给客户。

CreateReactApp 预置模板的 App.test.tsx 使用了 React Testing Library。

import React from 'react';
import { render, screen } from '@testing-library/react';
import App from './App';

test('renders learn react link', () => {
  render(<App);
  const linkElement = screen.getByText(/learn react/i);
  expect(linkElement).toBeInTheDocument();
});

Enzyme

另一种比较流行的测试 React 组件的框架是  Enzyme,它的 API 简洁优雅,能够用类似 JQuery 的语法,对开发非常友好。Enzyme 由 Airbnd 出品,但目前已经不怎么维护了。

为此,你需要装一些包:

yarn add -D enzyme enzyme-adapter-react-16

如果你使用了 TS,你还得补上类型声明。

yarn add -D @types/enzyme @types/enzyme-adapter-react-16
import Enzyme, { shallow } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
import Button from '../button';
Enzyme.configure({ adapter: new Adapter() });
it('Button with children', () => {
  const text = 'confirm';
  const btn = shallow(<Button>{text}</Button>);
  expect(btn.text()).toBe(text);
});

目前(2022.10.25) enzyme 官方只支持到 React 16,Enzyme 已死:

​https://dev.to/wojtekmaj/enzyme-is-dead-now-what-ekl。​

使用 Jest 测试 React 组件

我们先实现一个简单的 Button 组件。

import { CSSProperties, MouseEvent, FC } from 'react';
import classNames from 'classnames';
import './style.scss';

const clsPrefix = 'xigua-ui-btn';

export type ButtonProps = {
  type?: 'primary' | 'default'
  size?: 'large' | 'middle' | 'small';
  disabled?: boolean;
  children?: React.ReactNode;
  onClick?: (event: MouseEvent) => void;
  style?: CSSProperties;
  className?: string;
}

const Button: FC<ButtonProps> = (props) => {
  const {
    type = 'default',
    size = 'middle',
    disabled = false,
    children,
    onClick,
    style,
    className,
  } = props;

  const mixedClassName = classNames(
    clsPrefix,
    `${clsPrefix}-${type}`,
    `${clsPrefix}-${size}`,
    className
  );

  return (
    <button
      style={style}
      className={mixedClassName}
      disabled={disabled}
      onClick={onClick}
    >
      {children}
    </button>
  );
};

export default Button;

然后我们创建一个 button.test.tsx 测试文件。我们使用 React Testing Library。

我们写个测试。

import { render, screen } from '@testing-library/react';
import Button from '../button';

test('Button with children', () => {
  const text = 'confirm Btn';
  render(<Button>{text}</Button>);

  screen.debug();
});

render 方法会将 React 组件挂载到虚拟的文档树上。screen.debug() 用于调试,能让我们看到虚拟树的完整结构。

<body>
  <div>
    <button
      class="xigua-ui-btn xigua-ui-btn-default xigua-ui-btn-middle"
    >
      confirm Btn
    </button>
  </div>
</body>

测试 Button 的文本内容是否正常显示:

test('Button with children', () => {
  const text = 'confirm Btn';
  // 渲染 Button 组件
  render(<Button>{text}</Button>);
  
  // 找到内容为 text 的元素
  const BtnElement = screen.getByText(text);
  // 测试元素是否在 Document 上
  expect(BtnElement).toBeInTheDocument();
});

测试 Button 的 onClick 能否正常触发:

test('Button click', () => {
  let toggle = false;
  render(<Button onClick={() => { toggle = true; }} />);

  // 找到第一个 button 元素,然后触发它的点击事件 
  fireEvent.click(screen.getByRole('button'));
  // 看看 toggle 变量是否变成 true
  expect(toggle).toBe(true);
});

测试 Button 的 className 是否成功添加:

test('Button with custom className', () => {
  const customCls = 'customBtn';
  render(<button className={customCls} />);

  // 找到按钮元素
  const btn = screen.getByRole('button');
  // 元素的 className 列表上是否有我们传入的 className
  expect(btn).toHaveClass(customCls);
});

​https://github.com/F-star/xigua-ui/blob/main/src/components/button/​tests/button.test.tsx。

执行 yarn test :

图片

为了让代码更健壮,做模块的单元测试还是有必要的,Jest 作为流行的测试库值得一试。

责任编辑:姜华 来源: 前端西瓜哥

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK