前端单元测试入门(Jest)

开始

Jest是由Facebook发布的开源的、基于Jasmine的Javascript的单元测试框架。官方文档在这里

安装:

npm install --save-dev jest

然后创建一个sum.js文件,输出一个方法:

function sum(a, b) {
  return a + b;
}
module.exports = sum;

然后,创建一个名为sum.test.js的文件。这将包含我们的实际测试︰

const sum = require('./sum');

test('adds 1 + 2 to equal 3', () => {
  expect(sum(1, 2)).toBe(3);
});

然后将下面的配置部分添加到你的package.json里面:

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

然后就可以通过npm run test来启动单元测试,测试结果:

PASS  ./sum.test.js
✓ adds 1 + 2 to equal 3 (5ms)

webstorm增加Jest语法提示

WebStormPreferencesLanguages & FrameworksJavaScriptLibraries, 点击Download然后找到列表页中的Jest, 然后点击Download and Install.

匹配器

Jest通过匹配器来测试方法的结果是否符合预期,完整的API列表在这里

普通匹配器

最简单的测试值的方法是看是否精确匹配

test('two plus two is four', () => {
  expect(2 + 2).toBe(4);
});
  • expect的用处调用被测试的函数或者方法,返回一个结果
  • toBe就是匹配器,判断结果和期望是否相等

toBe使用的是Object.is()判断相等,如果要检查对象的值,需要使用toEqual

test('object', () => {
  let obj = {value: 'a'};
  expect(obj.value).toEqual('a')
});

真假值

JS中,undefinednullfalse有时需要区分,有时不需要,所以Jest提供了下面的API来进行匹配:
- toBeNull:只匹配null
- toBeUndefined:只匹配undefined
- toBeDefined:与toBeUndefined相反
- toBeNull:只匹配null
- toBeTruthy:匹配任何if语句为真
- toBeFalsy:匹配任何if语句为假

数字

对于数字有各种等价的匹配器
- toBe:=
- toEqual:=
- toBeGreaterThan:>
- toBeGreaterThanOrEqual:≥
- toBeLessThan:<
- toBeLessThanOrEqual:≤

对于浮点数,应该使用toBeClose而不是toEqual

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

字符串

使用toMatch来验证字符串中是否包含指定的字符串或者正则表达式

test('this is a joe in the sentence?', () => {
  expect('this is joe').toMatch(/joe/)
});

数组

使用toContain来验证数组中是否包含匹配子项

test('the Array contains the item?', () => {
  const arr = ['a', 'n', 'cc'];
  expect(arr).toContain('cc')
});

异常

如果一个函数在某些情况下会抛出错误,Jest也可以对这种情况进行预期,使用的API是toThrow

function fn() {
  throw new Error('this is a error')
}

test('function will throw an error', () => {
  expect(fn).toThrow();
  expect(fn).toThrow(Error);

  expect(fn).toThrow('this is a error');
  expect(fn).toThrow(/is/);
});

完整的API列表在这里

测试异步代码

异步代码的测试是很常见的一个需求,当运行异步代码时,Jest需要知道它当前测试的异步代码什么时候完成,才可以运行下一个测试。Jest 有多种办法来处理这种情况。

回调

最常见的异步模式是回调函数。

举个例子,假设你有一个叫做fetchData(callback)的函数,可以去获取一些数据,拿到数据之后就会调用callback(data)。你想要这个返回的数据是一个'peanut butter'字符串。默认情况下,Jest测试走到它们运行期的结束阶段就算是结束了。这意味着这个测试不会按照你预期的那样运行:

// Don't do this!
test('the data is peanut butter', () => {
  function callback(data) {
    expect(data).toBe('peanut butter');
  }

  fetchData(callback);
});

问题在于这个测试在fetchData运行完的那一瞬间就算结束了,它根本就来不及去调用那个回调函数(callback)。

为了解决这个问题,引入done()单参数,Jest会等待done结束执行后结束执行

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

  fetchData(callback);
});

如果done()没有被调用,测试就会失败,这也是你所希望发生的。

Promise

如果异步代码使用了Promise,处理方法更加简单,只需要在测试中返回一个Promise,Jest会等待Promise完成在进行校验,如果Promise被异常reject,则测试失败

function fetchDate(callBack) {
  return new Promise((resolve, reject) => {
    setTimeout(resolve, 3000, 'ok')
  })
}

test('the data is ok', () => {
  expect.assertions(1);
  return fetchDate().then(val => {
    expect(val).toBe('ok')
  })
});

上面的expect.assertions(1)的意思是指定这个测试中的断言的数量,在测试异步代码时经常会用到这个方法,确保异步代码的全部的回调函数都被调用

注意:测试方法一定要return一个promise,否则测试会在异步调用发起之后立刻结束

如果Promise的结果是reject,需要使用.catch方法

function fetchDate(callBack) {
  return new Promise((resolve, reject) => {
    setTimeout(reject, 3000, 'ok')
  })
}

test('the data is ok', () => {
  expect.assertions(1);
  return fetchDate().catch(val => {
    expect(val).toBe('ok')
  })
});

then()方法可以用resolves方法替代,catch方法可以用rejects方法替代:

test('the data is ok', () => {
  expect.assertions(1);
  return expect(fetchDate()).resolves.toBe('ok')
});

test('the data is ok', () => {
  expect.assertions(1);
  return expect(fetchDate()).rejects.toBe('ok')
});

Async/Await

可以在测试中使用asyncawait。 若要编写async测试,只要在传给test的函数前面加上async关键字就行了。

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

同样,在async函数中也可以使用resolvesrejects方法

test('the data is peanut butter', async () => {
  expect.assertions(1);
  await expect(fetchData()).resolves.toBe('peanut butter');
});

test('the fetch fails with an error', async () => {
  expect.assertions(1);
  await expect(fetchData()).rejects.toMatch('error');
});

测试准备和测后整理

辅助函数

Jest提供了辅助函数帮助我们在运行测试前做一些准备工作,在测试结束后进行整理工作:

  • beforeEachafterEach:在每个测试前/后执行
  • beforeAllafterAll:在文件前/后执行
  • beforeafter的块在describe块内部时,就只适用于该describe块内的测试

desribetest块的执行顺序

Jest会在真正的测试(test)开始之前执行测试文件里所有的describe处理程序,当describe运行后会按照test出现的顺序运行所有测试
(可以把每个test当做异步任务看待)

describe('outer', () => {
  console.log('describe outer-a');

  describe('describe inner 1', () => {
    console.log('describe inner 1');
    test('test 1', () => {
      console.log('test for describe inner 1');
      expect(true).toEqual(true);
    });
  });

  console.log('describe outer-b');

  test('test 1', () => {
    console.log('test for describe outer');
    expect(true).toEqual(true);
  });

  describe('describe inner 2', () => {
    console.log('describe inner 2');
    test('test for describe inner 2', () => {
      console.log('test for describe inner 2');
      expect(false).toEqual(false);
    });
  });

  console.log('describe outer-c');
});

// describe outer-a
// describe inner 1
// describe outer-b
// describe inner 2
// describe outer-c
// test for describe inner 1
// test for describe outer
// test for describe inner 2

单独测试

如果测试失败,可以使用test.only来单独运行一个测试:

test.only('this will be the only test that runs', () => {
  expect(true).toBe(false);
});

test('this test will not run', () => {
  expect('A').toBe('A');
});

Mock Function

Mock函数用来模拟函数、方法的外部依赖,让我们清除无关的影响,专注于被测代码本身

有两种方式可以mock函数:
1. 在测试代码中创建一个mock函数
2. 自己编写一个manual mock来覆盖模块之间的依赖

使用Mock函数

创建一个mock函数很简单:

const mockCallback = jest.fn();

mock函数一些重要的API:

  • mockFn.mockName(value):为jest.fn()设定一个名称,用于输出时辨识使用
  • mockFn.getMockName:获取使用mockFn.mockName设定的mock函数名
  • mockFn.mock:所有的mock函数都有一个特殊的属性.mock,它保存了次函数被调用的信息,并且追踪了每次调用时this的值
  • mockFn.mock.calls:函数被调用时传入的参数数组组成的二维数组,比如函数fn()被调用了两次,第一次是fn(arg1, arg2),第二次是fn(arg3,arg4),这时fn.mock.calls就是[[arg1, arg2], [arg3, arg4]]
  • mockFn.mock.instances:函数通过new关键字被实例化成员组成的数组,例如:

    const mockFn = jest.fn();
    
    const a = new mockFn();
    const b = new mockFn();
    
    mockFn.mock.instances[0] === a; // true
    mockFn.mock.instances[1] === b; // true
  • mockFn.mockReturnValue(value):接受一个参数,在mock函数被调用时总被作为返回值返回
  • mockFn.mockReturnValue(value):接受一个参数,在mock函数被调用时作为返回值返回一次
  • mockFn.mockReturnThis():语法糖,返回this

这样就可以使用mock函数进行测试:

function forEach(items, callback) {
  for (let index = 0; index < items.length; index++) {
    callback(items[index]);
  }
}

const mockCallback = jest.fn();
forEach([0, 1], mockCallback);
// 此模拟函数被调用了两次
expect(mockCallback.mock.calls.length).toBe(2);

// 第一次调用函数时的第一个参数是 0
expect(mockCallback.mock.calls[0][0]).toBe(0);

// 第二次调用函数时的第一个参数是 1
expect(mockCallback.mock.calls[1][0]).toBe(1);

Mock函数也可以用于在测试期间将测试值注入您的代码︰

const myMock = jest.fn();
console.log(myMock());
// > undefined

myMock
  .mockReturnValueOnce(10)
  .mockReturnValueOnce('x')
  .mockReturnValue(true);

console.log(myMock(), myMock(), myMock(), myMock());
// > 10, 'x', true, true

mock函数非常适用于函数式编程的代码中:

const filterTestFn = jest.fn();

// Make the mock return `true` for the first call,
// and `false` for the second call
filterTestFn.mockReturnValueOnce(true).mockReturnValueOnce(false);

const result = [11, 12].filter(filterTestFn);

console.log(result);
// > [11]
console.log(filterTestFn.mock.calls);
// > [ [11], [12] ]

mock实现

有时候mock函数不光要记录被调用的情况,还需要执行一些特定的操作,可以通过mockImplementation实现(或者直接向jest.fn()中传入一个函数作为参数)

const mockFn = jest.fn().mockImplementation(scalar => 42 + scalar);
// or: jest.fn(scalar => 42 + scalar);

const a = mockFn(0);
const b = mockFn(1);

a === 42; // true
b === 43; // true

mockFn.mock.calls[0][0] === 0; // true
mockFn.mock.calls[1][0] === 1; // true

有时候函数是从其他的模块中引入的,我们也需要将其mock掉:

// foo.js
module.exports = function() {
  // some implementation;
};

// test.js
jest.mock('../foo'); // this happens automatically with automocking
const foo = require('../foo');

// foo is a mock function
foo.mockImplementation(() => 42);
foo();
// > 42

当被模拟的函数运行完了mockImplementationOnce定义的实现时,它将执行jest.fn(如果被定义了)的默认实现:

const myMockFn = jest
  .fn(() => 'default')
  .mockImplementationOnce(() => 'first call')
  .mockImplementationOnce(() => 'second call');

console.log(myMockFn(), myMockFn(), myMockFn(), myMockFn());
// > 'first call', 'second call', 'default', 'default'

对于我们有通常链接的方法(因此总是需要返回this)的情况,我们有一个语法,API以.mockReturnThis()函数的形式来简化它,它也位于所有模拟器上:

const myObj = {
  myMethod: jest.fn().mockReturnThis(),
};

// is the same as

const otherObj = {
  myMethod: jest.fn(function() {
    return this;
  }),
};

mock函数的匹配器

Jest为我们提供了一些mock函数的匹配器

// 这个 mock 函数至少被调用一次
expect(mockFunc).toBeCalled();

// 这个 mock 函数至少被调用一次,而且传入了特定参数
expect(mockFunc).toBeCalledWith(arg1, arg2);

// 这个 mock 函数的最后一次调用传入了特定参数
expect(mockFunc).lastCalledWith(arg1, arg2);

// 所有的 mock 的调用和名称都被写入了快照
expect(mockFunc).toMatchSnapshot();

这些都是语法糖,我们都可以通过mockFn.mock属性获取到:

// 这个 mock 函数至少被调用一次
expect(mockFunc.mock.calls.length).toBeGreaterThan(0);

// 这个 mock 函数至少被调用一次,而且传入了特定参数
expect(mockFunc.mock.calls).toContain([arg1, arg2]);

// 这个 mock 函数的最后一次调用传入了特定参数
expect(mockFunc.mock.calls[mockFunc.mock.calls.length - 1]).toEqual([
  arg1,
  arg2,
]);

//  这个 mock 函数的最后一次调用的第一个参数是`42`
// (注意这个断言的规范是没有语法糖的)
expect(mockFunc.mock.calls[mockFunc.mock.calls.length - 1][0]).toBe(42);

// 快照会检查 mock 函数被调用了同样的次数,
// 同样的顺序,和同样的参数 它还会在名称上断言。
expect(mockFunc.mock.calls).toEqual([[arg1, arg2]]);
expect(mockFunc.mock.getMockName()).toBe('a mock name');

在Vue中配置Jest

自动配置

通过Vue-cli创造了模板脚手架时,可以选择是否启用单元测试,并且选择单元测试框架,这样Vue就帮助我们自动配置好了Jest。

手动配置

如果需要手动配置的话,首先要安装Jest和Vue Test Utils

npm install --save-dev jest @vue/test-utils

然后在package.json中定义一个单元测试的脚本。

// package.json
{
  "scripts": {
    "test": "jest"
  }
}

为了告诉Jest如何处理*.vue文件,需要安装和配置vue-jest预处理器:

npm install --save-dev vue-jest

接下来在package.json中创建一个jest块:

{
  // ...
  "jest": {
    "moduleFileExtensions": [
      "js",
      "json",
      // 告诉 Jest 处理 `*.vue` 文件
      "vue"
    ],
    "transform": {
      // 用 `vue-jest` 处理 `*.vue` 文件
      ".*\\.(vue)$": "<rootDir>/node_modules/vue-jest"
    }
  }
}

在自动配置的时候,这部分内容是作为test文件夹下jest.conf.js配置文件单独出现的,在启动单元测的时候指定了这个配置文件:

"scripts": {
  "unit": "jest --config test/unit/jest.conf.js --coverage",
  "test": "npm run unit",
},

注意:vue-jest目前并不支持vue-loader所有的功能,比如自定义块和样式加载。额外的,诸如代码分隔等 webpack 特有的功能也是不支持的。如果要使用这些不支持的特性,你需要用 Mocha 取代 Jest 来运行你的测试,同时用 webpack 来编译你的组件。

如果在webpack中配置了别名解析,比如把@设置为/src的别名,那么你也需要用moduleNameMapper选项为Jest增加一个匹配配置:

{
  // ...
  "jest": {
    // ...
    // 支持源代码中相同的 `@` -> `src` 别名
    "moduleNameMapper": {
      "^@/(.*)$": "<rootDir>/src/$1"
    }
  }
}

尽管最新版本的Node已经支持绝大多数的ES2015特性,你可能仍然想要在你的测试中使用ES modules语法和stage-x的特性。为此我们需要安装 babel-jest

npm install --save-dev babel-jest

接下来,需要在package.jsonjest.transform里添加一个入口,来告诉Jest用babel-jest处理JavaScript测试文件:(同样可以在jest.conf.js进行配置)

假设webpack使用了babel-preset-env,这时默认的Babel配置会关闭ES modules的转译,因为webpack已经可以处理ES modules了。然而,我们还是需要为我们的测试而开启它,因为Jest的测试用例会直接运行在Node上。

同样的,我们可以告诉babel-preset-env面向我们使用的Node版本。这样做会跳过转译不必要的特性使得测试启动更快。

了仅在测试时应用这些选项,可以把它们放到一个独立的 env.test 配置项中 (这会被babel-jest自动获取)。

.babelrc文件示例:

{
  "presets": [
    ["env", { "modules": false }]
  ],
  "env": {
    "test": {
      "presets": [
        ["env", { "targets": { "node": "current" }}]
      ]
    }
  }
}

测试覆盖率

Jest可以用来生成多种格式的测试覆盖率报告,通过扩展jest配置通常在package.jsonjest.config.js中) 的collectCoverage选项,然后添加collectCoverageFrom数组来定义需要收集测试覆盖率信息的文件。

{
  "jest": {
    // ...
    "collectCoverage": true,
    "collectCoverageFrom": [
      "**/*.{js,vue}",
      "!**/node_modules/**"
    ]
  }
}

这样就会开启默认格式的测试覆盖率报告。你可以通过coverageReporters选项来定制它们。

{
  "jest": {
    // ...
    "coverageReporters": ["html", "text-summary"]
  }
}
  • collectCoverage:配置测试覆盖率是否开启,默认是关闭的,应为会降低测试速度
  • collectCoverageFrom:配置收集测试覆盖率文件范围
  • coverageReporters:测试覆盖率输出形式,texttext-summary对应控制台的输出
  • coverageDirectory:测试覆盖率输出形式目录,配置为'<rootDir>/test/unit/coverage'

参考

猜你喜欢

转载自blog.csdn.net/duola8789/article/details/80197560