Jest

模拟模块 #

先看代码,isEntity是我们要测试的目标,其中引用了 metadata 模块。

1
2
3
4
5
import { metadata } from '@metadata';

function isEntity (item) {
  return !!metadata.getEntity(item.id)
}

如果 metadata 模块比较复杂,在 jest 的环境中不能直接初始化出来,这时候最好的办法就是通过 mock 模拟这个模块,使测试代码可以正常运行。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import { metadata } from '@metadata';

const entity = {
    custom1Entity: {}
}

jest.mock('@metadata', () => {
  return {
    metadata: {
      getEntity: jest.fn().mockImplementation(id => {
        return entity[id];
      }),
    }
  };
});

describe('模拟模块', () => {
    test('isEntity', () => {
        const item = { id: 'customEntity2' };
        expect(isEntity(item)).toBe(false)
        expect(metadata.getEntity.mock.calls.pop()[0]).toEqual(item.id);
        expect(metadata.getEntity.mock.calls.length).toBe(1);
    })
})

jest.mock 会模拟 @metadata 模块,无论是测试文件中还是原代码中的 @metadata 模块都会被模拟。 jest.fn() 只接受空参数的函数作为参数,并返回一个值。如果想有参数,就要在调用 mockImplementation 方法,来提供有逻辑的实现过程。

模拟ES6类 #

上面的mock方式

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
jest.mock('@metadata', () => {
  return {
    metadata: {
      getEntity: jest.fn().mockImplementation(id => {
        return entity[id];
      }),
    }
  };
});
jest.mock('@client', () => {
  return jest.fn().mockImplementation(() => {
    return {
      post: jest.fn()
    };
  });
})
import client from '@client';

client.post = jest.fn().mockImplementation(() =>
  Promise.resolve({
      data: {}
  })
)

如果上面的mock方式不起作用,可以看看mock的目标是一个 class ,比如这种:

1
2
3
4
5
6
import { Client } from './client';
export * from './types';

const client = new Client();

export default client;

手动模拟 #

mock 目录中,新建与模拟目标目录相同的一个文件。在测试文件中,就回用它代替原本的依赖。

模拟函数 #

jest api document 模拟函数也就是 jest.fn() 它返回一个 mockFn 对象,它有很多方法,可以从多个角度测试我们的代码。

名字

  • mockFn.mockName(name)
  • mockFn.getMockName()

调用信息:

  • mockFn.mock.calls 函数被调用时所接收到的参数。
  • mockFn.mock.results
  • mockFn.mock.instances
  • mockFn.mock.contexts
  • mockFn.mock.lastCall

清理mock,比如虚拟函数被调用了,mock.calls中会有值,可能影响其他用例的结果,就需要清理一下

  • mockFn.mockClear()
  • mockFn.mockReset()
  • mockFn.mockRestore()

模拟函数的实现,可以在mock模块时使用

  • mockFn.mockImplementation(fn) 模拟函数的实现,也就是中间有逻辑,可以根据接收参数,根据参数进行一些计算,与 mockReturnValue 做比较的话, mockReturnValue是直接返回一个结果,并可以通过 mockFn.mock.calls 拿到调用函数时的参数
  • mockFn.mockImplementationOnce(fn)

函数直接返回值:

  • mockFn.mockReturnThis()
  • mockFn.mockReturnValue(value)
  • mockFn.mockReturnValueOnce(value)

函数异步返回结果:

  • mockFn.mockResolvedValue(value)
  • mockFn.mockResolvedValueOnce(value)
  • mockFn.mockRejectedValue(value)
  • mockFn.mockRejectedValueOnce(value)

虚拟函数的例子 #

  1. 模拟模块,并模拟其中的方法
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
jest.mock('metadata', () => ({
  metadata: {
    getEnumValue: jest.fn().mockImplementation((id, value) => {
      const enumMap = {
        ChangeViewType: {
          'adjust': {
            id: 'adjust',
            name: 'adjust',
            title: '调整',
          },
        },
      };
      return enumMap[id][value];
    }),
  },
}));
  1. 模拟模块,其中的方法直接返回数据
1
2
3
4
5
6
7
8
9
jest.mock('metadata', () => ({
  metadata: {
    getEnumValue: jest.fn().mockReturnValue({
      id: 'adjust',
      name: 'adjust',
      title: '调整',
    }),
  },
}));

断言举例 #

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// 断言对象的实例
expect(buttonController).toBeInstanceOf(BudgetPlanChangeButtonController);

// 断言方法是否被调用,源代码中的函数可以通过spyOn追踪
expect(spy).toHaveBeenCalled();

// 断言虚拟函数的调用情况,mock.calls会记录调用函数的参数
expect(metadata.getEnumValue.mock.calls.length).toBe(1);
expect(metadata.getEnumValue.mock.calls[0]).toEqual([
  'ChangeViewType',
  'ChangeViewType.adjust',
]);

jest.spyOn(object, methodName) #

1
2
3
4
5
6
7
const video = {
  play() {
    return true;
  },
};

module.exports = video;
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
const video = require('./video');

afterEach(() => {
  // restore the spy created with spyOn
  jest.restoreAllMocks();
});

test('plays video', () => {
  const spy = jest.spyOn(video, 'play');
  const isPlaying = video.play();

  expect(spy).toHaveBeenCalled();
  expect(isPlaying).toBe(true);
});

jest.spyOn(object, methodName)会跟踪对应的方法,会使其成为一个 mock function ,就可以调用 mockFn 的各种方法和断言。

比如原函数是未被覆盖的,想要覆盖的话,就可以通过调用mockImplementation方法覆盖。

1
2
jest.spyOn(object, methodName).mockImplementation(() => customImplementation)
object[methodName] = jest.fn(() => customImplementation)

因为 jest.spyOn 是一个模拟。 您可以在 afterEach 方法上调用 jest.restoreAllMocks 恢复初始状态。

jest.spyOn(object, methodName, accessType?) #

accessType为get或set,可以追踪get/set方法。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
const video = {
  // it's a getter!
  get play() {
    return true;
  },
};

module.exports = video;

// xx.test.js
test('plays video', () => {
  const spy = jest.spyOn(video, 'play', 'get'); // we pass 'get'
  const isPlaying = video.play;

  expect(spy).toHaveBeenCalled();
  expect(isPlaying).toBe(true);
});