前端单元测试入门实践(下)

发表于:2024-1-11 09:49

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

 作者:黄成翰    来源:稀土掘金

  常见测试场景
  匹配
  toBe与toEqual
  两者都是用来验证相等的断言,toBe常用来比较值是否相等,toEqual常用来比较引用类型是否等价,按照官方解释,toEqual会递归对比对象实例的所有属性,因此也被称作深度相等。
  test('相等', () => {
    const foo = { bar: 1 };
    expect(foo.bar).toBe(1); // 通过
    expect(foo).toBe({ bar: 1 }); // 不通过
    expect(foo).toEqual({ bar: 1 }); // 通过
  });
  not
  not修饰符用来表示取反,用在其他断言之前,比如
  test('not', () => {
    const foo = 1;
    expect(foo).not.toBe(2); // 通过
  });
  toMatch
  toMatch允许我们传入一个正则表达式,用来精确匹配字符串。
  test('match', () => {
    const foo = 'hello jest';
    expect(foo).toMatch(/hello/i); // 通过
  });
  toContain
  toContain用来检测对象中是否包含某个值。
  test('contain', () => {
    const data = ['foo', 'bar'];
    expect(data).toContain('fo'); // 不通过
  });
  其他匹配器
  ·toBeNull:是否 null
  · toBeUndefined:是否 undefined
  · toBeDefined:是否定义
  · toBeTruthy:是否为真
  · toBeFalsy:是否为假
  · toBeGreaterThan 大于
  · toBeGreaterThanOrEqual 大于等于
  · toBeLessThan 小于
  · toBeLessThanOrEqual 小于等于
  · toBeCloseTo 匹配浮点数
  函数
  测试异常
  可以使用toThrow来测试函数执行过程中是否抛出错误,需要注意的是,在Jest中我们必须对被测试函数再做一层包装才有效。
  function onlyNumber(param) {
    if (typeof param !== 'number') {
      throw Error('Only Number!');
    }
    return param;
  }
  test('throw', () => {
    expect(onlyNumber('1')).toThrow('Only Number!'); // 通过
  });
  测试回调
  关键是需要手动调用done()。
  function getName(callback) {
    new Promise((resolve) => {
      setTimeout(() => {
        resolve('bar');
      }, 1000);  
    }).then((res) => {
      callback(res);
    });
  }
  describe('test callback', () => {
    // 用例1, 错误用法
    test('fault', () => {
      getName((res) => {
        expect(res).toBe('foo'); // 通过
      });
    });
    // 用例2, 正确用法
    test('right', (done) => {
      getName((res) => {
        expect(res).toBe('foo'); // 不通过
        done();
      });
    });
  });
  在错误的用例1中,无论断言是toBe什么,只要被测试函数本身不报错,测试用例都会通过,因为回调函数本身并未执行。
  需要在回调函数中手动调用done(),表示该回调函数执行以后,用例才算通过。
  测试异步
  这块测试我们可以通过使用 async 和 await 关键字来做到和写法和同步代码基本一致(推荐),也可以使用jest官方提供的resolves和rejects修饰符,当然,使用.then之类的链式写法也可以。后两种方法记得return。
  // 模拟一个异步操作
  function getName() {
    return new Promise((resolve) => {
      setTimeout(() => {
        resolve('bar');
      }, 1000);
    });
  }
  describe('test promise', () => {
    // 写法1
    test('style1', async () => {
      const res = await getName();
      expect(res).toBe('bar');
    });
    // 写法2
    test('style2', () => {
      return expect(getName()).resolves.toBe('bar');
    });
    // 写法3
    test('style3', () => {
      return getName().then((res) => {
        expect(res).toBe('bar');
      })
    });
  });
  如果在测试时发现jest不认识async、await等关键字,那就需要加强下babel,首先安装 @babel/plugin-transform-runtime。
  npm install --save-dev @babel/plugin-transform-runtime
  配置babel
  {
    "plugins": ["@babel/plugin-transform-runtime"]
  }
  Mock
  mock函数
  mock函数常用于被测试内部结构复杂的函数的运行是否符合预期,mock函数就是jest提供的一个被监控的函数,函数的参数、返回值、函数体、被调用次数等一切属性都可以自定义或观察。
  // 创建 mock 函数
  const func = jest.fn();
  // mock 函数返回值, 不 mock 默认 undefined
  func.mockReturnValue(1);
  // mock 函数返回值, 只生效一次,需要多次使用,方便 mock 不同情况下的返回值
  func.mockReturnValueOnce(1);
  // mock 函数实现
  func.mockImplementation((params) => {
    console.log('func be called with', params);
  });
  // mock 函数实现,只生效一次
  func.mockImplementationOnce(() => {});
  // mock 函数被调用次数
  console.log(func.mock.calls.length);
  // mock 函数第 i + 1 次被调用时所接受的实参
  let i = 2;
  console.log(func.mock.calls[i]);
  // mock 函数第 i + 1 次被调用时内部的 this 指向
  console.log(func.mock.instances[i]);
  具体的例子在mock定时器一节中。
  mock定时器
  在实际的被测试代码中,可能会遇到代码中存在计时器的延时操作。假如这个时间是30s,那么我们在对这块代码做测试时一定不想等待30s。因此,jest提供了一些与计时器有关的API来帮助我们跳过或加速时间。
  // 被测试代码
  function handleData(callback) {
    setTimeout(() => {
      callback('bar');
      seTimeout(() => {
        callback('foo');
      }, 1000);
    }, 30 * 1000);
  }
  // 测试代码
  // 每个测试用例都 mock 一个定时器, 避免串扰
  beforeEach(() => { // beforeEach是jest提供的生命周期钩子, 每个测试用例执行前触发
    jest.useFakeTimers(); // 一定要在 mock 计时器前进行声明
  });
  describe('test mock timer', () => {
    test('runAllTimers', () => {
      // mock 一个 callback 来用,
      // 因为在本次测试中我们只关心计时器, 具体的内部操作我们不关心,
      // 因此很适合用mock函数, 这样做的好处是假如内部是个耗时操作, 我们可以直接规避
      const fn = jest.fn();
      // 执行被测试代码
      handleData(fn);
      // 一次性执行完所有计时器
      jest.runAllTimers();
      // 回调执行了2次
      expect(fn.mock.calls.length).toBe(2); // 通过
      // 2次实参分别是'bar'和'foo'
      expect(fn.mock.calls).toEqual(['bar', 'foo']); // 通过
    });
    test('runOnlyPendingTimers', () => {
      const fn = jest.fn();
      handleData(fn);
      // 仅执行处于消息队列中的计时器
      jest.runOnlyPendingTimers();
      // 显然回调只会执行外层的一次
      expect(fn.mock.calls.length).toBe(1); // 通过
    });
    test('advanceTimersByTime', () => {
      const fn = jest.fn();
      handleData(fn);
      // 快进30s, 执行完外层计时器
      jest.advanceTimersByTime(30 * 1000);
      expect(fn.mock.calls.length).toBe(1); // 通过
      // 快进1s, 执行完内层计时器
      jest.advanceTimersByTime(1000);
      expect(fn.mock.calls.length).toBe(2); // 通过
    });
  });
  mock模块
  我们在代码中总是会引用到第三方模块,如果我们要测试的某一段代码依赖某个第三方模块,我们此时就可以mock它。最常见的例子就是网络请求。要mock一个模块,我们可以在被测模块的同级目录下创建一个__mocks__文件夹,并且在该文件下创建同名的mock模块;也可以不用这么做,直接在测试文件中导入原有模块,用jest.mock来mock想mock的模块。
  // 被mock模块
  import axios from 'axios';
  function fetchData1() {
    return axios.get('https://xxx.xxx.xxx/xxx', res => res.data);
  }
  function fetchData2() {
    return axios.get('https://xxx.xxx.xxx/xxx', res => res.data);
    // 预计返回
    // { type: '5G' }
  }
  export { fechData1, fetchData2 };
  // mock 指定路径下的模块
  jest.mock('./service', () => {
    // 导入真实模块内容
    const actualModules = jest.requireActual('./service');
    // 混入 mock 内容
    return {
      ...actualModules,
      fetchData1: jest.fn(() => {
        return new Promise((resolve, reject) => {
          resolve({
            foo: 'bar',
          });
        });
      }),
    };
  });
  // 此时导入的 fetchData1 是我们 mock 的, fetchData2 是原有的
  import { fetchData1, fetchData2 } from './service';
  describe('test mock modules', () => {
    test('fetchData1', async () => {
      const res = await fetchData1();
      expect(res).toEqual({foo: 'bar'}); // 通过
    });
    test('fetchData2', async () => {
      const res = await fechData2();
      expect(res).toEqual({type: '5G'}); // 预期通过
    });
  });
  本文内容不用于商业目的,如涉及知识产权问题,请权利人联系51Testing小编(021-64471599-8017),我们将立即处理
《2023软件测试行业现状调查报告》独家发布~

关注51Testing

联系我们

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

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

沪ICP备05003035号

沪公网安备 31010102002173号