最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • 那些年错过的React组件单元测试(上)

    正文概述 掘金(前端森林)   2021-03-25   532

    ? 写在前面

    关于前端单元测试,其实两年前我就已经关注了,但那时候只是简单的知道断言,想着也不是太难的东西,项目中也没有用到,然后就想当然的认为自己就会了。

    两年后的今天,部门要对以往的项目补加单元测试。真到了开始着手的时候,却懵了 ?

    我以为的我以为却把自己给坑了,我发现自己对于前端单元测试一无所知。然后我翻阅了大量的文档,发现基于dva的单元测试文档比较少,因此在有了一番实践之后,我梳理了几篇文章,希望对于想使用 Jest 进行 React + Dva + Antd 单元测试的你能有所帮助。文章内容力求深入浅出,浅显易懂~

    ? 前端自动化测试产生的背景

    在开始介绍jest之前,我想有必要简单阐述一下关于前端单元测试的一些基础信息。

    • 为什么要进行测试?

      在 2021 年的今天,构建一个复杂的web应用对于我们来说,并非什么难事。因为有足够多优秀的的前端框架(比如 ReactVue);以及一些易用且强大的UI库(比如 Ant DesignElement UI)为我们保驾护航,极大地缩短了应用构建的周期。但是快速迭代的过程中却产生了大量的问题:代码质量(可读性差、可维护性低、可扩展性低)低,频繁的产品需求变动(代码变动影响范围不可控)等。

      因此单元测试的概念在前端领域应运而生,通过编写单元测试可以确保得到预期的结果,提高代码的可读性,如果依赖的组件有修改,受影响的组件也能在测试中及时发现错误。

    • 测试类型又有哪些呢?

      一般常见的有以下四种:

      • 单元测试
      • 功能测试
      • 集成测试
      • 冒烟测试
    • 常见的开发模式呢?

      • TDD: 测试驱动开发
      • BDD: 行为驱动测试

    ? 技术方案

    针对项目本身使用的是React + Dva + Antd的技术栈,单元测试我们用的是Jest + Enzyme结合的方式。

    Jest

    关于Jest,我们参考一下其Jest 官网,它是Facebook开源的一个前端测试框架,主要用于ReactReact Native的单元测试,已被集成在create-react-app中。Jest特点:

    • 零配置
    • 快照
    • 隔离
    • 优秀的 api
    • 快速且安全
    • 代码覆盖率
    • 轻松模拟
    • 优秀的报错信息

    Enzyme

    EnzymeAirbnb开源的React测试工具库,提供了一套简洁强大的API,并内置Cheerio,同时实现了jQuery风格的方式进行DOM处理,开发体验十分友好。在开源社区有超高人气,同时也获得了React官方的推荐。

    ? Jest

    本篇文章我们着重来介绍一下Jest,也是我们整个React单元测试的根基。

    环境搭建

    安装

    安装JestEnzyme。如果React的版本是15或者16,需要安装对应的enzyme-adapter-react-15enzyme-adapter-react-16并配置。

    /**
     * setup
     *
     */
    
    import Enzyme from "enzyme"
    import Adapter from "enzyme-adapter-react-16"
    Enzyme.configure({ adapter: new Adapter() })
    

    jest.config.js

    可以运行npx jest --init在根目录生成配置文件jest.config.js

    /*
     * For a detailed explanation regarding each configuration property, visit:
     * https://jestjs.io/docs/en/configuration.html
     */
    
    module.exports = {
      // All imported modules in your tests should be mocked automatically
      // automock: false,
    
      // Automatically clear mock calls and instances between every test
      clearMocks: true,
    
      // Indicates whether the coverage information should be collected while executing the test
      // collectCoverage: true,
    
      // An array of glob patterns indicating a set of files for which coverage information should be collected
      collectCoverageFrom: ["src/**/*.{js,jsx,ts,tsx}", "!src/**/*.d.ts"],
    
      // The directory where Jest should output its coverage files
      coverageDirectory: "coverage",
    
      // An array of regexp pattern strings used to skip coverage collection
      // coveragePathIgnorePatterns: [
      //   "/node_modules/"
      // ],
    
    
      // An array of directory names to be searched recursively up from the requiring module's location
      moduleDirectories: ["node_modules", "src"],
    
      // An array of file extensions your modules use
      moduleFileExtensions: ["js", "json", "jsx", "ts", "tsx"],
    
    
      // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader
      // modulePathIgnorePatterns: [],
    
      // Automatically reset mock state between every test
      // resetMocks: false,
    
      // Reset the module registry before running each individual test
      // resetModules: false,
    
      // Automatically restore mock state between every test
      // restoreMocks: false,
    
      // The root directory that Jest should scan for tests and modules within
      // rootDir: undefined,
    
      // A list of paths to directories that Jest should use to search for files in
      // roots: [
      //   "<rootDir>"
      // ],
    
      // The paths to modules that run some code to configure or set up the testing environment before each test
      // setupFiles: [],
    
      // A list of paths to modules that run some code to configure or set up the testing framework before each test
      setupFilesAfterEnv: [
        "./node_modules/jest-enzyme/lib/index.js",
        "<rootDir>/src/utils/testSetup.js",
      ],
    
      // The test environment that will be used for testing
      testEnvironment: "jest-environment-jsdom",
    
      // Options that will be passed to the testEnvironment
      // testEnvironmentOptions: {},
    
      // The glob patterns Jest uses to detect test files
      testMatch: ["**/?(*.)+(spec|test).[tj]s?(x)"],
    
      // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped
      // testPathIgnorePatterns: [
      //   "/node_modules/"
      // ],
    
    
      // A map from regular expressions to paths to transformers
      // transform: undefined,
    
      // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation
      transformIgnorePatterns: ["/node_modules/", "\\.pnp\\.[^\\/]+$"],
    }
    
    

    这里只是列举了常用的配置项:

    • automock: 告诉 Jest 所有的模块都自动从 mock 导入.
    • clearMocks: 在每个测试前自动清理 mock 的调用和实例 instance
    • collectCoverage: 是否收集测试时的覆盖率信息
    • collectCoverageFrom: 生成测试覆盖报告时检测的覆盖文件
    • coverageDirectory: Jest 输出覆盖信息文件的目录
    • coveragePathIgnorePatterns: 排除出 coverage 的文件列表
    • coverageReporters: 列出包含 reporter 名字的列表,而 Jest 会用他们来生成覆盖报告
    • coverageThreshold: 测试可以允许通过的阈值
    • moduleDirectories: 模块搜索路径
    • moduleFileExtensions:代表支持加载的文件名
    • testPathIgnorePatterns:用正则来匹配不用测试的文件
    • setupFilesAfterEnv:配置文件,在运行测试案例代码之前,Jest 会先运行这里的配置文件来初始化指定的测试环境
    • testMatch: 定义被测试的文件
    • transformIgnorePatterns: 设置哪些文件不需要转译
    • transform: 设置哪些文件中的代码是需要被相应的转译器转换成 Jest 能识别的代码,Jest 默认是能识别 JS 代码的,其他语言,例如 Typescript、CSS 等都需要被转译。

    匹配器

    • toBe(value):使用 Object.is 来进行比较,如果进行浮点数的比较,要使用 toBeCloseTo
    • not:取反
    • toEqual(value):用于对象的深比较
    • toContain(item):用来判断 item 是否在一个数组中,也可以用于字符串的判断
    • toBeNull(value):只匹配 null
    • toBeUndefined(value):只匹配 undefined
    • toBeDefined(value):与 toBeUndefined 相反
    • toBeTruthy(value):匹配任何语句为真的值
    • toBeFalsy(value):匹配任何语句为假的值
    • toBeGreaterThan(number): 大于
    • toBeGreaterThanOrEqual(number):大于等于
    • toBeLessThan(number):小于
    • toBeLessThanOrEqual(number):小于等于
    • toBeInstanceOf(class):判断是不是 class 的实例
    • resolves:用来取出 promise 为 fulfilled 时包裹的值,支持链式调用
    • rejects:用来取出 promise 为 rejected 时包裹的值,支持链式调用
    • toHaveBeenCalled():用来判断 mock function 是否被调用过
    • toHaveBeenCalledTimes(number):用来判断 mock function 被调用的次数
    • assertions(number):验证在一个测试用例中有 number 个断言被调用

    命令行工具的使用

    在项目package.json文件添加如下script:

    "scripts": {
        "start": "node bin/server.js",
        "dev": "node bin/server.js",
        "build": "node bin/build.js",
        "publish": "node bin/publish.js",
    ++  "test": "jest --watchAll",
    },
    

    此时运行npm run test: 那些年错过的React组件单元测试(上)

    我们发现有以下几种模式:

    • f: 只会测试之前没有通过的测试用例
    • o: 只会测试关联的并且改变的文件(需要使用 git)(jest --watch 可以直接进入该模式)
    • p: 测试文件名包含输入的名称的测试用例
    • t: 测试用例的名称包含输入的名称的测试用例
    • a: 运行全部测试用例

    在测试过程中,你可以切换适合的模式。

    钩子函数

    类似于 react 或者 vue 的生命周期,一共有四种:

    • beforeAll():所有测试用例执行之前执行的方法
    • afterAll():所有测试用例跑完以后执行的方法
    • beforeEach():在每个测试用例执行之前需要执行的方法
    • afterEach():在每个测试用例执行完后执行的方法

    这里,我以项目中的一个基础 demo 来演示一下具体使用:

    Counter.js

    export default class Counter {
      constructor() {
        this.number = 0
      }
      addOne() {
        this.number += 1
      }
      minusOne() {
        this.number -= 1
      }
    }
    

    Counter.test.js

    import Counter from './Counter'
    const counter = new Counter()
    
    test('测试 Counter 中的 addOne 方法', () => {
      counter.addOne()
      expect(counter.number).toBe(1)
    })
    
    test('测试 Counter 中的 minusOne 方法', () => {
      counter.minusOne()
      expect(counter.number).toBe(0)
    })
    

    运行npm run test: 那些年错过的React组件单元测试(上)

    通过第一个测试用例加 1,number的值为 1,当第二个用例减 1 的时候,结果应该是 0。但是这样两个用例间相互干扰不好,可以通过 Jest 的钩子函数来解决。修改测试用例:

    import Counter from "../../../src/utils/Counter";
    let counter = null
    
    beforeAll(() => {
      console.log('BeforeAll')
    })
    
    beforeEach(() => {
      console.log('BeforeEach')
      counter = new Counter()
    })
    
    afterEach(() => {
      console.log('AfterEach')
    })
    
    afterAll(() => {
      console.log('AfterAll')
    })
    
    test('测试 Counter 中的 addOne 方法', () => {
      counter.addOne()
      expect(counter.number).toBe(1)
    })
    test('测试 Counter 中的 minusOne 方法', () => {
      counter.minusOne()
      expect(counter.number).toBe(-1)
    })
    

    运行npm run test: 那些年错过的React组件单元测试(上)

    可以清晰的看到对应钩子的执行顺序:

    beforeAll > (beforeEach > afterEach)(单个用例都会依次执行) > afterAll

    除了以上这些基础知识外,其实还有异步代码的测试、Mock、Snapshot 快照测试等,这些我们会在下面 React 的单元测试示例中依次讲解。

    异步代码的测试

    众所周知,JS中充满了异步代码。

    正常情况下测试代码是同步执行的,但当我们要测的代码是异步的时候,就会有问题了:test case实际已经结束了,然而我们的异步代码还没有执行,从而导致异步代码没有被测到。

    那怎么办呢?

    对于当前测试代码来说,异步代码什么时候执行它并不知道,因此解决方法很简单。当有异步代码的时候,测试代码跑完同步代码后不立即结束,而是等结束的通知,当异步代码执行完后再告诉jest:“好了,异步代码执行完了,你可以结束任务了”。

    jest提供了三种方案来测试异步代码,下面我们分别来看一下。

    done 关键字

    当我们的test函数中出现了异步回调函数时,可以给test函数传入一个done参数,它是一个函数类型的参数。如果test函数传入了donejest就会等到done被调用才会结束当前的test case,如果done没有被调用,则该test自动不通过测试。

    import { fetchData } from './fetchData'
    test('fetchData 返回结果为 { success: true }', done => {
      fetchData(data => {
        expect(data).toEqual({
          success: true
        })
        done()
      })
    })
    

    上面的代码中,我们给test函数传入了done参数,在fetchData的回调函数中调用了done。这样,fetchData的回调中异步执行的测试代码就能够被执行。

    但这里我们思考一种场景:如果使用done来测试回调函数(包含定时器场景,如setTimeout),由于定时器我们设置了 一定的延时(如 3s)后执行,等待 3s 后会发现测试通过了。那假如 setTimeout 设置为几百秒,难道我们也要在 Jest 中等几百秒后再测试吗?

    显然这对于测试的效率是大打折扣的!!

    jest中提供了诸如jest.useFakeTimers()jest.runAllTimers()toHaveBeenCalledTimesjest.advanceTimersByTimeapi来处理这种场景。

    返回 Promise

    如果代码中使用了Promise,则可以通过返回Promise来处理异步代码,jest会等该promise的状态转为resolve时才会结束,如果promisereject了,则该测试用例不通过。

    // 假设 user.getUserById(参数id) 返回一个promise
    it('测试promise成功的情况', () => {
      expect.assertions(1);
      return user.getUserById(4).then((data) => {
        expect(data).toEqual('Cosen');
      });
    });
    it('测试promise错误的情况', () => {
      expect.assertions(1);
      return user.getUserById(2).catch((e) => {
        expect(e).toEqual({
          error: 'id为2的用户不存在',
        });
      });
    });
    
    

    注意,上面的第二个测试用例可用于测试promise返回reject的情况。这里用.catch来捕获promise返回的reject,当promise返回reject时,才会执行expect语句。而这里的expect.assertions(1)用于确保该测试用例中有一个expect被执行了。

    对于Promise的情况,jest还提供了一对匹配符resolves/rejects,其实只是上面写法的语法糖。上面的代码用匹配符可以改写为:

    // 使用'.resolves'来测试promise成功时返回的值
    it('使用'.resolves'来测试promise成功的情况', () => {
      return expect(user.getUserById(4)).resolves.toEqual('Cosen');
    });
    // 使用'.rejects'来测试promise失败时返回的值
    it('使用'.rejects'来测试promise失败的情况', () => {
      expect.assertions(1);
      return expect(user.getUserById(2)).rejects.toEqual({
        error: 'id为2的用户不存在',
      });
    });
    

    async/await

    我们知道async/await其实是Promise的语法糖,可以更优雅地写异步代码,jest中也支持这种语法。

    我们把上面的代码改写一下:

    // 使用async/await来测试resolve
    it('async/await来测试resolve', async () => {
      expect.assertions(1);
      const data = await user.getUserById(4);
      return expect(data).toEqual('Cosen');
    });
    // 使用async/await来测试reject
    it('async/await来测试reject', async () => {
      expect.assertions(1);
      try {
        await user.getUserById(2);
      } catch (e) {
        expect(e).toEqual({
          error: 'id为2的用户不存在',
        });
      }
    });
    

    Mock

    介绍jest中的mock之前,我们先来思考一个问题:为什么要使用mock函数?

    在项目中,一个模块的方法内常常会去调用另外一个模块的方法。在单元测试中,我们可能并不需要关心内部调用的方法的执行过程和结果,只想知道它是否被正确调用即可,甚至会指定该函数的返回值。这个时候,mock的意义就很大了。

    jest中与mock相关的api主要有三个,分别是jest.fn()jest.mock()jest.spyOn()。使用它们创建mock函数能够帮助我们更好的测试项目中一些逻辑较复杂的代码。我们在测试中也主要是用到了mock函数提供的以下三种特性:

    • 捕获函数调用情况
    • 设置函数返回值
    • 改变函数的内部实现

    下面,我将分别介绍这三种方法以及他们在实际测试中的应用。

    jest.fn()

    jest.fn()是创建mock函数最简单的方式,如果没有定义函数内部的实现,jest.fn()会返回undefined作为返回值。

    // functions.test.js
    
    test('测试jest.fn()调用', () => {
      let mockFn = jest.fn();
      let res = mockFn('厦门','青岛','三亚');
    
      // 断言mockFn的执行后返回undefined
      expect(res).toBeUndefined();
      // 断言mockFn被调用
      expect(mockFn).toBeCalled();
      // 断言mockFn被调用了一次
      expect(mockFn).toBeCalledTimes(1);
      // 断言mockFn传入的参数为1, 2, 3
      expect(mockFn).toHaveBeenCalledWith('厦门','青岛','三亚');
    })
    

    jest.fn()所创建的mock函数还可以设置返回值,定义内部实现返回Promise对象

    // functions.test.js
    
    test('测试jest.fn()返回固定值', () => {
      let mockFn = jest.fn().mockReturnValue('default');
      // 断言mockFn执行后返回值为default
      expect(mockFn()).toBe('default');
    })
    
    test('测试jest.fn()内部实现', () => {
      let mockFn = jest.fn((num1, num2) => {
        return num1 + num2;
      })
      // 断言mockFn执行后返回20
      expect(mockFn(10, 10)).toBe(20);
    })
    
    test('测试jest.fn()返回Promise', async () => {
      let mockFn = jest.fn().mockResolvedValue('default');
      let res = await mockFn();
      // 断言mockFn通过await关键字执行后返回值为default
      expect(res).toBe('default');
      // 断言mockFn调用后返回的是Promise对象
      expect(Object.prototype.toString.call(mockFn())).toBe("[object Promise]");
    })
    

    jest.mock()

    一般在真实的项目里,测试异步函数的时候,不会真正的发送 ajax 请求去请求这个接口,为什么?

    比如有 1w 个接口要测试,每个接口要 3s 才能返回,测试全部接口需要 30000s,那么这个自动化测试的时间就太慢了

    我们作为前端只需要去确认这个异步请求发送成功就好了,至于后端接口返回什么内容我们就不测了,这是后端自动化测试要做的事情。

    这里以一个axios请求demo为例来说明:

    // user.js
    import axios from 'axios'
    
    export const getUserList = () => {
      return axios.get('/users').then(res => res.data)
    }
    
    

    对应测试文件user.test.js:

    import { getUserList } from '@/services/user.js'
    import axios from 'axios'
    // ??
    jest.mock('axios')
    // ??
    test.only('测试 getUserList', async () => {
      axios.get.mockResolvedValue({ data: ['Cosen','森林','柯森'] })
      await getUserList().then(data => {
        expect(data).toBe(['Cosen','森林','柯森'])
      })
    })
    

    我们在测试用例的最上面加入了jest.mock('axios'),我们让jest去对axios做模拟,这样就不会去请求真正的数据了。然后调用axios.get的时候,不会真实的请求这个接口,而是会以我们写的{ data: ['Cosen','森林','柯森'] }去模拟请求成功后的结果。

    jest.spyOn()

    jest.spyOn()方法同样创建一个mock函数,但是该mock函数不仅能够捕获函数的调用情况,还可以正常的执行被spy的函数。实际上,jest.spyOn()jest.fn()的语法糖,它创建了一个和被spy的函数具有相同内部代码的mock函数

    Snapshot 快照测试

    所谓snapshot,即快照也。通常涉及 UI 的自动化测试,思路是把某一时刻的标准状态拍个快照。

    describe("xxx页面", () => {
      // beforeEach(() => {
      //   jest.resetAllMocks()
      // })
      // 使用 snapshot 进行 UI 测试
      it("页面应能正常渲染", () => {
        const wrapper = wrappedShallow()
        expect(wrapper).toMatchSnapshot()
      })
    })
    

    当使用toMatchSnapshot的时候,Jest 将会渲染组件并创建其快照文件。这个快照文件包含渲染后组件的整个结构,并且应该与测试文件本身一起提交到代码库。当我们再次运行快照测试时,Jest 会将新的快照与旧的快照进行比较,如果两者不一致,测试就会失败,从而帮助我们确保用户界面不会发生意外改变。

    ? 总结

    到这里,关于前端单元测试的一些基础背景和Jest的基础api就介绍完了,在下一篇文章中,我会结合项目中的一个React组件来讲解如何做组件单元测试

    ? 参考链接

    • segmentfault.com/a/119000001…

    起源地下载网 » 那些年错过的React组件单元测试(上)

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元