最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • Jest单元测试入门和实例

    正文概述 掘金(秃头猿)   2021-03-27   977

    1. 为什么选择Jest?

    Jest 是 Facebook 出品的一个测试框架,相对其他测试框架,其一大特点就是就是内置了常用的测试工具,比如自带断言Mock 功能测试覆盖率工具,实现了开箱即用

    2. 使用

    1. 如何安装

    Jest 可以通过 npm 或 yarn 进行安装。以 npm 为例,既可用npm install -g jest进行全局安装;也可以只局部安装、并在 package.json 中指定 test 脚本:

    {
      "scripts": {
        "test": "jest"
      }
    }
    

    Jest 的测试脚本名形如*.test.js,不论 Jest 是全局运行还是通过npm run test运行,它都会**执行当前目录下所有的*.test.js 或 .spec.js 文件*、完成测试。

    2. 简单例子

    先让我们在一个react工程目录中src下新建一个__tests__文件夹,在里面写我们的各种测试案例。

    首先,创建一个sum.js文件:

    function sum(a, b){
        return a + b;
    }
    
    function foo(){
      return 'hello world'
    }
    
    export { sum,foo };
    

    然后在__tests__中,新建一个sum.test.js的文件:

    import { sum,foo } from './sum.js';
    
    describe('test testObject', () => {
      test('测试求和函数', () => {
          expect(sum(2, 2)).toBe(4);
      });
    
      test('测试foo函数', () => { // 也可以用it
          expect(foo()).toBe('hell world');
      });
    })
    

    test() 函数表示一个测试用例

    describe() 函数表示一组用例

    expect() 函数返回一个期望值对象,该对象提供了大量的工具方法来做结果判定

    toBe() 函数表示断言方法

    我们把 sum foo 置于 describe() 函数的处理回调函数中,就实现了对一组用例的测试。

    最后运行 yarn testnpm run test,将打印出下面的消息,表示测试通过了。

     PASS  __test__/sum.test.js
     PASS  __test__/foo.test.js
      √ 测试求和函数 (7ms)
      √ 测试foo函数 (1ms)
    
    Test Suites: 2 passed, 2 total
    Tests:       2 passed, 2 total
    Snapshots:   0 total
    Time:        4.8s
    

    3. 匹配器

    1. 判断相等:

    test('2加2等于4', () => {
        expect(2+2).toBe(4);
    });
    // 测试对象相等
    test('测试对象的值', () => {
        const data = {a: 1};
        expect(data).toEqual({a: 1});
    });
    // 测试不等,相反的值
    test('2加2不等于1', () => {
        expect(2 + 2).not.toBe(1);
    });
    

    2. 判断真假、空值:

    • toBeNull 只匹配 null;
    • toBeUndefined 只匹配 undefined;
    • toBeDefined 与 toBeUndefined 相反;
    • toBeTruthy 匹配任何 if 语句为真;
    • toBeFalsy 匹配任何 if 语句为假;
    test('null', () => {
      const n = null;
      expect(n).toBeNull();
      expect(n).toBeDefined();
      expect(n).not.toBeUndefined();
      expect(n).not.toBeTruthy();
      expect(n).toBeFalsy();
    });
    

    3. 判断数字:

    test('2加2', () => {
      const value = 2 + 2;
      expect(value).toBeGreaterThan(3); // 大于3
      expect(value).toBeGreaterThanOrEqual(4); // 大于或等于4
      expect(value).toBeLessThan(5); // 小于5
      expect(value).toBeLessThanOrEqual(4.5); // 小于或等于4.5
    });
    

    4. 判断符点数:

    test('两个浮点数字相加', () => {
      const value = 0.1 + 0.2;
      //expect(value).toBe(0.3);  这句会报错,因为浮点数有舍入误差
      expect(value).toBeCloseTo(0.3); // 这句可以运行
    });
    

    5. 判断字符串:toMatch()

    test('there is no I in team', () => {
      expect('team').not.toMatch(/I/);
    });
    
    test('but there is a "stop" in Christoph', () => {
      expect('Christoph').toMatch(/stop/);
    });
    

    6. 判断数组或可迭代的对象:toContain()

    const shoppingList = [
      'diapers',
      'kleenex',
      'beer',
    ];
    
    test('the shopping list has beer on it', () => {
      expect(shoppingList).toContain('beer');
    });
    

    7. 判断异常:toThrow()

    function compileAndroidCode() {
      throw new Error('这是一个错误消息');
    }
    
    test('compiling android goes as expected', () => {
      expect(compileAndroidCode).toThrow();
    
      // 可以匹配异常消息的内容,也可以用正则来匹配内容
      expect(compileAndroidCode).toThrow('这是一个错误消息');
      expect(compileAndroidCode).toThrow(/消息/);
    });
    

    Tips: 代码提示?

    expect 的工具方法太多记不住怎么办,安装 jest@types/jest 包含的声明文件,声明文件是定义了一些类型,写代码的时候就会有提示。

    npm i jest @types/jest
    

    4. 异步测试

    异步代码的测试,关键点在于告知测试框架测试何时完成,让其在恰当的时机进行断言。

    1. Callbacks回调函数:done

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

    it('Test async code with done', (done) => {
      setTimeout(() => {
        // expect something
        done();
      }, 500)
    });
    

    2. 返回Promise:expect.assertions(number)

    • assertions 断言

    如果使用的是 promise,需要在测试中 返回 一个 promise,Jest 会自动等待 promise 被解析处理,如果 promise 被拒绝,那么测试失败。

    例1:

    test("Test async code with promise", () => {
      // 一个rejected状态的 Promise 不会让测试失败
      expect.assertions(1);
      return doAsync().then((data) => {
        expect(data).toBe('example');
      });
    });
    
    test("Test promise with an error", () => {
      // 一个fulfilled状态的 Promise 不会让测试失败
      expect.assertions(1);
      return doAsync().catch(e => {
        expect(e).toMatch('error')
      });
    });
    

    函数 doAsync,该函数接收两个回调 callback1callback2,它将以未知顺序异步调用这两个回调。

    例2:

    test("doAsync calls both callbacks", () => {
      expect.assertions(2);
      function callback1(data) {
        expect(data).toBeTruthy();
      }
      function callback2(data) {
        expect(data).toBeTruthy();
      }
      doAsync(callback1, callback2);
    });
    

    使用 expect.assertions(2) 确保两个回调都实际被调用。

    • .resolves / .rejects Jest语法糖

    例1中的代码用匹配符 resolves/rejects (这里有s,非Promise)可以改写为:

    // 假设 doAsync() 返回一个promise,resolve的结果为字符串'example'
    it('Test async code with promise', () => {
      expect.assertions(1);
      return expect(doAsync()).resolves.toBe('example');
      });
    });
    
    it('Test promise with an error', () => {
      expect.assertions(1);
      return expect(doAsync()).rejects.toMatch('error'));
    });
    
    • async/await Promise语法糖

    实际开发中,我们更常用的是用 async/await 来开发业务代码,上面例子的也可以 async/await 实现

    // 假设 doAsync() 返回一个promise,resolve的结果为字符串'example'
    it('Test async code with promise', async () => {
      expect.assertions(1);
      const data = await doAsync();
      expect(data).toBe('example');
      });
    });
    

    async/await 也可以和 resolves/rejects 一起使用:

    // 假设 doAsync() 返回一个promise,resolve的结果为字符串'example'
    it('Test async code with promise', async () => {
      expect.assertions(1);
      await expect(doAsync()).resolves.toBe('example');
      });
    });
    

    3. done 和 assertions 区别

    done:异步回调确保测试

    assertions:Promise确保测试

    一般测试的时候,异步都是模拟 mock 出来的,要自己控制结束,而不是真正的异步。所以 expect.assertions 某些情况下无法替代 done

    5. Mock函数

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

    1. jest.fn()

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

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

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

    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执行后返回100
      expect(mockFn(10, 10)).toBe(100);
    })
    
    test('测试jest.fn()返回Promise', async () => {
      let mockFn = jest.fn().mockResolvedValue('default');
      let result = await mockFn();
      // 断言mockFn通过await关键字执行后返回值为default
      expect(result).toBe('default');
      // 断言mockFn调用后返回的是Promise对象
      expect(Object.prototype.toString.call(mockFn())).toBe("[object Promise]");
    })
    

    ? 实际使用:

    ts 开发中,有些类型定义为必填项

    onChange = (
      pagination: PaginationConfig,
      filters: Partial<Record<keyof T, string[]>>,
      sorter: SorterResult<T>,
      extra: TableCurrentDataSource<T>,
    ) => {}
    

    假如 filters 是函数类型,为必填项,实际测试不想传入这个值,传参可以填 jest.fn() mock掉这个函数。

    所有的 mock 函数都有一个特殊的 .mock 属性,它保存了关于此函数如何被调用、调用时的返回值的信息。

    function forEach(items, callback) {
      for (let index = 0; index < items.length; index++) {
        callback(items[index]);
      };
    };
    
    test('测试forEach函数', () => {
        const mockCallback = jest.fn(x => 42 + x);
        forEach([0, 1], mockCallback);
        // 此 mock 函数被调用了两次
        expect(mockCallback.mock.calls.length).toBe(2);
        // 第一次调用函数时的第一个参数是 0
        expect(mockCallback.mock.calls[0][0]).toBe(0);    
        // 第一次函数调用的返回值是 42
        expect(mockCallback.mock.results[0].value).toBe(42);
    })
    

    2. jest.mock()

    通常情况下,我们需要调用api,发送ajax请求,从后台获取数据。但是我们在做前端测试的时候,并不需要去调用真实的接口,所以此时我们需要模拟 axios/fetch 模块,让它不必调用api也能测试我们的接口调用是否正确。

    下面我们创建一个 events.js

    // events.js
    import fetch from './fetch';
     
    export default {
      async getPostList() {
        return fetch.fetchPostsList(data => {
          console.log('fetchPostsList be called!');
          // do something
        });
      }
    }
    

    测试代码如下:

    import events from '../src/events';
    import fetch from '../src/fetch';
     
    jest.mock('../src/fetch.js');
     
    test('mock 整个 fetch.js模块', async () => {
      expect.assertions(2);
      await events.getPostList();
      expect(fetch.fetchPostsList).toHaveBeenCalled();
      expect(fetch.fetchPostsList).toHaveBeenCalledTimes(1);
    });
    

    在测试代码中我们使用了 jest.mock('axios') 去mock整个 fetch.js 模块。如果注释掉这行代码,执行测试脚本时会出现以下报错信息:

    Jest单元测试入门和实例

    从这个报错中,我们可以总结出一个重要的结论:

    3. jest.spyOn()

    jest.spyOn() 方法创建一个mock函数,并且可以正常执行被spy的函数。 jest.spyOn()jest.fn() 的语法糖,它创建了一个和被spy的函数具有相同内部代码的mock函数。

    import events from '../src/events';
    import fetch from '../src/fetch';
     
    test('使用jest.spyOn()监控fetch.fetchPostsList被正常调用', async() => {
      expect.assertions(2);
      const spyFn = jest.spyOn(fetch, 'fetchPostsList');
      await events.getPostList();
      expect(spyFn).toHaveBeenCalled();
      expect(spyFn).toHaveBeenCalledTimes(1);
    })
    

    运行之后,可以看到shell中的打印信息,说明通过jest.spyOn()fetchPostsList被正常的执行了。

    Jest单元测试入门和实例

    ? 在实际项目的单元测试中:

    jest.fn() 常被用来进行某些有回调函数的测试;

    jest.mock() 可以mock整个模块中的方法,当某个模块已经被单元测试100%覆盖时,使用jest.mock()去mock该模块,节约测试时间和测试的冗余度是十分必要;

    jest.spyOn() 当需要测试某些必须被完整执行的方法时,常常需要使用;

    这些都需要开发者根据实际的业务代码灵活选择。

    6. Jest钩子函数

    1. 执行顺序和作用域

    钩子函数是指在某一时刻,jest会自动调用的函数。如下:

    • beforeAll:在所有测试用例执行之前执行
    • afterAll:等所有测试用例都执行之后执行 ,可以在测试用例执行结束时,做一些处理
    • beforeEach:每个测试用例执行前执行,可让每个测试用例中使用的变量互不影响,因为分别为每个测试用例实例化了一个对象
    • afterEach:每个测试用例执行结束后,做一些处理

    注意:钩子函数的作用域为: 所在的 describe 分组;

    import Counter from './Counter'
    
    // 使用类中的方法,首先要实例化
    let counter = null
    beforeAll(() => {
    	console.log('外部的 beforeAll 执行')
    })
    
    beforeEach(() => { 
    	counter = new Counter()
    	console.log('外部的 beforeEach 执行')
    })
    
    describe('Counter 的测试代码', () => {
    	describe('Counter 中的加法测试代码', () => {
    		beforeAll(() => {
    			console.log('内部的 beforeAll 执行')
    		})
    		test('测试 Counter 中的 addOne 方法', () => {
    			counter.addOne()
    			expect(counter.number).toBe(1)
    		})
    		
    		test('测试 Counter 中的 addTwo 方法', () => {
    			counter.addTwo()
    			expect(counter.number).toBe(2)
    		})
    	})
    	
    	describe('Counter 中的减法测试代码', () => {
    		test('测试 Counter 中的 minusOne 方法', () => {
    			counter.minusOne()
    			expect(counter.number).toBe(-1)
    		})
    	
    		test('测试 Counter 中的 minusTwo 方法', () => {
    			counter.minusTwo()
    			expect(counter.number).toBe(-2)
    		})
    	})	
    })
    

    打印结果:

    Jest单元测试入门和实例

    ? 实际使用:

    import { shallow, mount } from '@vue/test-utils'
    import Test2 from './Component'
    
    describe('Test for Test2 Component', () => {
      let wrapper;
    
      beforeEach(() => {
        wrapper = shallow(Test2);
      });
    
      afterEach(() => {
        wrapper.destroy()
      });
    
      it('returns the string in normal order if reversed property is not true', () => {
        wrapper.setProps({needReverse: false});
        wrapper.vm.inputValue = 'ok';
        expect(wrapper.vm.outputValue).toBe('ok')
      });
    });
    

    shallow / mount,创建一个包含被挂载和渲染的Vue组件的 Wrapper,每次执行测试语句都要 destroy() 一下, 这样每个test语句都是重新创建的独立 wrapper ,避免引起副作用。

    比如在测试 FormModal 时, 如果modal没有 destroy,在修改情况下,下次代入的还是旧的 form 值。

    ps: 在测试modal时,需要先 show 一下,否则是测不到的?,和实际代码逻辑是一样的。

    2. describe中的基础代码执行顺序

    import Counter from './Counter'
    
    // 使用类中的方法,首先要实例化
    let counter = null
    beforeEach(() => {
    	counter = new Counter()
    	console.log('外部的 beforeEach 执行')
    })
    
    describe('Counter 的测试代码', () => {
    	console.log('describe 11111')
    	describe('Counter 中的加法测试代码', () => {
    		console.log('describe 22222')
    		beforeAll(() => {
    			console.log('内部的 beforeAll 执行')
    		})
    		test.only('测试 Counter 中的 addOne 方法', () => {
    			counter.addOne()
    			expect(counter.number).toBe(1)
    		})
    		
    		test('测试 Counter 中的 addTwo 方法', () => {
    			counter.addTwo()
    			expect(counter.number).toBe(2)
    		})
    	})
    	
    	describe('Counter 中的减法测试代码', () => {
    		console.log('describe 33333')
    		test('测试 Counter 中的 minusOne 方法', () => {
    			counter.minusOne()
    			expect(counter.number).toBe(-1)
    		})
    	
    		test('测试 Counter 中的 minusTwo 方法', () => {
    			counter.minusTwo()
    			expect(counter.number).toBe(-2)
    		})
    	})	
    })
    

    打印结果:

    Jest单元测试入门和实例

    由打印结果可以看出,describe 中的基础代码并没有按照我们的意愿去执行,而是最先执行了,所以当我们在写测试代码的基础代码时,一定要在钩子函数内完成。

    3. only和skip

    test.only(name, fn)

    describe.only(name, fn)

    only 只对单个测试用例进行测试

    test.skip(name, fn)

    describe.skip(name, fn)

    skip 跳过某个测试用例进行测试

    7. 其他知识点

    • 写单元测试的时候,同时运行 --watch 命令,每次保存都会自动运行,查看当前test语句是否通过。
    npm run test page/Component/BatchUpload.test.ts --watch
    
    • 每写完一个测试文件, 都可以运行 --coverage 命令, 查看分支或者语句的覆盖率, 也可以定位到某个文件夹, 查看模块的覆盖率。
    npm run test page/Component/BatchUpload.test.ts --coverage
    
    • 单元测试指标

    % stmts 是语句覆盖率(statement coverage):是不是每个语句都执行了?

    % Branch 分支覆盖率(branch coverage):是不是每个 if 代码块都执行了?

    % Funcs 函数覆盖率(function coverage):是不是每个函数都调用了?

    % Lines 行覆盖率(line coverage):是不是每一行都执行了?

    Jest单元测试入门和实例

    8. Vue单元测试实例

    ? 传送门

    Jest结合Vue-test-utils使用的初步实践

    github: Vue-unit-test-with-jest

    9. 小结

    疑问:明知道sum函数返回a、b之和,为什么还要写测试语句呢 ?

    这个和form表单的校验有点类似,有单元测试可以对代码逻辑进行回归验证。

    而且在写测试的过程中, 可以发现自己程序的bug,会反向思考自己的代码逻辑,组件划分是否合理、逻辑是不是可以单独拿出来,这是一个相互促进的过程。

    很多开源的组件库,项目都有相对应的单元测试,对于开发质量也是一个保证,别人在用的时候也会增强使用信心。


    起源地下载网 » Jest单元测试入门和实例

    常见问题FAQ

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

    发表评论

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

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

    联系作者

    请选择支付方式

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