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

发表于:2022-10-27 09:43

字体: | 上一篇 | 下一篇 | 我要投稿

 作者:前端西瓜哥    来源:前端西瓜哥

  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:是否小于某个值,可以做性能测试,执行某个函数几千次,时间不能高于某个值。
  你可以用 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);
  });
  使用 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 作为流行的测试库值得一试。
  本文内容不用于商业目的,如涉及知识产权问题,请权利人联系51Testing小编(021-64471599-8017),我们将立即处理
《2023软件测试行业现状调查报告》独家发布~

关注51Testing

联系我们

快捷面板 站点地图 联系我们 广告服务 关于我们 站长统计 发展历程

法律顾问:上海兰迪律师事务所 项棋律师
版权所有 上海博为峰软件技术股份有限公司 Copyright©51testing.com 2003-2024
投诉及意见反馈:webmaster@51testing.com; 业务联系:service@51testing.com 021-64471599-8017

沪ICP备05003035号

沪公网安备 31010102002173号