51Testing软件测试论坛

标题: 单元测试如何在React 项目上进行? [打印本页]

作者: lsekfe    时间: 2022-10-27 14:03
标题: 单元测试如何在React 项目上进行?
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 基本使用
  我们先写一个简单的函数,作为被测试的模块。
  1. function sum(a, b) {

  2.     return a + b;

  3.   }

  4.   export default sum;
复制代码
然后我们用 Jest 来做测试。

  1.  import sum from './sum';

  2.   test('1 + 1 应该等于 2', () => {

  3.     expect(sum(1, 1)).toBe(2);

  4.   });
复制代码


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

[attach]144363[/attach]

test 方法创建了一个测试的作用域,该方法有三个参数:
  1. 测试的描述。
  2. 我们写测试代码的函数。
  3. 测试超时时间,默认为 5 秒,有些测试是异步的,我们需要等待。
  test 方法有一个别名叫做 it,二者的功能是一致的,只是语义不同。通常用 test,但在某些情况下更适合用 it。这种情况就是 it 可以和描述语句拼成一句话的时候,比如:

  1.  it('should be true', () => { /* 测试内容 */});
复制代码
it 方法和后面的 should be true 拼成了一句主语为 it 的句子,语义更好。
  我们通常使用 expect 来测试一个模块的逻辑是否符合预期。expect 会将模块返回的结果封装成一个对象,然后提供非常丰富的方法做测试。
  比如 toBe 就可以做 Object.is 的对比测试。

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

  2.   expect(sum(1, 1)).toBe(2);
复制代码
expect 的实现思路大致为:
  1. function expect(value) {

  2.     return {

  3.       toBe(comparedValue) {

  4.         if (Object.is(value, comparedValue)) {

  5.           // 记录测试成功

  6.         } else {

  7.           // 记录测试失败

  8.         }

  9.       },

  10.       // 其他 API

  11.       toBeTruthy() { /* ... */ },

  12.       // ...

  13.     }

  14.   }
复制代码
利用了闭包。
  还有一些其他的 toXX API,我们称为 matcher。比如:
  ·toEqual:对对象进行深递归的 Object.is 对比。
  · toBeTruthy:是否为真值。
  · not:对结果取反,比如expect(val).not.beBe(otherVal) 表示两值不相等才通过测试。
  · toContain:数组中是否含有某个元素。
  · toBeLessThan:是否小于某个值,可以做性能测试,执行某个函数几千次,时间不能高于某个值。
  你可以用 describe 方法将多个相关的 test 组合起来,这样能让你的测试用例更好地被组织,测试报告输出也更有条理。

  1. describe('一个有多个属性的对象的测试', () => {

  2.     test('test 1', async () => {

  3.       expect(obj.a).toBeTruthy();

  4.     });

  5.     test('test 2', async () => {

  6.       expect(obj.b).toBeTruthy();

  7.     });

  8.   });
复制代码
describe 里面可以嵌套 describe,即组里面还可以有组。
  异步测试
  如果使用异步测试,需要将 Promise 作为返回值。


  1. test('请求测试', () => {

  2.     return getData().then(res {

  3.       expect(res.data.success).toBe(true);

  4.     })

  5.   })
复制代码


或使用 async / await。

  1. test('请求测试', async () => {

  2.     const res = await getData();

  3.     expect(res.data.success).toBe(true);

  4.   })
复制代码


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

  1. test('异步测试', done => {

  2.     setTimeout(() {

  3.       expect('前端西瓜哥').toBeTruthy();

  4.       done();

  5.     }, 2000);

  6.   });
复制代码
生命周期函数
  beforeAll,在当前文件的正式开始测试前执行一次,适合做一些每次 test 前都要做的初始化操作,比如数据库的清空以及初始化。
  beforeEach,在当前文件的每个 test 执行前都调用一次。
  afterAll,在当前文件所有测试结束后执行一次,适合做一些收尾工作,比如将数据库清空。
  afterEach,在当前文件的每个test 执行完后都调用一次。
 React Testing Library
  本文不讲解安装和配置,我们先用 CreateReactApp 来搭建项目,并使用 TypeScript 模板。


  1.  yarn create react-app jest-app --template typescript
复制代码


执行单元测试的命令为:

  1.  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。


  1.  import React from 'react';

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

  3.   import App from './App';

  4.   test('renders learn react link', () => {

  5.     render(<App);

  6.     const linkElement = screen.getByText(/learn react/i);

  7.     expect(linkElement).toBeInTheDocument();

  8.   });
复制代码
Enzyme
  另一种比较流行的测试 React 组件的框架是  Enzyme,它的 API 简洁优雅,能够用类似 JQuery 的语法,对开发非常友好。Enzyme 由 Airbnd 出品,但目前已经不怎么维护了。
  为此,你需要装一些包:

  1. yarn add -D enzyme enzyme-adapter-react-16
复制代码
如果你使用了 TS,你还得补上类型声明。
  1.  yarn add -D @types/enzyme @types/enzyme-adapter-react-16
复制代码


示例:

  1. import Enzyme, { shallow } from 'enzyme';

  2.   import Adapter from 'enzyme-adapter-react-16';

  3.   import Button from '../button';

  4.   Enzyme.configure({ adapter: new Adapter() });

  5.   it('Button with children', () => {

  6.     const text = 'confirm';

  7.     const btn = shallow(<Button>{text}</Button>);

  8.     expect(btn.text()).toBe(text);

  9.   });
复制代码
使用 Jest 测试 React 组件
  我们先实现一个简单的 Button 组件。


  1.  import { CSSProperties, MouseEvent, FC } from 'react';

  2.   import classNames from 'classnames';

  3.   import './style.scss';

  4.   const clsPrefix = 'xigua-ui-btn';

  5.   export type ButtonProps = {

  6.     type?: 'primary' | 'default'

  7.     size?: 'large' | 'middle' | 'small';

  8.     disabled?: boolean;

  9.     children?: React.ReactNode;

  10.     onClick?: (event: MouseEvent) => void;

  11.     style?: CSSProperties;

  12.     className?: string;

  13.   }

  14.   const Button: FC<ButtonProps> = (props) => {

  15.     const {

  16.       type = 'default',

  17.       size = 'middle',

  18.       disabled = false,

  19.       children,

  20.       onClick,

  21.       style,

  22.       className,

  23.     } = props;

  24.     const mixedClassName = classNames(

  25.       clsPrefix,

  26.       `${clsPrefix}-${type}`,

  27.       `${clsPrefix}-${size}`,

  28.       className

  29.     );

  30.     return (

  31.       <button

  32.         style={style}

  33.         className={mixedClassName}

  34.         disabled={disabled}

  35.         onClick={onClick}

  36.       >

  37.         {children}

  38.       </button>

  39.     );

  40.   };

  41.   export default Button;
复制代码


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


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

  2.   import Button from '../button';

  3.   test('Button with children', () => {

  4.     const text = 'confirm Btn';

  5.     render(<Button>{text}</Button>);

  6.     screen.debug();

  7.   });
复制代码


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

  1. <body>

  2.     <div>

  3.       <button

  4.         class="xigua-ui-btn xigua-ui-btn-default xigua-ui-btn-middle"

  5.       >

  6.         confirm Btn

  7.       </button>

  8.     </div>

  9.   </body>
复制代码


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

  1. test('Button with children', () => {

  2.     const text = 'confirm Btn';

  3.     // 渲染 Button 组件

  4.     render(<Button>{text}</Button>);

  5.     

  6.     // 找到内容为 text 的元素

  7.     const BtnElement = screen.getByText(text);

  8.     // 测试元素是否在 Document 上

  9.     expect(BtnElement).toBeInTheDocument();

  10.   });
复制代码

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

  1. test('Button click', () => {

  2.     let toggle = false;

  3.     render(<Button onClick={() => { toggle = true; }} />);

  4.     // 找到第一个 button 元素,然后触发它的点击事件

  5.     fireEvent.click(screen.getByRole('button'));

  6.     // 看看 toggle 变量是否变成 true

  7.     expect(toggle).toBe(true);

  8.   });
复制代码

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

  1. test('Button with custom className', () => {

  2.     const customCls = 'customBtn';

  3.     render(<button className={customCls} />);

  4.     // 找到按钮元素

  5.     const btn = screen.getByRole('button');

  6.     // 元素的 className 列表上是否有我们传入的 className

  7.     expect(btn).toHaveClass(customCls);

  8.   });
复制代码


源码:
  https://github.com/F-star/xigua- ... sts/button.test.tsx
  执行 yarn test :


[attach]144364[/attach]

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







欢迎光临 51Testing软件测试论坛 (http://bbs.51testing.com/) Powered by Discuz! X3.2