react生态下jest单元测试

举报
建帅小伙儿 发表于 2022/09/25 03:21:52 2022/09/25
2k+ 0 0
【摘要】 一:jest框架搭建 1.在本地创建一个目录jest_practice 2.使用编辑器VScode打开目录,紧接着在终端中打开,执行npm init 3.执行以下命令: 注意:这里我们使用cnpm去安装速度会更快,npm速度会很慢! a.建议使用npm install –g jest(不需要单个去安装依赖),...

一:jest框架搭建

1.在本地创建一个目录jest_practice

2.使用编辑器VScode打开目录,紧接着在终端中打开,执行npm init

image-1650460118214

3.执行以下命令:

注意:这里我们使用cnpm去安装速度会更快,npm速度会很慢!
a.建议使用npm install –g jest(不需要单个去安装依赖),修改package.json文件即可。

b.安装jest框架,以及依赖


       cnpm install --save-dev jest babel-jest babel-core babel-preset-env regenerator-runtime
       cnpm i --save-dev @babel/plugin-transform-runtime
       cnpm i --save-dev @babel/preset-env
       cnpm install @babel/runtime
   
  

image-1650460136860

4.打开package.json文件,修改jest:

"test": "jest --config jest.config.json --no-cache --colors --coverage"
  

5.搭建好之后需要写个demo来测试是否正确

image-1650460173217

如上图说明jest框架搭建成功,进入编写case主题
%stmts是语句覆盖率(statement coverage):是不是每个语句都执行了?
%Branch分支覆盖率(branch coverage):是不是每个if代码块都执行了?
%Funcs函数覆盖率(function coverage):是不是每个函数都调用了?
%Lines行覆盖率(line coverage):是不是每一行都执行了?

6.报告配置

需要在module层执行npm install jest-html-reporters --save-dev
新增jest.config.json


       {
        "reporters": [
          "default",
           ["./node_modules/jest-html-reporters", {
            "publicPath": "./test/html-reports",
            "filename": "report.htm",
            "pageTitle": "Test Report",
            "expand": true
           }]
         ]
       }
   
  

执行完case会在html-report目录下生成report.html报告

完整报告:image-1650460200972

报错详情:image-1650460209425

7.执行case方式:

三者都可以,需要安装yarn(cnpm install yarn)
1.npm test //执行全量test.js后缀的文件
2.yarn test --watchALL
3.jest Hook.test.js //执行单个case

二:开工须知

Jest背景:

Jest是 Facebook 发布的一个开源的、基于 Jasmine 框架的 JavaScript单元测试工具。提供了包括内置的测试环境DOM API支持、断言库、Mock库等,还包含了Spapshot Testing、 Instant Feedback等特性。

Enzyme:
React测试类库Enzyme提供了一套简洁强大的API,并通过jQuery风格的方式进行DOM处理,开发体验十分友好。不仅在开源社区有超高人气,同时也获得了React官方的推荐。

1.举例,被测函数:
文件名:Hook.js


       constructor() {
          this.init();
         }
        init() {
          this.a = 1;
          this.b = 1;
         }
        sum() {
          return this.a + this.b;
         }
       }
       module.exports = Hook;
   
  

文件名:Hook.test.js


       describe('hook', () => {
        const hook = new Hook();
        // 每个测试用例执行前都会还原数据,所以下面两个测试可以通过。
        beforeEach(() => {
           hook.init();
         });
        test('test hook 1', () => {
           hook.a = 2;
           hook.b = 2;
          expect(hook.sum()).toBe(4);
         });
        test('test hook 2', () => {
          expect(hook.sum()).toBe(2);// 测试通过
         });
       });
   
  

执行此目录下以test.js结尾的case :jest –colors –coverage 结果如下:
执行单个case:jest Hook.test.js –colors –coverage

image-1650460268621

会在html-report目录下生成report.html文件

image-1650460273870

2.SnapShot Testing(快照测试):

快照测试第一次运行的时候会将被测试ui组件在不同情况下的渲染结果保存一份快照文件。后面每次再运行快照测试时,都会和第一次的比较,若组件代码有所改变,则快照测试会失败,如果组件代码是最新的,优化过得代码,则需要更新快照,免得每次执行报错。

更新快照命令:jest --updateSnapshot

被测组件代码如下:


       //被测组件
       import React from 'react';
       const STATUS = {
        HOVERED: 'hovered',
        NORMAL: 'normal',
       };
       export default class Link extends React.Component {
        constructor() {
          super();
          this.state = {
            class: STATUS.NORMAL,
           };
         }
         _onMouseEnter = () => {
          this.setState({class: STATUS.HOVERED});
         };
         _onMouseLeave = () => {
          this.setState({class: STATUS.NORMAL});
         };
        render() {
          return (
            <a
              className={this.state.class}
              href={this.props.page || '#'}
              onMouseEnter={this._onMouseEnter}
              onMouseLeave={this._onMouseLeave}
             >
               {this.props.children}
            </a>
           );
         }
       }
   
  

快照测试case:


       import React from "react";
       import Link from "./Link.react";
       import renderer from "react-test-renderer";// react-test-renderer则负责将组件输出成 JSON 对象以方便我们遍历、断言或是进行 snapshot 测试
       //React 组件的 render 结果是一个组件树,并且整个树最终会被解析成一个纯粹由 HTML 元素构成的树形结构
       it("renders correctly", () => {
        const tree = renderer
           .create(<Link page="http://www.instagram.com">Instagram</Link>)
           .toJSON();
        expect(tree).toMatchSnapshot();
       });
       it("renders as an anchor when no page is set", () => {
        const tree = renderer.create(<Link>Facebook</Link>).toJSON();
        expect(tree).toMatchSnapshot();
       });
       it("properly escapes quotes", () => {
        const tree = renderer
           .create(<Link>{"\"Facebook\" \\'is \\ 'awesome'"}</Link>)
           .toJSON();
        expect(tree).toMatchSnapshot();
       });
       it("changes the class when hovered", () => {
        const component = renderer.create(
          <Link page="http://www.facebook.com">Facebook</Link>
         );
        let tree = component.toJSON();
        expect(tree).toMatchSnapshot();
        // manually trigger the callback
         tree.props.onMouseEnter();
        // re-rendering
         tree = component.toJSON();
        expect(tree).toMatchSnapshot();
        // manually trigger the callback
         tree.props.onMouseLeave();
        // re-rendering
         tree = component.toJSON();
        expect(tree).toMatchSnapshot();
       });
       it("renders correctly", () => {
        const tree = renderer
           .create(<Link page="https://prettier.io">Prettier</Link>)
           .toJSON();
        expect(tree).toMatchInlineSnapshot(`
        <a
        className="normal"
        href="https://prettier.io"
        onMouseEnter={[Function]}
        onMouseLeave={[Function]}
        >
        Prettier
        </a>
        `);
       });
       //1.通常,在对象中有一些字段需要快照,这些字段是生成的(比如id和Dates)。如果尝试对这些对象进行快照,它们将强制快照在每次运行时失败.
       //2.Jest允许为任何属性提供非对称匹配器。在写入或测试快照之前,将检查这些匹配器,然后将其保存到快照文件而不是接收到的值
       it('will check the matchers and pass', () => {
        const user = {
          createdAt: new Date(),
          id: Math.floor(Math.random() * 20),
          name: 'LeBron James',
         };
        expect(user).toMatchSnapshot({
          createdAt: expect.any(Date),
          id: expect.any(Number),
         });
       });
   
  

//1.通常,在对象中有一些字段需要快照,这些字段是生成的(比如id和Dates)。如果尝试对这些对象进行快照,它们将强制快照在每次运行时失败.//2.Jest允许为任何属性提供非对称匹配器。在写入或测试快照之前,将检查这些匹配器,然后将其保存到快照文件而不是接收到的值


       it('will check the matchers and pass', () => {
        const user = {
          createdAt: new Date(),
          id: Math.floor(Math.random() * 20),
          name: 'LeBron James',
         };
        expect(user).toMatchSnapshot({
          createdAt: expect.any(Date),
          id: expect.any(Number),
         });
       });
   
  

生成快照文件:

image-1650460351260

image-1650460359600

快照文件:


       // Jest Snapshot v1, https://goo.gl/fbAQLP
       exports[`changes the class when hovered 1`] = `
       <a
        className="normal"
        href="http://www.facebook.com"
        onMouseEnter={[Function]}
        onMouseLeave={[Function]}
       >
        Facebook
       </a>
       `;
       exports[`changes the class when hovered 2`] = `
       <a
        className="hovered"
        href="http://www.facebook.com"
        onMouseEnter={[Function]}
        onMouseLeave={[Function]}
       >
        Facebook
       </a>
       `;
       exports[`changes the class when hovered 3`] = `
       <a
        className="normal"
        href="http://www.facebook.com"
        onMouseEnter={[Function]}
        onMouseLeave={[Function]}
       >
        Facebook
       </a>
       `;
       exports[`properly escapes quotes 1`] = `
       <a
        className="normal"
        href="#"
        onMouseEnter={[Function]}
        onMouseLeave={[Function]}
       >
        "Facebook" \\'is \\ 'awesome'
       </a>
       `;
       exports[`renders as an anchor when no page is set 1`] = `
       <a
        className="normal"
        href="#"
        onMouseEnter={[Function]}
        onMouseLeave={[Function]}
       >
        Facebook
       </a>
       `;
       exports[`renders correctly 1`] = `
       <a
        className="normal"
        href="http://www.instagram.com"
        onMouseEnter={[Function]}
        onMouseLeave={[Function]}
       >
        Instagram
       </a>
       `;
       exports[`will check the matchers and pass 1`] = `
       Object {
        "createdAt": Any<Date>,
        "id": Any<Number>,
        "name": "LeBron James",
       }
       `;
   
  

更新快照命令:jest --updateSnapshot

3.组件测试

组件代码:example 1


       'user strict';
       function timeGame(callback){
        console.log('ready...go!!!');
        setTimeout(() => {
          console.log('Time is up ,please stop!!!');
           callback && callback();
         }, 1000);
       }
       module.exports = timeGame;
       //export default timeGame;
   
  

module.exports = timeGame;
//export default timeGame;

组件测试代码:


       import { jest } from '@jest/globals';
       import ReactTestUtils from 'react-dom/test-utils';
       'user strict';
       jest.useFakeTimers();
       describe('时间计时器', () => {
        test('wait 1 second before ending the game', () => {
          const timeGame = require('./timeGame');
          timeGame();
          expect(setTimeout).toHaveBeenCalledTimes(1);
          expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), 1000)
         });
        test('calls the callback after 1 second ', () => {
          const timeGame = require('./timeGame');
          const callback = jest.fn();
          timeGame(callback);
          expect(callback).not.toBeCalled();
           jest.runAllTimers();
          expect(callback).toBeCalled();
          expect(callback).toHaveBeenCalledTimes(1);
         });
       });
   
  

example2:

组件代码:


       import React from "react";
       class Button extends React.Component {
        constructor() {
          super();
          this.state = {
            disabled: false,
           };
          this.handClick = this.handClick.bind(this);
         }
        handClick() {
          if (this.state.disabled) {
            return;
           }
          if (this.props.onClick) {
            this.props.onClick();
           }
          this.setState({ disabled: true });
          setTimeout(() => {
            this.setState({ disabled: false });
           }, 200);
         }
        render() {
          return (
            <button className="my-button" onClick={this.handClick}>
               {this.props.children}
            </button>
           );
         }
       }
       export default Button;
   
  

组件测试代码:


       import React from 'react';
       //import TestRenderer from 'react-test-renderer';调用 TestRenderer 的 create 方法并传入要 render 的组件就可以获得一个 TestRenderer 的实例
       import { jest } from '@jest/globals';
       import ReactTestUtils from 'react-dom/test-utils';
       import Button from './Button.jsx';
       describe('组件测试', () => {
        it('测试应该被回调', () => {
          const onClickMock = jest.fn();
          const testInstance = ReactTestUtils.renderIntoDocument(
            <Button onClick={onClickMock}>hello</Button>
           );
          const buttonDom = ReactTestUtils.findRenderedDOMComponentWithClass(testInstance, 'my-button');
          ReactTestUtils.Simulate.click(buttonDom);
          expect(onClickMock).toHaveBeenCalled();
         });
        it('点击之后2秒之内是否可用', () => {
          const testInstance = ReactTestUtils.renderIntoDocument(<Button>hello</Button>);
          const buttonDom = ReactTestUtils.findRenderedDOMComponentWithClass(testInstance, 'my-button');
          ReactTestUtils.Simulate.click(buttonDom);
          expect(testInstance.state.disabled).toBeTruthy();
           jest.useFakeTimers();
           jest.advanceTimersByTime(100);
          expect(testInstance.state.disabled).toBeTruthy();
           jest.advanceTimersByTime(201);
          expect(testInstance.state.disabled).toBeTruthy();
         });
       });
   
  

4.函数测试


       //函数:去除空格
       function removeSpace(s) {
        if (s == undefined || s == "") {
            return "";
         } else {
            return s.replace(/(^\s*)|(\s*$)/g, "");
         }
       }
       export default removeSpace;
   
  

函数测试:


       import removeSpace from './removeSpace';
       //带空格的字符串
       test('Removal of space', function () {
        const string = ' camelot.china ';
        return expect(removeSpace(string)).toBe('camelot.china');
       });
       //字符串string = undefined
       test('string == undifined', () => {
        const string = undefined;
        return expect(removeSpace(string)).toBe("")
       });
       //字符串string = ""
       test('string = ""', () => {
        const string = "";
        return expect(removeSpace(string)).toBe("")
       });
   
  

image-1650460505677

image-1650460511831

5.mock测试


       //mock_fuction.js
       export default {
        sum(a , b){
          return a + b
         }
       };
   
  

       import React from 'react';
       import mock_function from './mock_fuction';
       import { jest } from '@jest/globals';
       import { object } from 'prop-types';
       //mock_fuction.test.js
       test('测试jest.fn()', () => {
        let mockFn = jest.fn();
        let result = mockFn(1, 2, 3);
        expect(result).toBeUndefined();
        expect(mockFn).toHaveBeenCalledWith(1, 2, 3);
        expect(mockFn).toBeCalled();
        expect(mockFn).toBeCalledTimes(1);
       });
       test('sum(5, 5) ', () => {
        expect(mock_function.sum(5, 5)).toBe(10);
       });
       test('测试jest.fn()返回固定值', () => {
        let mockFn = jest.fn().mockResolvedValue('default');
        expect(mockFn).toBe('default');
       });
       test('测试jest.fn()内部实现', () => {
        let mockFn = jest.fn((num1, num2) => {
          return num1 * num2;
         });
        expect(mockFn(9, 9)).toBe(100);
       });
       test('测试jest.fn()返回promise', async () => {
        let mockFn = jest.fn().mockResolvedValue('default');
        let result = await mockFn();
        expect(result).toBe('default');
        expect(Object.prototype.toString.call(mockFn())).toBe("[object Promise]")
       });
   
  

文章来源: blog.csdn.net,作者:懿曲折扇情,版权归原作者所有,如需转载,请联系作者。

原文链接:blog.csdn.net/qq_41332844/article/details/126837418

【版权声明】本文为华为云社区用户转载文章,如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

作者其他文章

评论(0

抱歉,系统识别当前为高风险访问,暂不支持该操作

    全部回复

    上滑加载中

    设置昵称

    在此一键设置昵称,即可参与社区互动!

    *长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

    *长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。