Vue单元测试---jest测试框架

1,362 阅读6分钟

编写第一个单元测试

这里通过vue-cli脚手架搭建一个项目,并使用eslint进行代码规范,创建一个sum.js文件,并进行测试sum.test.js

//创建并导出一个sum.js文件
function sum(a, b) {
  return a + b;
}
module.exports = sum;
//编写sum.test.js文件,并进行测试
const sum = require('./sum');

test('adds 1 + 2 to equal 3', () => {
  expect(sum(1, 2)).toBe(3);
});
遇见的问题

由于代码使用的eslint进行代码规范的,test和expect只使用了一次,就会出现错误,所以我们需要再eslint的配置文件中添加忽略文件

"globals":{
    "test": true,
    "expect": true,
    "window": true
  },

使用匹配器

// 测试值是否匹配
test('object assignment', () => {
  const data = { one: 1 }
  data['two'] = 2
  expect(data).toEqual({ one: 1, two: 2 })
})

// 测试相反的匹配
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)
    }
  }
})

// jest的匹配
// 测试真实性
test('null', () => {
  const n = null
  expect(n).toBeNull()// 只匹配null
  expect(n).toBeDefined()// 只匹配 undefined
  expect(n).not.toBeUndefined()// 与toBeUndefined相反
  expect(n).not.toBeTruthy()// 匹配任何if语句为真
  expect(n).toBeFalsy()// 匹配任何if语句为假
})

test('zero', () => {
  const z = 0
  expect(z).not.toBeNull()
  expect(z).toBeDefined()
  expect(z).not.toBeUndefined()
  expect(z).not.toBeTruthy()
  expect(z).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)
  expect(value).toBe(4)
  expect(value).toEqual(4)
})

// 测试数字的时候对于浮点数近似,使用toBeClose而不是toEqual
test('两个浮点数字相加', () => {
  const value = 0.1 + 0.2
  // expect(value).toBe(0.3);           这句会报错,因为浮点数有舍入误差
  expect(value).toBeCloseTo(0.3) // 这句可以运行
})

// 弦乐
test('there is no I in team', () => {
  expect('team').not.toMatch(/I/)
})

test('but there is a "stop" in Christoph', () => {
  expect('Christoph').toMatch(/stop/)
})

多次重复设置

如果有多次重复测试就需要使用钩子函数,beforeEach和afterEach,假设我们要在每个测试之前都测试调用方法,或者是在每个测试之后测试调用方法,就需要使用到他们两个钩子函数

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

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

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

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

beforeEach和,afterEach能够通过与异步代码测试相同的方式处理异步代码— —他们可以采用done参数或返回一个诺言。例如,如果initializeCityDatabase()返回解决数据库初始化时的诺言,我们会想返回这一诺言:

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

一次性设置

当设置是异步的时,这尤其麻烦,因此您不能内联地进行设置。在某些情况下,您只需要在文件的开头做一次设置。开玩笑提供beforeAll和afterAll处理这种情况。

例如,如果initializeCityDatabase和clearCityDatabase都返回了promise,城市数据库可以在测试中重用,我们就能把我们的测试代码改成这样:

beforeAll(() => {
  return initializeCityDatabase();
});

afterAll(() => {
  return clearCityDatabase();
});

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

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

关于测试作用域问题

默认情况下before和after的块可以应用到文件中的每个此时,此外可以通过describe块将测试分组,当before或者是after被使用在测试套件(describe)中的时候,他的作用域就只在他当前的作用域中合作,但是如果有顶级的before还有after,他的作用域是最大的

describe和test的执行顺序

Jest会在所有真正的测试开始之前执行测试文件里所有的describe处理程序(handlers)。这是在before和after处理程序里面(而不是在describe块中)进行准备工作和整理工作的另一个原因。块运行完后,,可能情况下,最好会按收集阶段遇到的顺序依次进行测试,依次运行所有测试,,等待每一个测试完成并整理好,然后才继续往下走。

也就是说,先同步执行所有的describe套件(包括内嵌的),然后执行所有的test测试实例

快照测试

快照测试主要用于UI的回归测试,第一次运行测试的时候,会在tests目录下生成snapshots目录,以及相应的组件并以snap为扩展名,之后每次改动,都会在测试中标记出来,如果是自己想要的变动,可以运行jest--updateSnapshot来更新快照,值得注意的是,UI测试以来react-test-render模块,并且要在引入react-native之后

ES6 class Mock

ES6是带有一些语法糖的构造函数,因此,ES6类的任何模拟都必须是一个函数,或一个实际的ES6类,因此您可以在模拟功能中模拟他们

创建ES6类模拟的四种方法

自动模拟

调用jest.mock('./sound-player')返回一个有用的自动模拟,您可以用来监视对类构造函数及其所有方法的调用,它将ES6类替换为模拟构造函数,并将其所有的方法替换为始终返回的模拟函数,方法调用保存在theAutomaticMock.mock.instances[index].methodName.mock.calls,如果您在类中使用箭头函数,则他们不会成为Mock的一部分,这么做的原因是他们只是持有该函数的属性

import SoundPlayer from './sound-player';
import SoundPlayerConsumer from './sound-player-consumer';
jest.mock('./sound-player'); // SoundPlayer is now a mock constructor

beforeEach(() => {
  // Clear all instances and calls to constructor and all methods:
  SoundPlayer.mockClear();
});

it('We can check if the consumer called the class constructor', () => {
  const soundPlayerConsumer = new SoundPlayerConsumer();
  expect(SoundPlayer).toHaveBeenCalledTimes(1);
});

it('We can check if the consumer called a method on the class instance', () => {
  // Show that mockClear() is working:
  expect(SoundPlayer).not.toHaveBeenCalled();

  const soundPlayerConsumer = new SoundPlayerConsumer();
  // Constructor should have been called again:
  expect(SoundPlayer).toHaveBeenCalledTimes(1);

  const coolSoundFileName = 'song.mp3';
  soundPlayerConsumer.playSomethingCool();

  // mock.instances is available with automatic mocks:
  const mockSoundPlayerInstance = SoundPlayer.mock.instances[0];
  const mockPlaySoundFile = mockSoundPlayerInstance.playSoundFile;
  expect(mockPlaySoundFile.mock.calls[0][0]).toEqual(coolSoundFileName);
  // Equivalent to above check:
  expect(mockPlaySoundFile).toHaveBeenCalledWith(coolSoundFileName);
  expect(mockPlaySoundFile).toHaveBeenCalledTimes(1);
});
手动模拟

通过将模拟实现保存在文件夹中来创建手动模拟_mocks_,这使您可以指定实现,并且可以在测试文件中使用他

用模块工厂参数调用

jest.mock(path,moduleFactory)接受模块工厂参数,模块工厂是一个返回模拟的函数,为了模拟构造函数,模拟工厂必须返回构造函数,换句话说,模拟工厂必须是返回函数的函数--高阶函数

import SoundPlayer from './sound-player';
const mockPlaySoundFile = jest.fn();
jest.mock('./sound-player', () => {
  return jest.fn().mockImplementation(() => {
    return {playSoundFile: mockPlaySoundFile};//返回的变量名必须是Mock开头的函数名
  });
});

在工厂函数中,模块工厂功能必须返回一个功能,也就是说,jest.mock()传的第二个参数不能是一个箭头函数,必须是一个ES5中的构造函数

使用mockImplementation()或替换模拟mockImplementationOnce()

可以通过mockImplementation()现有的模拟来替换上述所有的模拟,以更改单个测试或者是所有测试的实现

跟踪使用情况

在上面我们详细的描述了构造函数的模拟情况,以及模拟方式,但是,对于测试人员来说,不仅仅只想要测试构造函数是否成功,最重要的是传入的参数是否正确,所以我们还要对构造函数进行追踪监视

监视构造函数

为了跟踪对构造函数的调用,请用jest模拟函数替换高阶函数,使用创建他jest.fn(),然后使用指定他的而实现mockImplementation()。

import SoundPlayer from './sound-player';
jest.mock('./sound-player', () => {
  // Works and lets you check for constructor calls:
  return jest.fn().mockImplementation(() => {
    return {playSoundFile: () => {}};
  });
});

绕过模块模拟