使用Jest做单元测试

尉迟龙光
2023-12-01

Jest是什么?

Jest is a delightful JavaScript Testing Framework with a focus on simplicity.
It works with projects using: Babel, TypeScript, Node, React, Angular, Vue and more!
Jest是一个关注于简单使用且令人愉悦的JavaScript测试框架,它能够和Babel、TypeScript、Node、React、Angular、Vue等项目配合使用;

起步

  1. 安装jest
npm i -D jest
  1. 通过script开启单元测试
{
	"scripts": { "test": "jest" }
}
  1. 使用TypeScript和Babel
npm i -D @babel/preset-env @babel/core @babel/preset-typescript npm i -D babel-jest
  1. 配置文件

配置babel.config.js:react和typescript,配置env当process.env.NODE_ENV是test时对.less、.sass、.styl进行忽略避免在jest测试时对样式文件作出响应。

// babel.config.js
module.exports = {
  presets: [
    '@babel/preset-env',
    '@babel/preset-typescript',
    '@babel/preset-react'
  ],
  env: {
	test: {
		plugins: ['babel-plugin-transform-require-ignore', {
			extensions: ['.less', '.sass', '.styl']
		}]
	}	
  }
};

配置jest.config.js

module.exports = {
    rootDir: './test/', // 测试目录
    // 对jsx、tsx、js、ts文件采用babel-jest进行转换
    transform: {
        '^.+\\.[t|j]sx?$': 'babel-jest',
    },
    testRegex: '(/__tests__/.*|(\\.|/)(test|spec))\\.[jt]s?$',
    moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
    collectCoverage: true, // 统计覆盖率
    testEnvironment: 'jsdom', // 测试环境,默认为”jsdom“
    collectCoverageFrom: ['**/*.ts'],
    coverageDirectory: './coverage', // 测试覆盖率的文档
    globals: { 
        window: {}, // 设置全局对象window
    },
    setupFiles: ['<rootDir>/setupFiles/shim.js'], 
    // 测试前执行的文件,主要可以补齐模拟一些在node环境下的方法但又window下需要使用
};

// shim.js
// 在测试环境下模拟requestAnimationFrame函数
global.window.requestAnimationFrame = function (cb) {
    return setTimeout(() => {
        cb();
    }, 0);
};

编写测试用例

使用Matchers编写测试用例,例如:

test('测试用例名称', () => {
	expect(2 + 2).toBe(4);
	expect(2 + 2).not.toBe(3);
	expect({ one: 1, two: 2 }).toEqual({ one: 1, two: 2 });
	expect(null).toBeNull();
	expect(null).toBeUndefined();
	expect(null).toBeTruthy();
	expect(null).toBeFalsy();
})

toBe使用的是Object.is所以可以用来测试非引用变量,当要测试Object时应该使用toEqual,这个方法会递归的进行比较。
toBeNull、toBeUndefined、toBeTruthy、ToBeFalsy是测试一些边界情况使用的API
下面是关于数字类型的API,语义非常明显不再需要解释

test('测试类型的API', () => {
  const value = 2 + 2;
  expect(value).toBeGreaterThan(3);
  expect(value).toBeGreaterThanOrEqual(3.5);
  expect(value).toBeLessThan(5);
  expect(value).toBeLessThanOrEqual(4.5);
});

更多类型的matcher将写在附录中,尽情参考

测试异步代码

默认的jest代码是一下执行到最后的,所以通常下面的代码是无法被正确测试的

// 不会生效
test('the data is peanut butter', () => {
  function callback(data) {
    expect(data).toBe('peanut butter');
  }
  fetchData(callback);
});

而下面的才能生效,通过done告诉jest是否测试完毕

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

同样的也是支持promise的测试

test('the data is peanut butter', () => {
  return fetchData().then(data => {
    expect(data).toBe('peanut butter');
  });
});
// resolve和reject两种情况进行测试
test('the data is peanut butter', () => {
  return expect(fetchData()).resolves.toBe('peanut butter');
});
test('the fetch fails with an error', () => {
  return expect(fetchData()).rejects.toMatch('error');
});
// 或者结合async和await
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.toThrow('error');
});

假如在测试代码前需要做一些通用操作

那么可以使用beforeEachafterEachbeforeAllafterAlldescribe通过Each在每个test前后进行执行,通过All在当前文件的所有test前后进行执行
,由describe包裹起来的部分,将形成一个scope使得All和Each只当前scope内会生效。示例如下:

beforeAll(() => console.log('1 - beforeAll'));
afterAll(() => console.log('1 - afterAll'));
beforeEach(() => console.log('1 - beforeEach'));
afterEach(() => console.log('1 - afterEach'));
test('', () => console.log('1 - test'));
describe('Scoped / Nested block', () => {
  beforeAll(() => console.log('2 - beforeAll'));
  afterAll(() => console.log('2 - afterAll'));
  beforeEach(() => console.log('2 - beforeEach'));
  afterEach(() => console.log('2 - afterEach'));
  test('', () => console.log('2 - test'));
});
// 1 - beforeAll
// 1 - beforeEach
// 1 - test
// 1 - afterEach
// 2 - beforeAll
// 1 - beforeEach
// 2 - beforeEach
// 2 - test
// 2 - afterEach
// 1 - afterEach
// 2 - afterAll
// 1 - afterAll

mock函数

jest.fn()能够创建一个mock函数,它的.mock属性保存了函数被调用的信息,还追踪了每次调用的this值。

const mockCallback = jest.fn();
forEach([0, 1], mockCallback);
test('该模拟函数被调用了两次', () => {
    // 此模拟函数被调用了两次
    expect(mockCallback.mock.calls.length).toBe(2);
})
test('第一次调用函数时的第一个参数是0', () => {
    // 第一次调用函数时的第一个参数是 0
    expect(mockCallback.mock.calls[0][0]).toBe(0);
})
test('第二次调用函数时的第一次参数是1', () => {
    // 第二次调用函数时的第一个参数是 1
    expect(mockCallback.mock.calls[1][0]).toBe(1);
})

// 表示被调用的次数
// The function was called exactly once
expect(someMockFunction.mock.calls.length).toBe(1);
// [n]表示第几次调用[m]表示第几个参数
// The first arg of the first call to the function was 'first arg'
expect(someMockFunction.mock.calls[0][0]).toBe('first arg');
// The second arg of the first call to the function was 'second arg'
expect(someMockFunction.mock.calls[0][1]).toBe('second arg');

// The return value of the first call to the function was 'return value'
expect(someMockFunction.mock.results[0].value).toBe('return value');

// 被实例化两次
// This function was instantiated exactly twice
expect(someMockFunction.mock.instances.length).toBe(2);
// 第一次实例化返回的对象有一个name属性并且值是test
// The object returned by the first instantiation of this function
// had a `name` property whose value was set to 'test'
expect(someMockFunction.mock.instances[0].name).toEqual('test');

实际应用

mock的实际应用

上述对于mock的简单说明其实看过一遍是很懵的无法直到它的实际用途,那么下面我将列举一个我在开发中使用jest.fn()模拟函数来对方法进行单元测试的例子:
首先我们有一个功能,目的是能使页面滑动到某一个位置,最顶部或者是中间或者是底部,这个函数通常都被用于展示在页面上的回到顶部按钮。
函数类似下面这样

export const scrollTo: (y?: number, option?: { immediately: boolean }) => void = (
    y = 0,
    option = { immediately: false }
) => {
    if (option.immediately) {
        window.scrollTo(0, y);
        return;
    }
    const top = document.body.scrollTop || document.documentElement.scrollTop;
    const clientHeight = document.body.clientHeight || document.documentElement.clientHeight;
    const scrollHeight = document.body.scrollHeight || document.documentElement.scrollHeight;
    if (top === y) {
        return;
    }
    if (y > scrollHeight - clientHeight && y <= scrollHeight) {
        y = scrollHeight - clientHeight;
    }
    if (Math.abs(top - y) > 1) {
        let movDistance = Math.ceil((y - top) / 8);
        let nextDestination = top + movDistance;
        if (Math.abs(top - y) < 8) {
            nextDestination = y;
        }
        window.requestAnimationFrame(scrollTo.bind(this, y));
        window.scrollTo(0, nextDestination);
    }
};

我们可以明确的知道函数里读取了scrollTop、clientHeight、scrollHegith和使用了window.requestAnimationFrame、window.scrollTo方法。这三个属性和这两个方法在node环境中其实都是没有的。

针对window.requestAnimationFrame其实我们在上面的shim.js已经做了模拟,方法的具体多久执行我们不需要在意,只需要知道过一段时间他就应该执行其就可以了,所以使用了setTimeout来进行模拟,这个api在node端也是存在的。那么对于其他的属性和方法,我们可以有下面的测试代码。

const scrollHeight = 8000;
const clientHeight = 1000;
const fakeWindow = {
    scrollTop: 0,
};
beforeAll(() => {
    // 定义网页整体高度
    jest.spyOn(document.documentElement, 'scrollHeight', 'get').mockImplementation(() => scrollHeight);
    // 定义窗口高度
    jest.spyOn(document.documentElement, 'clientHeight', 'get').mockImplementation(() => clientHeight);
    // 劫持scrollTop的获取,存放在fakeWindow里
    jest.spyOn(document.documentElement, 'scrollTop', 'get').mockImplementation(() => fakeWindow.scrollTop);
    // 或者像下面这样去操作
});

describe('测试立即达到对应位置', () => {
    beforeEach(() => {
        fakeWindow.scrollTop = 1000;
    });
    test('立即回到顶部', () => {
        const mockScrollTo = jest.fn().mockImplementation((x = 0, y = 0) => {
            fakeWindow.scrollTop = y;
        });
        global.window.scrollTo = mockScrollTo;
        scrollTo(0, { immediately: true });
        const length = mockScrollTo.mock.calls.length;
        expect(mockScrollTo.mock.calls[length - 1][0]).toEqual(0);
    });
});
  1. 通过jest.spyOn对三个属性的get进行定义,scrollHeight和clientHeight是一个静态值我们只需要返回一个固定的值即可,scrollTop则是一个需要在scrollTo函数下进行改变的值,这个时候我们get劫持始终访问一个对象里的值
  2. 对scrollTo进行mock,使每次调用scrollTo函数时都是去改变fakeWindow里的属性值,这样读取和设置我们就都是一个值了
  3. 然后通过对mockScrollTo的mock属性读取,我们就能获取一些记录值,对于这个函数就是最后一次调用一定是传入了目标位置,也就是0。

这样我们就通过jest.spyOn()、jest.fn()方法完成了我们对scrollTo方法的滑动到顶部的单元测试,再添加更多的滑动到底部、中间等测试,我们就算完成了这个方法的基本情况测试,在生产环境使用也就更加自信拉~

其他注意事项

待补充

附录

待补充

 类似资料: