Jest学习笔记

2019-09-20

前端测试发展近况

对于前端来说,像后端一样写测试用例的同学并不多。毕竟前端应用是围绕浏览器进行应用开发的,多了UI这一层,输入和输出都是在UI上而不是像后端一样的纯数据,这造成了测试的问题。目前有调查称近半的前端开发者从来没有接触过编写前端测试用例。

针对这种情况,目前前端的测试应该分为单元测试和集成测试。

  • 单元测试:类似后端,测试某个代码片段,一般来说不带UI,检测其输入输出是否符合预期。这一类测试往往比较精确,运行速度也快。流行的有Jest、Mocha
  • 集成测试(UI测试):使用自动化UI测试工具,配合浏览器对实际UI进行测试,产生真实的UI事件、截图对比。因为涉及真实的UI,这一类测试可能天生带有失败率,运行速度也比较慢。流行的有Puppeteer、PhantomJS

同样为了满足测试条件,前端开发者应该尽量降低视图层和逻辑层的耦合,便于进行精准的单元测试,同时也是一种良好的设计模式。

今天看的Jest就是属于单元测试。

Jest

Jest是Facebook推出的Javscript单元测试框架,运行在浏览器和Node平台上。有以下特点:

  • 零配置开箱即用
  • 实时测试(其实就是改了测试文件自动跑一下)
  • 对象快照,便于监控对象变化

官方入门示例

安装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')

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

package.json

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

执行测试:

$ npm run test

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

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        1.08s
Ran all test suites.

这个入门示例虽然很短也很简单,但为我们展示了jest很重要的基本概念:

  • 测试用例可以由测试套件(test suite)在逻辑上分组,创建测试套件对应describe方法,创建测试用例对应test方法。
  • 单元测试由断言方法(assert)驱动,如例子中的expect方法和tobe相等断言。如果断言不报错,则认为测试通过。
  • 测试用例文件名符合*.test.js的形式,jest就会自动去找到项目下的测试用例文件去运行。也不需要显式用js引入测试相关方法。所以jest自称零配置。
  • 由npm指令jest去启动执行测试用例。在控制台打印出测试结果。

断言

和大部分单元测试框架一样,Jest单元测试是断言驱动的。一个精确的断言驱动,至少应该包括:

  • 测试执行的程序过程。
  • 程序过程的输入。
  • 程序过程的预期输出。
  • 断言是否成功的结果。

对我们最简单的Jest例子来说:

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

expect接受一个过程sum和过程的输入2,2tobe定义一个预期输出是当且仅当输出为4,断言是否成功的结果是执行是否抛出JS Error。所以Jest满足成为一个精确的断言工具的能力。

Jest提供的断言匹配方法有:

  • tobe,用Object.is来判断的“完全相等”
  • toEqual,用于对对象或数组递归比较判断相等
  • toBeTruthytoBeFalsy,用于判断真假值
  • 针对number类型的有:
    • toBeGreaterThan
    • toBeGreaterThanOrEqual
    • toBeLessThan
    • toBeLessThanOrEqual
  • 针对string类型的有:
    • toMatch 匹配一个正则规则
  • 针对可迭代类型(Array, Set, Map, 自定义迭代器)
    • toContain检查是否包含
  • 针对Exception的有:
    • toThrow

异步测试

JavaScript是一门天生充满异步的语言,JavaScript测试框架也就必须要有对异步的支持,否则只能算一个残废。

对于Jest来说,他不知道什么时候异步调用会结束,如果不做任何处理,当测试用例走完了Jest就认为测试结束了,显然是不适用于异步的。那么Jest需要知道有多少回调会被完成,或者有完成的标记。

回调函数callbacks

对于回调函数形式的异步。Jest提供了一个无参数的函数done()来标记测试完成。如果done()没有被调用,最终测试用例会被标记为失败。

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

  fetchData(callback);
});

Promise

如果是Promise类型的异步,同样也可以使用done参数来标记异步结束。或者也可以return这个Promise,测试框架会自动等待这个Promise变为Resolve或Reject

done形式:

function promisedSum(a, b) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(a + b);
    }, 2000);
  });
}

test("adds 1 + 2 to equal 3", (done) => {
  function cb(data) {
    expect(data).toBe(3);
    done()
  }
  promisedSum(1,2).then(cb)
});

return形式:

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

catch错误也是一样的处理形式:

function promisedReject() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      reject("err");
    }, 2000);
  });
}

test("adds 1 + 2 to equal 3", (done) => {
  function cb(data) {
    expect(data).toMatch('err');
    done()
  }
  promisedReject(1,2).catch(cb)
});

async

Jest还可以在测试用例中直接接收async函数语法,对于测试async函数或者是返回值为promise的函数更加好用,就不需要调用其他api了,推荐这种。

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

测试初始化(setup)和拆除(teardown)

有时候测试的单元是需要程序环境的,比如需要数据库、需要其他模块数据等。这时候就需要在单元测试启动前先初始化环境,在结束时拆除环境。

Jest提供beforeEachafterEach来做这两件事。

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

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

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

Mock函数

对于JS这种灵活的语言来说,经常会有把函数传来传去的做法。要测试函数被调用的情况,可以很大程度覆盖一些测试场景。

mock函数可以用来测试函数被调用时的参数、返回值、调用次数。用jest.fn来创建,也是一个实用的测试方法。

// 待测试的方法
function forEach(items, callback) {
  for (let index = 0; index < items.length; index++) {
    callback(items[index]);
  }
}

const mockCallback = jest.fn(x => 42 + x);
forEach([0, 1], mockCallback);

// 断言mock函数被调用了两次
expect(mockCallback.mock.calls.length).toBe(2);

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

// 断言mock函数第一次被调用的返回值的42
expect(mockCallback.mock.results[0].value).toBe(42);