利用 Jest 进行测试

发表于:2017-9-11 11:07

字体: | 上一篇 | 下一篇 | 我要投稿

 作者:ELCARIM    来源:51Testing软件测试网采编

  Jest
  常用的单元测试框架是 jasmine ,Mocha + Chai,不同于这些测试框架,jest 的集成度更高,提供的功能也更丰富,利用好 jest 所提供的功能,能大大提升测试用例的执行效率。
  Jest 特点:
  1.测试用例并行执行,更高效
  2.强大的 Mock 功能
  3.内置的代码覆盖率检查,不需要在引入额外的工具
  4.集成 JSDOM,可以直接进行 DOM 相关的测试
  5.更易用简单,几乎不需要额外配置
  6.可以直接对 ES Module Import 的代码测试
  7.有快照测试功能,可对 React 等框架进行 UI 测试
  断言库基本用法
  jest 使用的断言风格与 jasmine 是类似的。
  // be and equal
  expect(4 * 2).toBe(8);                      // ===
  expect({bar: 'bar'}).toEqual({bar: 'baz'}); // deep equal
  expect(1).not.toBe(2);
  // boolean
  expect(1 === 2).toBeFalsy();
  expect(false).not.toBeTruthy();
  // comapre
  expect(8).toBeGreaterThan(7);
  expect(7).toBeGreaterThanOrEqual(7);
  expect(6).toBeLessThan(7);
  expect(6).toBeLessThanOrEqual(6);
  // Promise
  expect(Promise.resolve('problem')).resolves.toBe('problem');
  expect(Promise.reject('assign')).rejects.toBe('assign');
  // contain
  expect(['apple', 'banana']).toContain('banana');
  expect([{name: 'Homer'}]).toContainEqual({name: 'Homer'});
  // match
  expect('NBA').toMatch(/^NB/);
  expect({name: 'Homer', age: 45}).toMatchObject({name: 'Homer'});
  相比于 Mocha 的书写风格(expect(8).to.be.equal(8)),jest 的断言风格更加简洁,同时保持着优秀的可读性。
  同时可以根据需要扩展自己的断言库:
  expect.extend({
    toBeEven(received) {
      const even = (received % 2 === 0);
      if (event) {
        return {
          message: () => (`expected ${received} not to be even Number`),
          pass: true,
        };
      } else {
        return {
          message: () => (`expected ${received} to be even number`),
          pass: false,
        };
      }
    },
  });
  expect(10).toBeEven();
  expect(9).not.toBeEven();
  Mock Function
  以上的断言基本是用在测试同步函数的返回值,如果所测试的函数存在异步逻辑。那么在测试时就应该利用 jest 的 mock function 来进行测试。通过 mock function 可以轻松地得到回调函数的调用次数、参数等调用信息,而不需要编写额外的代码去获取相关数据,让测试用例变得更可读。
  function getDouble(val, callback) {
    if(val < 0) {
      return;
    }
    setTimeout(() => {
      callback(val * val);
    }, 100);
  };
  const mockFn = jest.fn();
  getDouble(10, mockFn);
  expect(mockFn).not.toHaveBeenCalled()
  setTimeout(() => {
    expect(mockFn).toHaveBeenCalledTimes(1);
    expect(mockFn).toHaveBeenCalledWith(20);
  }, 110);
  除了可以创建一个 mock function 作为回调函数以外,jest 还可以利用 mock function 跟踪对象上已有的方法。
  const api = {
    getRandom(range) {
      return Math.floor(Math.Random() * range);
    },
  };
  const spy = hest.spyOn(api, 'getRandomID');
  api.getRandom(1000);
  expect(spy).toHaveBeenCalled();
  expect(spy).toHaveBeenCalledWith(1000);
  spy.mockReset();
  spy.mockRestore();      // 恢复原有的方法
  Mock 模块
  某些情况下,我们要测试模块可能会依赖于另一个模块。
  // getRandom.js
  export default function getRandom(range) {
    return Math.floor(Math.Random() * range);
  };
  // createModule.js
  import getRandom from './getRandom';
  export default function createModule(name) {
    return {
      name,
      id: `${name}-${getRandom(10000)}`,
    };
  };
  getRandom 返回的值是随机的,这样每次调用 createModule 时得到的 id 是无法确定的,意味着无法对 id 的值进行全等的断言测试。
  为此,jest 提供了 mock 功能,让我们可以对这些依赖模块进行 mock。
  在 getRandom.js 的同一级路径下,创建一个子目录__mocks__,并把新写的 mock 模块放到里面。
  //__mocks__/getRandom.js
  let num = 0;
  function getRandom() {
    return num;
  }
  getRandom.__set = function(_num) {
    num = _num;
  };
  export default getRandom;
  那么在测试脚本当中,调用 jest.mock 方法就可实现对该模块的 mock。
  import getRandom from './getRandom';
  import createModule from './createModule';
  jest.mock('./getRandom');
  describe('test mock', function() {
    it('test', function() {
      getRandom.__set(100);
      const module = createModule('module');
      expect(module.id).toBe('module-100');
    });
  });
  对模块进行 mock 的最大好处不仅仅在于方便地控制所依赖模块的返回值,还可以提高测试执行的效率。假如有一个模块需要调用 fs 模块进行文件读写,在进行测试时,就可以对这个模块进行 mock。那么在测试中,就不需要真正地去进行硬盘读写,提升了测试的效率。
  // __mocks__/fs.js
  const path = require('path');
  const fs = jest.genMockFromModule('fs');
  let mockFiles = Object.create(null);
  function __setMockFiles(newMockFiles) {
    mockFiles = Object.create(null);
    for (const file in newMockFiles) {
      const dir = path.dirname(file);
      if (!mockFiles[dir]) {
        mockFiles[dir] = [];
      }
      mockFiles[dir].push(path.basename(file));
    }
  }
  function readdirSync(directoryPath) {
    return mockFiles[directoryPath] || [];
  }
  fs.__setMockFiles = __setMockFiles;
  fs.readdirSync = readdirSync;
  export default fs;
  除了 fs 模块,在封装与异步请求相关的接口时,也可以通过这个功能对异步请求返回的数据进行 mock,而不必要建一个 mock server 去执行真正的异步请求。
  Mock Timers
  jest 除了为我们提供 mock 整个模块的功能外,还继承了对 timers mock,也就是 jest 可以劫持 setTimout、setInterval、clearTimeout、clearInterval 等方法,模拟 timer 的功能。
  例如本文中的第一个例子,被测试函数中需要用到定时器,在这里就可以利用 jest.useFaceTimers 来进行 mock。这样,就不需要真正地去等待 timers 执行完才去进程断言。
  function getDouble(val, callback) {
    if(val < 0) {
      return;
    }
    setTimeout(() => {
      callback(val * val);
    }, 100);
  };
  jest.useFakeTimers();
  const mockFn = jest.fn();
  getDouble(10, mockFn);
  expect(mockFn).not.toHaveBeenCalled()
  jest.runAllTimers();
  expect(mockFn).toHaveBeenCalledTimes(1);
  expect(mockFn).toHaveBeenCalledWith(20);
  jest.useRealTimers();
  再看一个简单的 debounce 的例子。
  function debounce(fn, wait) {
    let timestamp = null, timer = null, context;
    return function(...args) {
      timestamp = +new Date();
      context = this;
      function later() {
        const last = (+new Date()) - timestamp;
        if (last < wait && last > 0) {
          clearTimer(timer);
          timer = setTimeout(later, wait - last);
        } else {
          fn.call(context, ...args);
          clearTimer(timer);
        }
      }
      if (!timer) {
        timer = setTimeout(later, wait);
      }
    };
  }
  测试脚本如下
  describe('debounce', function() {
    it('should be called after 100 ms', function() {
      const mockFn = jest.fn();
      const run = debounce(mockFn, 100);
      jest.useFakeTimers();
      run();
      jest.runTimersToTime(50);   // 第 50 ms
      run();
      expect(mockFn).not.toHaveBeenCalled();
      jest.runTimersToTime(50);   // 第 100 ms
      expect(mockFn).not.toHaveBeenCalled();
      jest.runTimersToTime(50);   // 第 150 ms
      expect(mockFn).toHaveBeenCalledTime(1);
      jest.useRealTimers();
    });
  });
  通过 mock timer,我们期待的是当定时器运行到第 150 ms 时,mockFn 才会执行一次。但这个用例会执行失败。因为当定时器运行到第 100 ms 时,mockFn 就被执行了。这是由于 jest 的 mock timer 的实现机制导致的。
  jest 会将 setTimeout 替换为自带的 setTimeout 方法,该方法调用时会将对应的回调函数登记到对应的_timers列表当中。当中会通过 expiry 记录定时器的到期时间。当执行 jest.runTimersToTime(time) 时,就会进行判断 _now + time >= _timer.expiry ,如果达到过期时间,_timer.callback 就会被立即执行。
  // _timers 列表中的数据结构
  {
    callback: () => callback.apply(null, args),
    expiry: _now + delay,
    interval: null,
    type: 'timeout',
  }
  因此在 jest 的 mock timer 环境下, 定时器回调函数的执行实际上已经变成了同步的了,它会在调用 jest.runTimers 这类方法时进行判断并执行符合条件的回调方法。
  在定时器回调执行时,实现的时间并没有流逝,而 debounce 方法中需要通过 Date 类记录调用时间。所以无论是在第 50 ms 还是第 100 ms, run 执行时的 timestamp 跟第一次执行时是一样的,所以在第 100 ms时, last === 0,因此 mockFn 被执行。
  mock timer 能模拟定时器的行为,但并不是真正地加速时间运行,所以通过 Date 获取的时间不会跟着一起增加。当被测模块需中还需要调用 Date 时,就还需要对 Date 进行模拟。为此我们可以利用 mockdate 这个 npm 包。
  mockdate 通过修改全局的 Date 类,达到控制 Date 的时间的目的。
  因此测试用例需要作调整,在调用 jest.runTimersToTime 之前先修改 Date 的当前时间。
  import MockDate from 'mockdate';
  let now = +new Date();
  function fastforward(time) {
    now += time;
    MockDate.set(now);
    jest.runTimersTo(time);
  }
  describe('debounce', function() {
    it('should be called after 100 ms', function() {
      const mockFn = jest.fn();
      const run = debounce(mockFn, 100);
      jest.useFakeTimers();
      run();
      fastforward(50);   // 第 50 ms
      run();
      expect(mockFn).not.toHaveBeenCalled();
      fastforward(50);   // 第 100 ms
      expect(mockFn).not.toHaveBeenCalled();
      fastforward(50);   // 第 150 ms
      expect(mockFn).toHaveBeenCalledTime(1);
      jest.useRealTimers();
      MockDate.reset();
    });
  });
  在 mock timer 的帮助下,我们可以测试在实际使用时可能会用到很长时间间隔定时器的模块,例如一个跨天的倒计时模块。它可以方便地让测试覆盖到代码的每一个分支。
  总结
  代码测试能够保障代码的质量和功能,在开发过程进行测试能够提前发现 bug,在进行代码维护、移植或重构时,测试能够保障代码功能的完整性。对于一些复用度高、需要长期维护的公用代码来说,利用测试来进行质量保障是非常有必要的。
《2023软件测试行业现状调查报告》独家发布~

关注51Testing

联系我们

快捷面板 站点地图 联系我们 广告服务 关于我们 站长统计 发展历程

法律顾问:上海兰迪律师事务所 项棋律师
版权所有 上海博为峰软件技术股份有限公司 Copyright©51testing.com 2003-2024
投诉及意见反馈:webmaster@51testing.com; 业务联系:service@51testing.com 021-64471599-8017

沪ICP备05003035号

沪公网安备 31010102002173号