51Testing软件测试论坛

 找回密码
 (注-册)加入51Testing

QQ登录

只需一步,快速开始

微信登录,快人一步

查看: 1091|回复: 0
打印 上一主题 下一主题

[原创] 单元测试如何在React 项目上进行?

[复制链接]
  • TA的每日心情
    无聊
    昨天 09:13
  • 签到天数: 935 天

    连续签到: 2 天

    [LV.10]测试总司令

    跳转到指定楼层
    1#
    发表于 2022-10-27 14:03:40 | 只看该作者 回帖奖励 |倒序浏览 |阅读模式
    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 命令,得到测试结果。



    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 :




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


    本帖子中包含更多资源

    您需要 登录 才可以下载或查看,没有帐号?(注-册)加入51Testing

    x
    分享到:  QQ好友和群QQ好友和群 QQ空间QQ空间 腾讯微博腾讯微博 腾讯朋友腾讯朋友
    收藏收藏
    回复

    使用道具 举报

    本版积分规则

    关闭

    站长推荐上一条 /1 下一条

    小黑屋|手机版|Archiver|51Testing软件测试网 ( 沪ICP备05003035号 关于我们

    GMT+8, 2024-4-24 04:36 , Processed in 0.068026 second(s), 23 queries .

    Powered by Discuz! X3.2

    © 2001-2024 Comsenz Inc.

    快速回复 返回顶部 返回列表