JEST入门笔记

170 阅读6分钟

写在前面

  • 也许前端学了很久也不会接触到jest
    • 比如我就是。。。
  • 面对新事物,内心还是会很害怕的
  • 今天,我终于鼓起勇气,学习了jest
    • 希望通过学习,以后能在开源项目提一些单元测试的PR
    • 然后,大家也要鼓起勇气,jest没有那么难

快速开始

安装

  • 初始化package.jsonnpm init
  • 安装jest
# npm
npm install --save-dev jest
# yarn
yarn add --dev jest

完成首个jest测试

  • 1.先写一个两数相加的函数,创建sum.js文件:
function sum(a, b) {
    return a + b;
}
module.exports = sum;
  • 2.创建sum.test.js测试文件
    • test函数第一个参数是测试名字
    • test函数第二个参数是一个函数
      • expect表示测试执行
      • toBe表示结果应该是多少
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 test
# or
npm run test

image.png

匹配器的使用

  • 我们在前面看到了测试用了expecttoBe函数就完成了测试
  • 但是这两个函数并不能覆盖所有的测试情况
  • 下面我们认识一些其他的匹配器吧
    • 匹配器很多,也只能介绍一些常用的

常用的匹配器

  • toBe一般用来精确匹配
  • toEqual用来匹配对象
test('对象赋值', () => {
    const data = {one: 1};
    data['two'] = 2;
    expect(data).toEqual({one: 1, two: 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);
    }
  }
});

真值

  • 代码中的undefinednullfalse有不同的含义,你可以使用真值判断来搞定它们
  • toBeNull只匹配null
  • toBeUndefined只匹配undefined
  • toBeDefinedtoBeUndefined相反
  • toBeTruthy匹配任何if结果为真
  • toBeFalsy匹配任何if结果为假
test('null', () => {
    const n = null;
    expect(n).toBeNull();
    expect(n).toBeDefined();
    expect(n).not.toBeUndefined();
    expect(n).not.toBeTruthy();
    expect(n).toBeFalsy();
});

数字

  • 对于数字,我们也有对应的匹配器
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

字符串

  • 可以检查是否具有正则表达式的字符串expect('Christoph').toMatch(/stop/);

数组和可迭代对象

  • 可以通过toContain来检查一个数组或可迭代对象是否包含某个特定项
const shoppingList = [
  'diapers',
  'kleenex',
  'trash bags',
  'paper towels',
  'milk',
];

test('shoppingList数组中包含milk', () => {
  expect(shoppingList).toContain('milk');
  expect(new Set(shoppingList)).toContain('milk');
});

错误抛出

  • 如果想测试某函数在调用时是否抛出了错误,你需要使用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);

  // 你可以自己定义确切的错误消息内容或者使用正则表达式
  expect(() => compileAndroidCode()).toThrow('you are using the wrong JDK');
  expect(() => compileAndroidCode()).toThrow(/JDK/);
});
  • 注意:抛出错误的函数需要在expect的包装函数中调用,否则 toThrow断言总是会失败(不会捕获到前面的错误)。

测试异步代码

  • 假设fetchData()返回的是一个promise对象

Promise

  • 为你的测试返回一个Promise,则Jest会等待Promise的resove状态 If the promise is rejected, the test will fail.
test('the data is peanut butter', () => {
  return fetchData().then(data => {
    expect(data).toBe('peanut butter');
  });
});

Async/Await

  • 您可以在测试中使用 async 和 await。 写异步测试用例时,可以在传递给test的函数前面加上async
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');
  }
});
  • 也可以将asyncawait.resolves.rejects一起使用
test('the data is peanut butter', async () => {
    await expect(fetchData()).resolves.toBe('peanut butter');
});
test('the fetch fails with an error', async () => {
    await expect(fetchData()).rejects.toMatch('error');
});

安装和移除

  • 写测试的时候你经常需要在运行测试前做一些准备工作,和在运行测试后进行一些整理工作

Repeating Setup

  • 比如我们要进行一些数据库的交互,在每个测试之前调用方法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();
});

一次性设置

  • 同样的,我们可以一次性设置所有的,jest提供了对应的api-beforeAllafterAll

作用域

  • 当然我们可能希望这个设置只对某几个测试有效

  • 我们就可以使用作用域

    • describe
  • 比如说,我们不仅有一个城市的数据库,还有一个食品数据库。 我们可以为不同的测试做不同的设置︰

// 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 veal', () => {
    expect(isValidCityFoodPair('Vienna', 'Wiener Schnitzel')).toBe(true);
  });

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

模拟函数

  • Mock 函数允许你测试代码之间的连接——实现方式包括:擦除函数的实际实现、捕获对函数的调用 ( 以及在这些调用中传递的参数) 、在使用 new 实例化时捕获构造函数的实例、允许测试时配置返回值。
const mockCallback = jest.fn(x => 42 + x);
forEach([0, 1], mockCallback);

// 此 mock 函数被调用了两次
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);

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

组件测试实战准备

  • 其实要测试(比如react组件),其实主要分为两类吧
    • 快照测试
    • dom操作测试
  • 下面就这两点,我们来详细看一看

快照测试

  • 在测试 React 组件时可以采用类似的方法。您可以使用测试渲染器为您的 React 树快速生成可序列化的值,而不是渲染需要构建整个应用程序的图形 UI。
import renderer from 'react-test-renderer';
import Link from '../Link';

it('renders correctly', () => {
  const tree = renderer
    .create(<Link page="http://www.facebook.com">Facebook</Link>)
    .toJSON();
  expect(tree).toMatchSnapshot();
});

属性匹配器

  • Jest 允许为任何属性提供不对称匹配器。在写入或测试快照之前检查这些匹配器,然后将其保存到快照文件而不是接收到的值:
it('will check the matchers and pass', () => {
  const user = {
    createdAt: new Date(),
    id: Math.floor(Math.random() * 20),
    name: 'LeBron James',
  };

  expect(user).toMatchSnapshot({
    createdAt: expect.any(Date),
    id: expect.any(Number),
  });
});

// Snapshot
exports[`will check the matchers and pass 1`] = `
Object {
  "createdAt": Any<Date>,
  "id": Any<Number>,
  "name": "LeBron James",
}
`;

DOM操作

  • 另一类通常被认为难以测试的函数是直接操作 DOM 的代码。让我们看看我们如何测试以下 jQuery 代码片段,它监听点击事件,异步获取一些数据并设置 span 的内容。
  • displayUser.js
'use strict';

const $ = require('jquery');
const fetchCurrentUser = require('./fetchCurrentUser.js');

$('#button').click(() => {
  fetchCurrentUser(user => {
    const loggedText = 'Logged ' + (user.loggedIn ? 'In' : 'Out');
    $('#username').text(user.fullName + ' - ' + loggedText);
  });
});

  • displayUser-test.js
'use strict';

jest.mock('../fetchCurrentUser');

test('displays a user after a click', () => {
  // Set up our document body
  document.body.innerHTML =
    '<div>' +
    '  <span id="username" />' +
    '  <button id="button" />' +
    '</div>';

  // This module has a side-effect
  require('../displayUser');

  const $ = require('jquery');
  const fetchCurrentUser = require('../fetchCurrentUser');

  // Tell the fetchCurrentUser mock function to automatically invoke
  // its callback with some data
  fetchCurrentUser.mockImplementation(cb => {
    cb({
      fullName: 'Johnny Cash',
      loggedIn: true,
    });
  });

  // Use jquery to emulate a click on our button
  $('#button').click();

  // Assert that the fetchCurrentUser function was called, and that the
  // #username span's inner text was updated as we'd expect it to.
  expect(fetchCurrentUser).toBeCalled();
  expect($('#username').text()).toEqual('Johnny Cash - Logged In');
});