测试框架Jest

2,993 阅读7分钟

本文所有资料请参考:github.com/antgod/reac…

测试框架Jest

1. 快速开始

安装jest

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 test,jest输入如下内容

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

你刚刚成功的编写了一个jest测试!

这个测试用例使用了expecttoBe进行两个值相同的测试。想要学习更多的关于jest测试,请参考 Using Matchers

从命令行运行

你也可以直接从命令行执行,可以输入一些有空的参数(npm install jest -g)。

Here's how to run Jest on files matching my-test, using config.json as a configuration file and display a native OS notification after the run:

jest my-test --notify --config=config.json

如果你想学习更多的命令行执行,请参考 Jest CLI Options

附加配置

使用 Babel:
安装babel-jestregenerator-runtime 包:

npm install --save-dev babel-jest regenerator-runtime

注意: 如果你使用npm 3或者npm 4,你不用指明安装regenerator-runtime

添加一份.babelrc文件到你的工程根目录,比如,如果你使用es6或者react.js需要使用babel-preset-es2015babel-preset-react预设:

{
  "presets": ["es2015", "react"]
}

这样你会使用es6与react所有指定的语法。

注意: 如果你使用更多的babel编译配置,请使用babel's env option,记住jest将会自动定义node_env作为测试。

使用webpack

jest可以实用在工程内使用webpack管理你的资产,样式和编辑。webpack 提供一些特别的功能相比于其他工具。更多资料请参考 webpack guide

2. 使用匹配

普通匹配

jest使用matchers让你通过不同的方法测试值。你需要熟记很多不同的matchers。这里只介绍最常用的matchers

最简单的测试值相等的是精确相等:

test('two plus two is four', () => {
  expect(2 + 2).toBe(4);
});

上段代码,expect(2 + 2)返回一个“期待”对象。除了调用matchers,关于期待对象你不需要做太多。在这段代码中,.toBe(4)是一个matcher。当jest运行时,将追踪所有失败的matchers,所以它可以精确的打印错误信息。

toBe使用===精确等于测试。如果你想深度测试对象相等,使用toEqual代替。

test('object assignment', () => {
  const data = {one: 1};
  data['two'] = 2;
  expect(data).toEqual({one: 1, two: 2});
});

toEqual 递归的查找每个字段对比是否相等。

你可以使用not去测试matcher的反面:

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

类型判断

有时候你需要判断undefined,null与false,但有时候你不需要明确的区分他们。jest包含工具明确的区分他们。

  • toBeNull matches only null
  • toBeUndefined matches only undefined
  • toBeDefined is the opposite of toBeUndefined
  • toBeTruthy matches anything that an if statement treats as true
  • toBeFalsy matches anything that an if statement treats as false

举个例子:

test('null', () => {
  const n = null;
  expect(n).toBeNull();
  expect(n).toBeDefined();
  expect(n).not.toBeUndefined();
  expect(n).not.toBeTruthy();
  expect(n).toBeFalsy();
});

test('zero', () => {
  const z = 0;
  expect(z).not.toBeNull();
  expect(z).toBeDefined();
  expect(z).not.toBeUndefined();
  expect(z).not.toBeTruthy();
  expect(z).toBeFalsy();
});

你可以使用这些matcher做精确的匹配。

数字

多种途径比较数字

test('two plus two', () => {
  const value = 2 + 2;
  expect(value).toBeGreaterThan(3);
  expect(value).toBeGreaterThanOrEqual(3.5);
  expect(value).toBeLessThan(5);
  expect(value).toBeLessThanOrEqual(4.5);

  // toBe and toEqual are equivalent for numbers
  expect(value).toBe(4);
  expect(value).toEqual(4);
});

你可以使用toBeCloseTo进行浮点比较

test('adding floating point numbers', () => {
  const value = 0.1 + 0.2;
  expect(value).not.toBe(0.3);    // 浮点数不会直接相等
  expect(value).toBeCloseTo(0.3); // 使用closeTo方法进行浮点数字比较
});

字符串

你可以使用toMatch测试正则表达式来验证string字符串

test('there is no I in team', () => {
  expect('team').not.toMatch(/I/);
});

test('but there is a "stop" in Christoph', () => {
  expect('Christoph').toMatch(/stop/);
});

数组

你可以检验数组是否包含某一个特别项

const shoppingList = [
  'diapers',
  'kleenex',
  'trash bags',
  'paper towels',
  'beer',
];

test('the shopping list has beer on it', () => {
  expect(shoppingList).toContain('beer');
});

表达式

你可以使用toThrow来检验函数是否抛出异常

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

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

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

更多

这是一个just尝试,查看完整的matchers列表renerence docs

一旦你掌握了一个可用的matcher,建议下一步学习jest如何检验异步代码

3.测试异步代码

js运行异步代码是很普遍的。jest需要知道什么时候代码测试已完成,才能移动到下一个测试。jest有几种方法处理。

Callbacks

最普遍的异步是通过回调函数。

比如,如果你调用fetchData(callback)函数去拉取异步数据并且结束时候调用callback(data)。你要测试的数据是否等于peanut buter

默认情况下,Jest走完测试代码就完成测试。这意味着测试不能按期进行。

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

  fetchData(callback);
});

问题在于测试工作将在fetchData结束的时候,也就是在回调函数之前结束。

为了解决这个问题,这是另一种形式。使用参数done来代替无参函数。Jest将会延迟测试直到done回调函数执行完毕。

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

  fetchData(callback);
});

如果done一直没有调用,测试将会失败。

Promises

如果你使用promise,有一种简单的方法处理异步测试。你的测试代码中Jest返回一个Promise,并且等待Promiseresolve。如果Promiserejected,测试自动失败。

比如,还是那个fetchData,这次使用了回调,返回一个promise假定reslove一个字符串peanut butter。测试代码如下:

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

确保返回一个Promise-如果你省略了return,你的测试代码将会在fetchData之前结束。

你也可以使用resolves关键字在你的expect代码后,然后Jest将会把等待状态转换成resolve。如果promiserejected,测试自动失败。

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

Async/Await

如果你使用async/await,你可以完美嵌入测试。写一个async测试,仅使用async关键字在你的matchers函数前面就能通过测试。比如,还是fetchData这个测试方案可以写成

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

在这种情况下,async/await仅仅是个有效的promises样例的逻辑语法糖。

这些表单特别优秀,你可以混合使用这些在你的代码库或者单个文件中,它仅仅使你的测试变得更加简单。

安装与卸载

写测试的过程中,在你运行测试之前,你需要做一些初始化工作,去做一些需要测试的事情之前,并且你需要做一些结束工作,去做一些测试结束的事情。Jest提供了helper函数去处理这些工作。

多测试重复初始化

如果你有很多测试有重复的工作,你可以是使用beforeEachafterEach

比如你有很多测试运行之前需要调用initializeCityDatabase(),而且测试结束 后需要调用clearCityDatabase()。你可以这么做:

beforeEach(() => {
  initializeCityDatabase();
});

afterEach(() => {
  clearCityDatabase();
});

test('city database has Vienna', () => {
  expect(isCity('Vienna')).toBeTruthy();
});

test('city database has San Juan', () => {
  expect(isCity('San Juan')).toBeTruthy();
});

同样的,如果你的initializeCityDatabase函数返回一个promise,你可以使用return返回这个函数。

beforeEach(() => {
  return initializeCityDatabase();
});

一次性设置

如果你有很多测试有共同的重复的工作,并且重复的工作只在测试开始与测试结束的地方运行一次,你可以是使用beforeAllbeforeAll

比如你有一个测试开始之前要调用initializeCityDatabase(),而且测试结束 后需要调用clearCityDatabase()。你可以这么做:

beforeAll(() => {
  return initializeCityDatabase();
});

afterAll(() => {
  return clearCityDatabase();
});

test('city database has Vienna', () => {
  expect(isCity('Vienna')).toBeTruthy();
});

test('city database has San Juan', () => {
  expect(isCity('San Juan')).toBeTruthy();
});

范围

默认情况下,beforeafter会对文件的每个测试生效。如果你只想对某些测试生效,你可以使用describe块。 beforeafter仅仅会在声明块中运行。

比如,我们不仅需要城市初始化,还需要食物初始化,我们可以对不同的测试做不同的初始化。

// Applies to all tests in this file
beforeEach(() => {
  return initializeCityDatabase();
});

test('city database has Vienna', () => {
  expect(isCity('Vienna')).toBeTruthy();
});

test('city database has San Juan', () => {
  expect(isCity('San Juan')).toBeTruthy();
});

describe('matching cities to foods', () => {
  // Applies only to tests in this describe block
  beforeEach(() => {
    return initializeFoodDatabase();
  });

  test('Vienna <3 sausage', () => {
    expect(isValidCityFoodPair('Vienna', 'Wiener Schnitzel')).toBe(true);
  });

  test('San Juan <3 plantains', () => {
    expect(isValidCityFoodPair('San Juan', 'Mofongo')).toBe(true);
  });
});

建议

如果你仅仅想测试一个测试用例,或者跨过某个测试用例,你可以使用fitxit

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

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

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