51Testing软件测试论坛

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

QQ登录

只需一步,快速开始

微信登录,快人一步

手机号码,快捷登录

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

[转贴] 一篇文章搞定前端单元测试框架jest

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

    连续签到: 1 天

    [LV.10]测试总司令

    跳转到指定楼层
    1#
    发表于 2022-5-9 10:47:37 | 只看该作者 回帖奖励 |倒序浏览 |阅读模式
     前言
      虽然有很多前端团队压根现在甚至未来都不太可能使用单元测试,包括我自己的团队,原因无非是耽误时间,开发任务本身就比较重等等理由。
      但是我觉得一味的图快,永远是饮鸩止渴,陷入恶性循环,项目快 --> 代码烂 --> 修改和加功能花费更多的时间和精力 --> 来不及做优化必须更快 --> 项目快 --> 代码烂 --> ... 无限循环。
      这就是做单元测试我认为最重要的原因就是,重构代码时,确认功能没有问题,不怕人员流动,功能迁移,最主要的是跟产品撕b,测试用例就是最好的证据。
      业务项目用不到的话,如果你写库,不写单测,可能用的同学都会有所顾忌,所以会写单测是对高级以上前端必备的技能。
      单元测试框架基本原理
      例如如下的一个测试用例,感受一下基本的样子长啥,我们后面会把其中用到的方法自己实现一个简单版本。
    1.  // 意思是字符串hello是否包含ll
    2.   test('测试字符串中是否包含 ll'), () => {
    3.       expect(findStr('hello')).toMatch('ll')
    4.   })
    5.   function findStr(str){
    6.       return `${str} world`
    7.   }
    复制代码
     我们可以简单的实现一下上面测试用例用到的方法,test、expect、toMatch,这样就算掌握了基本的测试框架原理。
      test
     function test(desc, fn){
          try{
              fn();
              console.log(`?  通过测试用例`)
          }catch{
              console.log(`? 没有通过测试用例`)
          }
      }

     expect、toMatch

     function expect(ret){
          return {
              toMatch(expRet){
                  if(typeof ret === 'string'){ throw Error('') }
                  if(!ret.includes(expRet)){ throw Error('') }
              }
          }
      }

     jest基本配置
      必备工具:
    1.  $ npm i -D jest babel-jest ts-jest @types/jest
    复制代码
     参考配置jest.config.js,测试文件均放在tests目录中:
      下面的testRegex表示匹配的tests文件夹下的以test或者spec结尾的jsx或者tsx文件。
    1.  module.exports = {
    2.     transform: {
    3.       '^.+\\.tsx?最后在package.json的scripts中加入:[/font][/color]
    4. [code] {
    5.       test: "jest"
    6.       // 如果要测试覆盖率,后面加上--coverage
    7.       // 如果要监听所有测试文件 --watchAll
    8.   }
    复制代码
    匹配器
      匹配器(Matchers)是Jest中非常重要的一个概念,它可以提供很多种方式来让你去验证你所测试的返回值。举个例子就明白什么是匹配器了。
      这里的匹配器扫一眼即可,大概知道有那么回事,用的时候查你想要的匹配器就行,不用刻意去记忆。
      相等匹配,这是我们最常用的匹配规则:
    1. test('two plus two is four', () => {
    2.     expect(2 + 2).toBe(4);
    3.   });
    复制代码
    在这段代码中 expact(2 + 2) 将返回我们期望的结果,通常情况下我们只需要调用expect就可以,括号中的可以是一个具有返回值的函数,也可以是表达式。后面的toBe 就是一匹配器。
      下面列举一些常用的匹配器:
      普通匹配器
      ·toBe:object.is 相当于 ===
    1.  test('测试加法 3 + 7', () => {
    2.     // toBe 匹配器 matchers object.is 相当于 ===
    3.     expect(10).toBe(10)
    4.   })
    复制代码
    · toEqual:内容相等,匹配内容,不匹配引用

    1. test('toEqual 匹配器', () => {
    2.     // toEqual 匹配器 只会匹配内容,不会匹配引用
    3.     const a = { one: 1 }
    4.     expect(a).toEqual({ one: 1 })
    5.   })
    复制代码
    与真假有关的匹配器
      · 真假
      · toBeNull:只匹配 Null
    1. test('toBeNull 匹配器', () => {
    2.     // toBeNull
    3.     const a = null
    4.     expect(a).toBeNull()
    5.   })
    复制代码
    toBeUndefined:只匹配 undefined;

    1. test('toBeUndefined 匹配器', () => {
    2.     const a = undefined
    3.     expect(a).toBeUndefined()
    4.   })
    复制代码
     toBeDefined: 与 toBeUndefined 相反,这里匹配 null 是通过的。

    1.  test('toBeDefined 匹配器', () => {
    2.     const a = null
    3.     expect(a).toBeDefined()
    4.   })
    复制代码
    toBeTruthy:匹配任何 if 语句为 true;

    1.  test('toBeTruthy 匹配器', () => {
    2.     const a = 1
    3.     expect(a).toBeTruthy()
    4.   })
    复制代码
    toBeFalsy:匹配任何 if 语句为 false;

    1. test('toBeFalsy 匹配器', () => {
    2.     const a = 0
    3.     expect(a).toBeFalsy()
    4.   })
    复制代码
     not:取反

    1.  test('not 匹配器', () => {
    2.     const a = 1
    3.     // 以下两个匹配器是一样的
    4.     expect(a).not.toBeFalsy()
    5.     expect(a).toBeTruthy()
    6.   })
    复制代码
    数字
      toBeGreaterThan:大于。
    1.  test('toBeGreaterThan', () => {
    2.     const count = 10
    3.     expect(count).toBeGreaterThan(9)
    4.   })
    复制代码
     toBeLessThan:小于。

    1.  test('toBeLessThan', () => {
    2.     const count = 10
    3.     expect(count).toBeLessThan(12)
    4.   })
    复制代码
    toBeGreaterThanOrEqual:大于等于。

    1.  test('toBeGreaterThanOrEqual', () => {
    2.     const count = 10
    3.     expect(count).toBeGreaterThanOrEqual(10) // 大于等于 10
    4.   })
    复制代码
     toBeLessThanOrEqual:小于等于。

    1.  test('toBeLessThanOrEqual', () => {
    2.     const count = 10
    3.     expect(count).toBeLessThanOrEqual(10) // 小于等于 10
    4.   })
    复制代码
     toBeCloseTo:计算浮点数。

    1. test('toBeCloseTo', () => {
    2.     const firstNumber = 0.1
    3.     const secondNumber = 0.2
    4.     expect(firstNumber + secondNumber).toBeCloseTo(0.3) // 计算浮点数
    5.   })
    复制代码
     字符串
      toMatch: 匹配某个特定项字符串,支持正则。
    1.  test('toMatch', () => {
    2.     const str = 'http://www.zsh.com'
    3.     expect(str).toMatch('zsh')
    4.     expect(str).toMatch(/zsh/)
    5.   })
    复制代码
    数组
      toContain:匹配是否包含某个特定项。
    1. test('toContain', () => {
    2.     const arr = ['z', 's', 'h']
    3.     const data = new Set(arr)
    4.     expect(data).toContain('z')
    5.   })
    复制代码
     异常
      toThrow
    1. const throwNewErrorFunc = () => {
    2.     throw new Error('this is a new error')
    3.   }
    4.   test('toThrow', () => {
    5.     // 抛出的异常也要一样才可以通过,也可以写正则表达式
    6.     expect(throwNewErrorFunc).toThrow('this is a new error')
    7.   })
    复制代码


      测试异步代码
      假设请求函数如下:
    1.  const fethUserInfo = fetch('http://xxxx')
    复制代码
    测试异步代码有好几种方式,我就推荐一种我认为比较常用的方式。

    1. // fetchData.test.js
    2.   // 测试promise成功需要加.resolves方法
    3.   test('the data is peanut butter', async () => {
    4.       await expect(fethUserInfo()).resolves.toBe('peanut butter');
    5.   });
    6.   // 测试promise成功需要加.rejects方法
    7.   test('the fetch fails with an error', async () => {
    8.       await expect(fethUserInfo()).rejects.toMatch('error');
    9.   });
    复制代码
    作用域
      jest提供一个describle函数来分离各个test测试用例,就是把相关的代码放到一类分组中,这么简单,看个例子就懂了。
    1.  // 分组一
    2.   describe('Test xxFunction', () => {
    3.     test('Test default return zero', () => {
    4.         expect(xxFunction()).toBe(0)
    5.     })
    6.     // ...其它test
    7.   })
    8.   // 分组二
    9.   describe('Test xxFunction2', () => {
    10.     test('Pass 3 can return 9', () => {
    11.         expect(xxFunction2(3)).toBe(9)
    12.     })
    13.     // ...其它test
    14.   })
    复制代码
    钩子函数
      jest中有4个钩子函数:
      ·beforeAll:所有测试之前执行
      · afterAll:所有测试执行完之后
      · beforeEach:每个测试实例之前执行
      · afterEach:每个测试实例完成之后执行
      我们举例来说明为什么需要他们。
      在 index.js 中写入一些待测试方法。
    1.  export default class compute {
    2.     constructor() {
    3.       this.number = 0
    4.     }
    5.     addOne() {
    6.       this.number += 1
    7.     }
    8.     addTwo() {
    9.       this.number += 2
    10.     }
    11.     minusOne() {
    12.       this.number -= 1
    13.     }
    14.     minusTwo() {
    15.       this.number -= 2
    16.     }
    17.   }
    复制代码
    假如我们要在 index.test.js 中写测试实例:

    1. import compute from './index'
    2.   const Compute = new compute()
    3.   test('测试 addOne', () => {
    4.     Compute.addOne()
    5.     expect(Compute.number).toBe(1)
    6.   })
    7.   test('测试 minusOne', () => {
    8.     Compute.minusOne()
    9.     expect(Compute.number).toBe(0)
    10.   })
    复制代码


      这里两个测试实例相互之间影响了,共用了一个computet实例,我们可以将const Compute = new compute()放在beforEach里面就可以解决了,每次测试实例之前先重新new compute。
      同理,你想在每个test测试完毕后单独运行什么可以放入到afterEach中我们接着看一下什么情况下使用beforeAll,假如我们测试数据库数据是否保存正确。
      我们在测试最开始,也就是?beforeAll生命周期里,新增1条数据到数据库里。测试完后,也就是?afterAll周期里,删除之前添加的数据。最后利用全局作用域afterAll确认数据库是否还原成初始状态。
      这里说到:
    1. // 模拟数据库
    2.   const userDB = [
    3.     { id: 1, name: '小明' },
    4.     { id: 2, name: '小花' },
    5.   ]
    6.   // 新增数据
    7.   const insertTestData = data => {
    8.     // userDB,push数据
    9.   }
    10.   // 删除数据
    11.   const deleteTestData = id => {
    12.     // userDB,delete数据
    13.   }
    14.   // 全部测试完
    15.   afterAll(() => {
    16.     console.log(userDB)
    17.   })
    18.   describe('Test about user data', () => {
    19.     beforeAll(() => {
    20.         insertTestData({ id: 99, name: 'CS' })
    21.     })
    22.     afterAll(() => {
    23.         deleteTestData(99)
    24.     })
    25.   })
    复制代码
     jest里的Mock
      为什么要使用Mock函数?
      在项目中,经常会碰见A模块掉B模块的方法。并且,在单元测试中,我们可能并不需要关心内部调用的方法的执行过程和结果,只想知道它是否被正确调用即可,甚至会指定该函数的返回值。此时,就需要mock函数了。
      Mock函数提供的以下三种特性,在我们写测试代码时十分有用:
      ·捕获函数调用情况
      · 设置函数返回值
      · 改变函数的内部实现
      jest.fn()
      jest.fn()是创建Mock函数最常用的方式。
    1.  test('测试jest.fn()', () => {
    2.     let mockFn = jest.fn();
    3.     let result = mockFn(1);
    4.     // 断言mockFn被调用
    5.     expect(mockFn).toBeCalled();
    6.     // 断言mockFn被调用了一次
    7.     expect(mockFn).toBeCalledTimes(1);
    8.     // 断言mockFn传入的参数为1
    9.     expect(mockFn).toHaveBeenCalledWith(1);
    10.   })
    复制代码
     jest.fn()所创建的Mock函数还可以设置返回值,定义内部实现或返回Promise对象。

    1.  test('测试jest.fn()返回固定值', () => {
    2.     let mockFn = jest.fn().mockReturnValue('default');
    3.     // 断言mockFn执行后返回值为default
    4.     expect(mockFn()).toBe('default');
    5.   })
    6.   test('测试jest.fn()内部实现', () => {
    7.     let mockFn = jest.fn((num1, num2) => {
    8.       return num1 * num2;
    9.     })
    10.     // 断言mockFn执行后返回100
    11.     expect(mockFn(10, 10)).toBe(100);
    12.   })
    13.   test('测试jest.fn()返回Promise', async () => {
    14.     let mockFn = jest.fn().mockResolvedValue('default');
    15.     let result = await mockFn();
    16.     // 断言mockFn通过await关键字执行后返回值为default
    17.     expect(result).toBe('default');
    18.     // 断言mockFn调用后返回的是Promise对象
    19.     expect(Object.prototype.toString.call(mockFn())).toBe("[object Promise]");
    20.   })
    复制代码
      2. jest.mock()
      fetch.js文件夹中封装的请求方法可能我们在其他模块被调用的时候,并不需要进行实际的请求(请求方法已经通过单测或需要该方法返回非真实数据)。此时,使用jest.mock()去mock整个模块是十分有必要的。
      下面我们在src/fetch.js的同级目录下创建一个src/events.js。
    1.   import fetch from './fetch';
    2.   export default {
    3.     async getPostList() {
    4.       return fetch.fetchPostsList(data => {
    5.         console.log('fetchPostsList be called!');
    6.         // do something
    7.       });
    8.     }
    9.   }

    10.   import events from '../src/events';
    11.   import fetch from '../src/fetch';
    12.   jest.mock('../src/fetch.js');
    13.   test('mock 整个 fetch.js模块', async () => {
    14.     expect.assertions(2);
    15.     await events.getPostList();
    16.     expect(fetch.fetchPostsList).toHaveBeenCalled();
    17.     expect(fetch.fetchPostsList).toHaveBeenCalledTimes(1);
    18.   });
    复制代码
     在测试代码中我们使用了jest.mock('../src/fetch.js')去mock整个fetch.js模块。如果注释掉这行代码,执行测试脚本时会出现以下报错信息。
      从这个报错中,我们可以总结出一个重要的结论:
      在jest中如果想捕获函数的调用情况,则该函数必须被mock或者spy!
      3. jest.spyOn()
      jest.spyOn()方法同样创建一个mock函数,但是该mock函数不仅能够捕获函数的调用情况,还可以正常的执行被spy的函数。实际上,jest.spyOn()是jest.fn()的语法糖,它创建了一个和被spy的函数具有相同内部代码的mock函数。
      上图是之前jest.mock()的示例代码中的正确执行结果的截图,从shell脚本中可以看到console.log('fetchPostsList be called!');这行代码并没有在shell中被打印,这是因为通过jest.mock()后,模块内的方法是不会被jest所实际执行的。这时我们就需要使用jest.spyOn()。
    1. // functions.test.js
    2.   import events from '../src/events';
    3.   import fetch from '../src/fetch';
    4.   test('使用jest.spyOn()监控fetch.fetchPostsList被正常调用', async() => {
    5.     expect.assertions(2);
    6.     const spyFn = jest.spyOn(fetch, 'fetchPostsList');
    7.     await events.getPostList();
    8.     expect(spyFn).toHaveBeenCalled();
    9.     expect(spyFn).toHaveBeenCalledTimes(1);
    10.   })
    复制代码
    执行npm run test后,可以看到shell中的打印信息,说明通过jest.spyOn(),fetchPostsList被正常的执行了。
      快照
      快照就是对你对比的数据会存一份副本,啥意思呢,我们举个例子:
      这是index.js
    1. export const data2 = () => {
    2.     return {
    3.       name: 'zhangsan',
    4.       age: 26,
    5.       time: new Date()
    6.     }
    7.   }
    复制代码
     在 index.test.js 中写入一些测试实例:
    1.  import { data2 } from "./index"
    2.   it('测试快照 data2', () => {
    3.     expect(data2()).toMatchSnapshot({
    4.       name: 'zhangsan',
    5.       age: 26,
    6.       time: expect.any(Date) //用于声明是个时间类型,否则时间会一直改变,快照不通过
    7.     })
    8.   })
    复制代码
    ·toMatchSnapshot会将参数将快照进行匹配
      · expect.any(Date) 用于匹配一个时间类型
      执行npm run test会生成一个__snapshots__文件夹,里面是生成的快照,当你修改一下测试代码时,会提示你,快照不匹配。
      如果你确定你需要修改,按 u 键,即可更新快照。这用于UI组件的测试非常有用。
      React的BDD单测
      接下来我们看下react代码如何进行测试,用一个很小的例子来说明。
      案例中引入了enzyme。Enzyme?来自 airbnb 公司,是一个用于 React 的 JavaScript 测试工具,方便你判断、操纵和历遍 React Components 输出。
      我们达成的目的是检测:
      · 用户进入首页,看到两个按钮,分别是counter1和counter2
      · 点击counter1,就能看到两个按钮的文字部分分别是"counter1"和"counter2"
      react代码如下:
    1.  import React from 'react';
    2.   function Counter(){
    3.       return (
    4.           <ul>
    5.               <li>
    6.                   <button id='counter1' className='button1'>counter1</button>
    7.               </li>
    8.               <li>
    9.                   <button id='counter2' className='button2'>counter2</button>
    10.               </li>
    11.           </ul>
    12.       )
    13.   }
    复制代码
     单测的文件:
    1.  import Counter from xx;
    2.   import { mount } from 'enzyme';
    3.   describle('测试APP',() => {
    4.       test('用户进入首页,看到两个按钮,分别是counter1和counter2,并且按钮文字也是counter1和counter2',()=>{
    5.           const wrapper = mount(<Counter />);
    6.           const button = wrapper.find('button');
    7.           except(button).toHaveLength(2);
    8.           except(button.at(0).text()).toBe('counter1');
    9.           except(button.at(1).text()).toBe('counter2');
    10.       })
    11.   })
    复制代码


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

    使用道具 举报

    本版积分规则

    关闭

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

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

    GMT+8, 2024-11-22 20:55 , Processed in 0.067300 second(s), 23 queries .

    Powered by Discuz! X3.2

    © 2001-2024 Comsenz Inc.

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