测试 2020-09-11 11:28:30

Jest是什么

Jest是 Facebook 的一套开源的 JavaScript 测试框架, 它自动集成了断言、JSDom、覆盖率报告等开发者所需要的所有测试工具,是一款几乎零配置的测试框架。并且它对同样是 Facebook 的开源前端框架 React 的测试十分友好。

他适用但不局限于使用以下技术的项目:Babel, TypeScript, Node, React, Angular, Vue

特性

1. 零配置

Jest的目标是在大部分JavaScript项目上实现开箱即用,无需配置。

2. 快照

构建能够轻松追踪大Object的测试。快照可以独立于测试代码,也可以集成进代码行内。

3. 隔离的

测试程序在自己的进程并行运算以最大限度地提高性能。

4. 优秀的 api

itexpect - Jest将整个工具包放在一个地方。好书写,好维护,非常方便。

入门指南

本文中我们使用yarn作为包管理器

安装

yarn add --dev jest

新增test脚本

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

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

编写你的第一个jest测试

创建一个 overlap.js 文件,作为被测试模块

/**
 * 判断两个区间是否重叠
 * @param {*} a 
 * @param {*} b 
 */
function overlap(a, b) {
  const [startA, endA] = a;
  const [startB, endB] = b;

  if (startA < endB && endA > startB) {
    return true;
  }
  return false;
}

module.exports = overlap;

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

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

const a = [5, 10];

test('[5, 10] overlap with [1,4]', () => {
  expect(overlap(a, [1,4])).toBe(false);
});

运行

运行yarn test命令

Jest将打印下面这个消息:

$ jest
 PASS  algorithm/overlap.test.js
  ✓ [5, 10] overlap with [1,4] (1 ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        1.182 s
Ran all test suites.
✨  Done in 2.24s.

<span id='babel'>使用Babel</span>

Jest本身是不支持ES6语法的,为了能够使用ES6的语法特性进行单元测试,我们需要使用Babel。

安装所需的依赖:

yarn add --dev babel-jest @babel/core @babel/preset-env

新增Babel配置文件

在工程的根目录下创建一个babel.config.js文件用于配置与你当前Node版本兼容的Babel:

// babel.config.js
module.exports = {
  presets: [
    [
      '@babel/preset-env',
      {
        targets: {
          node: 'current',
        },
      },
    ],
  ],
};

Babel的配置取决于具体的项目使用场景 ,可以查阅Babel官方文档来获取更多详细的信息。

匹配器

toBe

toBe 使用 Object.is

Truthiness

  • toBeNull 只匹配 null
  • toBeUndefined 只匹配 undefined
  • toBeDefined 与 toBeUndefined 相反
  • toBeTruthy 匹配任何 if 语句为真
  • toBeFalsy 匹配任何 if 语句为假

数字

  • toBeGreaterThan 大于
  • toBeGreaterThanOrEqual 大于等于
  • toBeLessThan 小于
  • toBeLessThanOrEqual 小于等于

对于比较浮点数相等,使用 toBeCloseTo 而不是 toEqual,因为你不希望测试取决于一个小小的舍入误差。

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

字符串

您可以检查对具有 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/);
});

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

异常

你可以通过 toThrow 测试函数被调用时是否抛出错误

function compileAndroidCode() {
  throw new Error('you are using the wrong JDK');
}

test('compiling android goes as expected', () => {
  expect(() => compileAndroidCode()).toThrow();
  expect(() => compileAndroidCode()).toThrow(Error);

  // You can also use the exact error message or a regexp
  expect(() => compileAndroidCode()).toThrow('you are using the wrong JDK');
  expect(() => compileAndroidCode()).toThrow(/JDK/);
});

toEqual

toEqual 递归检查对象或数组的每个字段。

匹配器详细文档

测试异步代码

在JavaScript中执行异步代码是很常见的。 当你有以异步方式运行的代码时,Jest 需要知道当前它测试的代码是否已完成,然后它可以转移到另一个测试。 Jest有若干方法处理这种情况

回调

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

默认情况下,Jest 测试一旦执行到末尾就会完成。 那意味着该测试将不会按预期工作:

function fetchData(callback) {
  const data = 'peanut butter'
  setTimeout(() => {
    callback(data);
  }, 2000);
}

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

  fetchData(callback);
});

callback未执行测试就结束了

我们可以在test方法的回调函数中传入参数done,Jest会等done回调函数执行结束后结束测试。

test('the data is peanut butter', done => {
  function callback(data) {
    try {
      expect(data).toBe('peanut butter');
      done();
    } catch (error) {
      done(error);
    }
  }
  // 上例中的fetchData函数  
  fetchData(callback);
});

若done函数从未被调用,测试用例会报超时错误,默认5000ms

Timeout - Async callback was not invoked within the 5000 ms timeout specified by jest.setTimeout.Timeout - Async callback was not invoked within the 5000 ms timeout specified by jest.setTimeout.Error:

若 expect 执行失败,它会抛出一个错误,后面的 done() 不再执行。 若我们想知道测试用例为何失败,我们必须将 expect 放入 try 中,将 error 传递给 catch 中的 done函数。 否则,最后控制台将显示一个超时错误失败,不能显示我们在 expect(data) 中接收的值。

Promises

test回调函数,必须返回一个promise,如果return,那么就会出现promise在settled之前,测试用例就已经结束。

// 测试promise
function fetchData(type) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (type === 200) {
        resolve('peanut butter');
      } else {
        reject('error')
      }
    }, 2000);
  });
}
test('the data is peanut butter',  () => {
  return fetchData(200).then(data => {
    expect(data).toBe('peanut butter');
  })
});

如果你期望一个promise被rejected,可以使用.catch方法,需要确保添加expect.assertions 来验证一定数量的断言被调用。

test('the fetch fails with an error', () => {
  expect.assertions(1);
  return fetchData().catch(e => expect(e).toMatch('error'));
});

.resolves

您也可以在 expect 语句中使用 .resolves 匹配器,Jest 将等待此 Promise 决议。 如果承诺被拒绝,则测试将自动失败。

test('the data is peanut butter', () => {
  return expect(fetchData(200)).resolves.toBe('peanut butter');
});

.rejects

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

Async/Await

我们也可以在测试中使用 async 和 await

test('the data is peanut butter', async () => {
  const data = await fetchData(200);
  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');
  }
});

expect.assertions(number)

expect.assertions(number) 可以校验测试过程中是否调用了一定数量的断言

断言是编程术语,表示为一些布尔表达。
编写代码时,我们总是会做出一些假设,断言就是用于在代码中捕捉这些假设。程序员相信在程序中的某个特定点该表达式值为真,可以在任何时候启用和禁用断言验证,因此可以在测试时启用断言而在部署时禁用断言。同样,程序投入运行后,最终用户在遇到问题时可以重新启用断言。

function fetchData(type) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (type === 200) {
        resolve('peanut butter');
      } else {
        reject('error')
      }
    }, 2000);
  });
}

test('doAsync calls both callbacks', () => {
  expect.assertions(2);

  const promise1 = fetchData(200);
  const promise2 = fetchData(200);
  
  return Promise.all([
    promise1.then((data) => {
      expect(data).toBe('peanut butter');
    }), 
    promise2.then((data)=> {
      expect(data).toBe('peanut butter');
    })
  ])
});

通过expect.assertions(2);确保promise1,promise2里的断言都被调用了,注释任一一个expect断言,都会提示缺少断言。

所以在测试异步代码时,该方法可以保证回调内的断言都被调用了。

describe

describe可以将测试分组,产生作用域,当 before 和 after 的块在 describe 块内部时,则其只适用于该 describe 块内的测试。before、after函数定义和用法见jest setup-teardown

describe 和 test 块的执行顺序

Jest 会在所有真正的测试开始之前执行测试文件里所有的 describe 处理程序(handlers)

当 describe 块运行完后,,默认情况下,Jest 会按照 test 出现的顺序(译者注:原文是in the order they were encountered in the collection phase)依次运行所有测试,等待每一个测试完成并整理好,然后才继续往下走。

示例:

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可以告诉jest当前测试文件中仅运行该用例

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

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

常见错误

1. Your test suite must contain at least one test

文件中没有测试脚本所导致的

Jest会自动找到项目中所有使用.spec.js或.test.js文件命名的测试文件并执行, 确保所有测试文件中都包含测试脚本

2. 使用import关键字报错

import overlap from './overlap.babel';
^^^^^^

SyntaxError: Cannot use import statement outside a module

  at Runtime._execModule (node_modules/jest-runtime/build/index.js:1179:56)

jest默认不支持es6语法,可以通过使用Babel来支持

参考资料

  1. Jest官方中文文档
  2. 使用Jest测试JavaScript (入门篇)
  3. 断言-百度百科