当前位置: 首页 > 工具软件 > Jest > 使用案例 >

Jest

鲁靖
2023-12-01

Jest 是 Facebook 的一套开源的 JavaScript 测试框架。相比其他的测试框架,Jest具有如下的一些特点:

  1. 适应性:Jest是模块化、可扩展和可配置的;
  2. 沙箱和快速:Jest虚拟化了JavaScript的环境,能模拟浏览器,并且并行执行;
  3. 快照测试:Jest能够对React 树进行快照或别的序列化数值快速编写测试,提供快速更新的用户体验;
  4. 支持异步代码测试:支持promises和async/await;
  5. 自动生成静态分析结果:不仅显示测试用例执行结果,也显示语句、分支、函数等覆盖率;

在软件的测试领域,测试主要分为:单元测试、集成测试和功能测试。
单元测试:又称为模块测试,就是对每个单元进行的测试,一般针对的是函数、类或单个组件,不涉及系统和集成。
集成测试,也叫组装测试或联合测试,在单元测试的基础上,将所有模块按照设计要求组装成为子系统或系统,进行集成测试。
功能测试,就是对产品的各功能进行验证,根据功能测试用例,逐项测试,检查产品是否达到用户要求的功能。

安装:

使用yarnyarn add --dev jest或者npmnpm install --save-dev jest进行安装。

生成基础配置文件:

Jest将根据项目提出一系列问题,并且将创建一个基础配置文件jest.config.js。

npx jest --init

使用:

Jest中可以使用console,不能使用debugger。

  1. 首先创建sum.js被测试文件,假设有一个函数:
    function sum(a, b) {
      return a + b;
    }
    module.exports = sum;
    
  2. 然后创建一个名为sum.test.js的测试文件。
    const sum = require('./sum');
    
    test('adds 1 + 2 to equal 3', () => {
      expect(sum(1, 2)).toBe(3);
    });
    
  3. 将以下配置部分添加到package.json 里面:
    {
      "scripts": {
        "test": "jest"
      }
    }
    
  4. 运行 yarn testnpm run test 命令,Jest会自动找到当前目录下所有的*.test.js*.spec.js文件并执行。Jest将打印下面这个消息:
    PASS  ./sum.test.js
    ✓ adds 1 + 2 to equal 3 (5ms)
    

匹配器:

Jest提供了expect函数用来包装被测试的方法并返回一个对象,该对象中包含一系列的匹配器来更方便的进行断言。

普通匹配器:
  1. toBe():是否精确匹配。
    test('two plus two is four', () => {
      expect(2 + 2).toBe(4);
    });
    
    如果要检查对象的值,要使用toEqual()代替,toEqual()递归检查对象或数组的每个字段。
    test('object assignment', () => {
      const data = {one: 1};
      data['two'] = 2;
      expect(data).toEqual({one: 1, two: 2});
    });
    
  2. not:允许测试结果不等于某个值的情况。
    test('adding positive numbers is not zero', () => {
      for (let a = 1; a < 10; a++) {
        for (let b = 1; b < 10; b++) {
          expect(a + b).not.toBe(0);
        }
      }
    });
    
字符串:
  1. toHaveLength():用来测试字符串和数组类型的长度是否满足预期。
  2. toMatch():可以通过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/);
    });
    
数字:
  1. toBe()/toEqual():等于。对于数字来说,toBe()和toEqual()是等价的。
  2. toBeCloseTo():浮点数相等要使用toBeCloseTo()。
  3. toBeGreaterThan():大于。
  4. toBeGreaterThanOrEqual():大于等于。
  5. toBeLessThan():小于。
  6. toBeLessThanOrEqual(): 小于等于。
test('two plus two', () => {
  const value = 2 + 2;
  expect(value).toBeGreaterThan(3);
});

test('adding floating point numbers', () => {
  const value = 0.1 + 0.2;
  expect(value).toBeCloseTo(0.3); 
});
Truthiness:

在测试中,有时需要精准区分 undefined 、null 和 false,有时又不需要区分,jest 都满足。

  1. toBeNull():只匹配 null。
  2. toBeUndefined():只匹配 undefined。
  3. toBeDefined ():与 toBeUndefined() 相反。
  4. toBeTruthy(): 匹配任何 if 语句为真。
  5. toBeFalsy(): 匹配任何 if 语句为假。
test('null', () => {
  const n = null;
  expect(n).toBeNull();
});
Arrays and iterables:

可以通过 toContain() 来检查一个数组或可迭代对象是否包含某个特定项。

const shoppingList = [
  'diapers',
  'kleenex',
  'trash bags',
  'paper towels',
  'beer',
];
 
test('the shopping list has beer on it', () => {
  expect(shoppingList).toContain('beer');
  expect(new Set(shoppingList)).toContain('beer');
});
Exceptions:

toThrow():测试被测试方法是否按照预期抛出异常。

必须使用一个函数将将被测试的函数做一个包装,否则会因为函数抛出导致该断言失败。

function compileAndroidCode() {
  throw new ConfigError('you are using the wrong JDK');
}
 
test('compiling android goes as expected', () => {
  expect(compileAndroidCode).toThrow();
  expect(compileAndroidCode).toThrow('you are using the wrong JDK');
});

生命周期勾子:

jest 测试提供了一些测试的生命周期 API,可以辅助在每个 case 的开始和结束做一些处理。

  1. beforeAll:在所有测试开始前执行;
  2. afterAll:在所有测试结束后执行;
  3. beforeEach:在每个测试开始前执行;
  4. afterEach:在每个测试结束后执行;

用例:

用例的表示:

test()函数来描述一个测试用例。

test('should get "Hello world"', () => {
    expect(hello()).toBe('Hello world') 
})
用例的预处理或后处理:

有时想在测试开始之前进行下环境的检查,或者在测试结束之后作一些清理操作,这就需要对用例进行预处理或后处理。

对测试文件中所有的用例进行统一的预处理,可以使用 beforeAll() 函数。后处理有对应的 afterAll()函数。

而如果想在每个用例开始前进行都预处理,则可使用 beforeEach() 函数。后处理有对应的afterEach() 函数。

如果只是想对某几个用例进行同样的预处理或后处理,可以将先将这几个用例归为一组。使用 describe() 函数即可表示一组用例,再将上面提到的四个处理函数置于 describe() 的处理回调内,就实现了对一组用例的预处理或后处理。

describe('test testObject', () => {
    beforeAll(() => {
        // 预处理操作
    })
    test('is foo', () => {
       expect(testObject.foo).toBeTruthy()
    })
    test('is not bar', () => {
        expect(testObject.bar).toBeFalsy(
    })
    afterAll(() => {
        // 后处理操作
    })
})

测试异步代码:

测试异步代码的三种实现方式:回调函数、Promise、Async/Await。

回调函数:

例如:有一个fetchData(callback)的函数用来获取数据,并且在获取完成的时候调用callback 函数,希望测试返回的数据是“peanut butter” 。

// 错误
test('the data is peanut butter', () => {
  function callback(data) {
    expect(data).toBe('peanut butter');
  }
 
  fetchData(callback);
});

上面代码的问题在于:一旦fetchData完成,Jest测试也就执行完成,然后才会调用回调,这并不是按所期望的那样的去运行。

Jest提供了一种用于测试异步代码的实现方式, done() 被执行则意味着callback函数被调用。

test('the data is peanut butter', done => {
  function callback(data) {
    expect(data).toBe('peanut butter');
    done();
  }
 
  fetchData(callback);
});
Promise:

如果代码中使用了Promise的方式,处理异步测试将会变得更加简单。Jest从测试中返回一个Promise,然后等待Promise被实现,如果没有实现,那么就判断测试是失败的。

test('the data is peanut butter', () => {
  expect.assertions(1);  //assertions(1)代表的是在当前的测试中至少有一个断言是被调用的,否则判定为失败。

  return fetchData().then(data => {
    expect(data).toBe('peanut butter');
  });
});

如果断言的承诺并没有被实现,那么可以添加 .catch 方法。

test('the fetch fails with an error', () => {
  expect.assertions(1);
  return fetchData().catch(e => expect(e).toMatch('error'));
});
Async/Await:
test('the data is peanut butter', async () => {
  expect.assertions(1);
  const data = await fetchData();
  expect(data).toBe('peanut butter');
});
 
test('the fetch fails with an error', async () => {
  expect.assertions(1);
  try {
    await fetchData();
  } catch (e) {
    expect(e).toMatch('error');
  }
});

mock函数:

mock测试就是在测试过程中,对于某些不容易构造或者不容易获取的对象,用一个虚拟的对象来创建以便继续进行测试的测试方法。

Mock函数提供的以下三种特性:

  1. 捕获函数调用情况;
  2. 设置函数返回值;
  3. 改变函数的内部实现;

在实际项目的单元测试中,jest.fn()常被用来进行某些有回调函数的测试;jest.mock()可以mock整个模块中的方法,当某个模块已经被单元测试100%覆盖时,使用jest.mock()去mock该模块,节约测试时间和测试的冗余度是十分必要;当需要测试某些必须被完整执行的方法时,常常需要使用jest.spyOn()。

jest.fn():

jest.fn() 是创建Mock函数最简单的方式。

jest.fn()所创建的Mock函数可以设置返回值。

// functions.test.js
test('测试jest.fn()返回固定值', () => {
  let mockFn = jest.fn().mockReturnValue('default');
  // 断言mockFn执行后返回值为default
  expect(mockFn()).toBe('default');
})

还定义内部实现或返回Promise对象。如果没有定义函数内部的实现,jest.fn() 会返回undefined作为返回值。

// functions.test.js
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]");
})

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);
})

例如:在fetch.js中封装了一个fetchPostsList方法,该方法请求了JSONPlaceholder提供的接口,并通过传入的回调函数返回处理过的返回值。如果想测试该接口能够被正常请求,只需要捕获到传入的回调函数能够被正常的调用即可。

// fetch.js
import axios from 'axios';
export default {
  async fetchPostsList(callback) {
    return axios.get('https://jsonplaceholder.typicode.com/posts').then(res => {
      return callback(res.data);
    })
  }
}

// functions.test.js
import fetch from '../src/fetch.js'
test('fetchPostsList中的回调函数应该能够被调用', async () => {
  expect.assertions(1);
  let mockFn = jest.fn();
  await fetch.fetchPostsList(mockFn);

  // 断言mockFn被调用
  expect(mockFn).toBeCalled();
})
jest.mock():

jest.mock()可以mock整个模块。

通过 jest.mock() 后,模块内的方法是不会被 jest 实际执行的。

例如:fetch.js文件夹中封装的请求方法在其他模块被调用的时候,并不需要进行实际的请求(请求方法已经通过单侧或需要该方法返回非真实数据)。

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

// functions.test.js
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.spyOn():

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

通过 jest.spyOn() 后,模块内的方法是被 jest 实际执行。

// functions.test.js
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);
})
 类似资料: