Skip to content

Latest commit

 

History

History
483 lines (403 loc) · 17.5 KB

File metadata and controls

483 lines (403 loc) · 17.5 KB

Jest测试框架 - 基础篇

1 简介

零配置测试平台
Jest 被 Facebook 用来测试包括 React 应用在内的所有 JavaScript 代码。Jest 的一个理念是提供一套完整集成的 “零配置” 测试体验。

高速和沙盒
Jest 跨工人以最大化性能并行化的测试运行。控制台消息都是缓冲并输出测试结果。沙盒测试文件和自动全局状态将为每个测试重置,因此测试代码间不会冲突。

内置代码覆盖率报告
使用——coverage轻松创建代码覆盖率报告。不需要额外的设置或库!Jest可以从整个项目收集代码覆盖信息,包括未测试的文件。

无需配置
在你使用 create-react-app 或 react-native init 创建你的 React 或 React Native 项目时,Jest 都已经被配置好并可以使用了。在 __tests__文件夹下放置你的测试用例,或者使用 .spec.js 或 .test.js 后缀给它们命名。不管你选哪一种方式,Jest 都能找到并且运行它们。

2 开始

安装
yarn add --dev jest
或者 npm install --save-dev jest

开始编写第一个测试用例
在项目 Test 目录里面创建一个 math.js 的文件:

export function sqrt(x) {
    if (x < 0) throw new Error("负值没有平方根");
    return Math.exp(Math.log(x)/2);
}

然后同级目录下面创建 math.test.js 的文件:

import {sqrt} from './math';
describe('main', function () {
    test('4的平方根等于2', function () {
        expect(sqrt(4)).toBe(2)
    });
});

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

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

最后,运行 npm run test, Jest将打印以下消息

 PASS  Test/math.test.js
  main
    √ 4的平方根等于2 (3ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        2.031s

3 匹配器

匹配器就类似于断言语句,接受了期望函数之后, 就可以书写对期望的匹配了。

常用匹配器有如下:

expect({a:1}).toBe({a:1})//判断两个对象是否相等
expect(n).toEqual();//同上toBe()
expect(1).not.toBe(2)//判断不等

expect(n).toBeNull(); //判断是否为null
expect(n).toBeUndefined(); //判断是否为undefined
expect(n).toBeDefined(); //判断结果与toBeUndefined相反

expect(n).toBeTruthy(); //判断结果为true
expect(n).toBeFalsy(); //判断结果为false

expect(value).toBeGreaterThan(3); //大于3
expect(value).toBeGreaterThanOrEqual(3.5); //大于等于3.5
expect(value).toBeLessThan(5); //小于5
expect(value).toBeLessThanOrEqual(4.5); //小于等于4.5
expect(value).toBeCloseTo(0.3); // 浮点数判断相等

expect('Christoph').toMatch(/stop/); //正则表达式判断
expect(['one','two']).toContain('one'); //不解释

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

test('compiling android goes as expected', () => {
  expect(compileAndroidCode).toThrow();
  expect(compileAndroidCode).toThrow(ConfigError); //判断抛出异常
})

4 测试异步代码

异步测试的解决方式其实跟mocha 的解决方案是一致的。 使用单个参数调用 done,而不是将测试放在一个空参数的函数。 Jest会等done回调函数执行结束后,结束测试。
来举一个例子:

test('async function run err', function () {
   let x = true;
   let f = function() {
       x = false;
       expect(x).toBeTruthy();
   };
   setTimeout(f, 4000);
})

上面这个例子将会永远成功, 因为我们异步执行的时候, 还没有执行到f方法, 测试用例已经执行完毕了, 也就是说, 测试执行完毕的时候, 压根就还没执行到f方法。

当我们加上done()之后, 将会成为另外一件事儿了, done()之后, 是一定要执行了done()才算是整个test单元执行完毕, 否则就会永远挂起。 这个地方简介也引出了一个需要注意的问题, 就是如果有了test测试单元回调函数有了done参数, done(), 就一定要执行, 否则测试就会挂起。

test('async function run success', function (done) {
    let x = true;
    let f = function() {
        x = false;
        expect(x).toBeTruthy();
        done();
    };
    setTimeout(f, 4000);
})

上面示例将会抛错, 原因就是执行了异步函数方法 f, 然后将 x改为false了, 然后匹配器 匹配的确实true, 所以代码报错。

Promise
Jest是内置Promise的测试的, 测试返回一个 Promise, Jest 会等待这一 Promise 来解决。 但是有几个问题需要注意:

  • 如果承诺被拒绝,则测试将自动失败 (一定要有return)
test('Promise testing resolve', function () {
    let timer = +new Date();
    let flag = timer % 2 === 1 ? true : false;
    let returnPromise = function () {
        return new Promise((resolve, reject) => {
            if (flag) {
                resolve(flag)
            } else {
                reject(flag)
            }
        });
    };

    return returnPromise()
        .then(flag => {
            expect(flag).toBeFalsy();
        })
})
  • 如果你想要 Promise 被拒绝,使用 .catch 方法。 请确保添加 expect.assertions 来验证一定数量的断言被调用。
test('Promise testing reject', function () {
    let timer = +new Date();
    let flag = timer % 2 === 1 ? true : false;
    let returnPromise = function () {
        return new Promise((resolve, reject) => {
            if (flag) {
                resolve(flag)
            } else {
                reject(flag)
            }
        });
    };

    return returnPromise()
        .catch(flag => {
            expect.assertions(1);
            expect(flag).tobeFalsy();
        })
});

如果不加 expect.assertions(1); 下面的断言 expect(flag).tobeFalsy(); 是不成立的。

使用.resolves/.rejects 的Promise测试 可以直接使用 .resolves/.rejects 来匹配自己想要的Promise状态, 而且Jest也会等待 Promise的状态返回。

test('Promise testing used .resolves and .rejects', function () {
    let timer = +new Date();
    let flag = timer % 2 === 1 ? true : false;
    let returnPromise = function () {
        return new Promise((resolve, reject) => {
            if (flag) {
                resolve(flag)
            } else {
                reject(flag)
            }
        });
    };
    return expect(returnPromise()).resolves.toBeTruthy();
});

如果是 catch 里面去了, 也会直接报错, 如果是成功的返回, 就会走断言的判断结果。
同理, 如果是判断rejected 状态, 直接用 .rejectes 就可以了。

async/await:
Jest同样是支持 async/await 的, 写法可以参考下面

test('async/await testing', async function () {
    let timer = +new Date();
    let flag = timer % 2 === 1 ? true : false;
    let returnPromise = function () {
        return new Promise((resolve, reject) => {
            if (flag) {
                resolve(flag)
            } else {
                reject(flag)
            }
        });
    };
    let result = await returnPromise();
    expect(result).toBeTruthy();
})

对于对于Promise场景, 也可以直接使用async/await

test('Promise testing used .resolves and .rejects', async function () {
    let timer = +new Date();
    let flag = timer % 2 === 1 ? true : false;
    let returnPromise = function () {
        return new Promise((resolve, reject) => {
            if (flag) {
                resolve(flag)
            } else {
                reject(flag)
            }
        });
    };
    await expect(returnPromise()).resolves.toBeTruthy();
});

5 Setup and Teardown

写测试的时候你经常需要在运行测试前做一些准备工作,和在运行测试后进行一些整理工作。 Jest 提供辅助函数来处理这个问题。

为多次测试重复设置
使用 beforeEachafterEach; 分别表示每次test执行之前和执行之后, 都要执行的函数示例, 这两个作为设置函数, 接受一个回调函数作为参数。
其内部同样也可以使用异步函数, 但是要记得return; 例如我们有下面这样的示例例子:

describe('Setup and Teardown', function () {
    beforeEach(function () {
        console.log('beforeEach')
    });

    afterEach(function () {
        console.log('afterEach')
    });

    test('match string one', function () {
        expect('yanlele').toMatch('yan');
    });

    test('match string two', function () {
        expect('yanlele').toMatch('lele');
    })
});

输出的日志结果如下:

  Setup and Teardown
  console.log Test/01、基础部分/03、setupAndTeardown.test.js:8
    beforeEach

  console.log Test/01、基础部分/03、setupAndTeardown.test.js:12
    afterEach

  console.log Test/01、基础部分/03、setupAndTeardown.test.js:8
    beforeEach

  console.log Test/01、基础部分/03、setupAndTeardown.test.js:12
    afterEach

    ✓ match string one (78ms)
    ✓ match string two (3ms)

一次性设置
有的情况是只需要在一个test文件中所有测试用例或者用例集执行之前或者之后执行, 而且只执行一次,这个时候就要用到 beforeAllafterAll
用法与上面的beforeEachafterEach是一样的, 这里就不做演示了。

只执行一个测试关键字only 如果只希望执行一个测试用例, 那么直接使用test.only('Description', Function); 就可以了, 这个地方跟mocha是一模一样的。

6 Mock Function

Jest 中的三个与 Mock 函数相关的API,分别是jest.fn()、jest.spyOn()、jest.mock()。 使用它们创建Mock函数能够帮助我们更好的测试项目中一些逻辑较复杂的代码,例如测试函数的嵌套调用,回调函数的调用等。

Mock函数提供的以下三种特性,在我们写测试代码时十分有用:

  • 捕获函数调用情况
  • 设置函数返回值
  • 改变函数的内部实现

jest.fn()

jest.fn()是�创建Mock函数最简单的方式,如果没有定义函数内部的实现,jest.fn()会返回undefined作为返回值。

test('测试jest.fn()调用', () => {
    let mockFn = jest.fn();
    mockFn(1, 2, 3);
    mockFn(1, 2, 3);
    let result = mockFn(1, 2, 3);

    // 断言mockFn的执行后返回undefined
    expect(result).toBeUndefined();
    // 断言mockFn被调用
    expect(mockFn).toBeCalled();
    // 断言mockFn被调用了一次
    expect(mockFn).toBeCalledTimes(3);
    // 断言mockFn传入的参数为1, 2, 3
    expect(mockFn).toHaveBeenCalledWith(1, 2, 3);
});

jest.fn()所创建的Mock函数还可以设置返回值,定义内部实现或返回Promise对象。

test('测试jest.fn()返回固定值', () => {
    let mockFn = jest.fn().mockReturnValue('default');
    // 断言mockFn执行后返回值为default
    expect(mockFn()).toBe('default');
});

test('测试�jest.fn()内部实现', () => {
    let mockFn = jest.fn((num1, num2) => {
        return num1 * num2;
    });
    // 断言mockFn执行后返回100
    expect(mockFn(10, 10)).toBe(100);
});

test('测试jest.fn()返回Promise', async () => {
    let mockFn = jest.fn().mockResolvedValue('default');
    let result = await mockFn();
    // 断言mockFn通过await关键字执行后返回值为default
    expect(result).toBe('default');
    // 断言mockFn调用后返回的是Promise对象
    expect(Object.prototype.toString.call(mockFn())).toBe("[object Promise]");
});

上面的代码是jest.fn()提供的几个常用的API和断言语句,下面我们在src/fetch.js文件中写一些被测试代码, 以更加接近业务的方式来理解Mock函数的实际应用。
被测试代码中依赖了axios这个常用的请求库和JSONPlaceholder这个上篇文章中提到免费的请求接口��, 请先在shell中执行npm install axios --save安装依赖。

// fetch.js
import axios from 'axios';

export default {
  async fetchPostsList(callback) {
    return axios.get('https://jsonplaceholder.typicode.com/posts').then(res => {
      return callback(res.data);
    })
  }
}

我们在fetch.js中封装了一个fetchPostsList方法,该方法请求了JSONPlaceholder提供的接口, 并通过传入的回调函数返回处理过的返回值。如果我们想测试该接口能够被正常请求, 只需要捕获到传入的回调函数能够被正常的调用即可。下面是functions.test.js中的测试的代码。

import fetch from '../src/fetch.js'

test('fetchPostsList中的回调函数应该能够被调用', async () => {
  expect.assertions(1);
  let mockFn = jest.fn();
  await fetch.fetchPostsList(mockFn);

  // 断言mockFn被调用
  expect(mockFn).toBeCalled();
});

jest.mock();

fetch.js文件夹中封装的请求方法可能我们在其他模块被调用的时候,并不需要进行实际的请求(请求方法已经通过单侧或需要该方法返回非真实数据)。 此时,使用jest.mock()去mock整个模块是十分有必要的。

下面我们在util/fetch.js的同级目录下创建一个util/events.js。

import fetch from './fetch'

export default {
    async getPostList() {
        return fetch.fetchPostsList(data => {
            console.log('fetchPostsList be called');
        })
    }
}

在 05、jest.mock.test.js 写下如下测试代码

import events from "../util/events";
import fetch from "../util/fetch";

jest.mock('../util/fetch.js');

test('mock 整个 fetch.js模块', async () => {
    expect.assertions(2);
    await events.getPostList();
    expect(fetch.fetchPostsList).toHaveBeenCalled();
    expect(fetch.fetchPostsList).toHaveBeenCalledTimes(1);
}, 10000);

这个地方需要注意的是, jest.mock() 要写在外层; 测试代码中我们使用了jest.mock('../src/fetch.js')去mock整个fetch.js模块。如果注释掉这行代码,执行测试脚本时会出现以下报错信息

Error: expect(jest.fn())[.not].toHaveBeenCalled()

jest.fn() value must be a mock function or spy.
Received:
  function: [Function fetchPostsList]

      55 |         expect.assertions(2);
      56 |         await events.getPostList();
    > 57 |         expect(fetch.fetchPostsList).toHaveBeenCalled();
         |                                      ^
      58 |         expect(fetch.fetchPostsList).toHaveBeenCalledTimes(1);
      59 |     }, 10000);
      60 | });

从这个报错中,我们可以总结出一个重要的结论:
在jest中如果想捕获函数的调用情况,则该函数必须被mock或者spy!

jest.spyOn()

jest.spyOn()方法同样创建一个mock函数,但是该mock函数不仅能够捕获函数的调用情况,还可以正常的执行被spy的函数。 实际上,jest.spyOn()是jest.fn()的语法糖,它创建了一个和被spy的函数具有相同内部代码的mock函数。
img01

上图是之前jest.mock()的示例代码中的正确执行结果的截图, 从shell脚本中可以看到console.log('fetchPostsList be called!'); 这行代码并没有在shell中被打印,这是因为通过jest.mock()后,模块内的方法是不会被jest所实际执行的。 这时我们就需要使用jest.spyOn()。

it('使用jest.spyOn()监控fetch.fetchPostsList被正常调用', async () => {
    expect.assertions(2);
    const spyFn = jest.spyOn(fetch, 'fetchPostsList');
    await events.getPostList();
    expect(spyFn).toHaveBeenCalled();
    expect(spyFn).toHaveBeenCalledTimes(1);
})

总结

这篇文章中我们介绍了jest.fn(),jest.mock()和jest.spyOn()来创建mock函数, 通过mock函数我们可以通过以下三个特性去更好的编写我们的测试代码:

  • 捕获函数调用情况
  • 设置函数返回值
  • 改变函数的内部实现 在实际项目的单元测试中,jest.fn()常被用来进行某些有回调函数的测试; jest.mock()可以mock整个模块中的方法,当某个模块已经被单元测试100%覆盖时,使用jest.mock()去mock该模块,节约测试时间和测试的冗余度是十分必要; 当需要测试某些必须被完整执行的方法时,常常需要使用jest.spyOn()。这些都需要开发者根据实际的业务代码灵活选择。

参考文章