Jest 可用于模擬導入到要測試的文件中的 ES6 類。
ES6 類是帶有一些語法糖的構造函數(shù)。因此,任何 ES6 類的模擬都必須是一個函數(shù)或一個實際的 ES6 類(這又是另一個函數(shù))。所以你可以使用模擬函數(shù)來模擬它們。
我們將使用一個播放聲音文件的類的人為示例,?SoundPlayer
?,以及使用該類的使用者類?SoundPlayerConsumer
?。我們將?SoundPlayer
?在我們的測試中模擬?SoundPlayerConsumer
?.
// sound-player.js
export default class SoundPlayer {
constructor() {
this.foo = 'bar';
}
playSoundFile(fileName) {
console.log('Playing sound file ' + fileName);
}
}
// sound-player-consumer.js
import SoundPlayer from './sound-player';
export default class SoundPlayerConsumer {
constructor() {
this.soundPlayer = new SoundPlayer();
}
playSomethingCool() {
const coolSoundFileName = 'song.mp3';
this.soundPlayer.playSoundFile(coolSoundFileName);
}
}
調用?jest.mock('./sound-player')
?返回一個有用的“自動模擬”,你可以使用它來監(jiān)視對類構造函數(shù)及其所有方法的調用。它取代了ES6類與模擬構造,并將其所有方法始終返回未定義的模擬函數(shù)。方法調用保存在?theAutomaticMock.mock.instances[index].methodName.mock.calls
?.
請注意,如果你在類中使用箭頭函數(shù),它們將不會成為模擬的一部分。原因是箭頭函數(shù)不存在于對象的原型中,它們只是持有對函數(shù)的引用的屬性。
如果不需要替換類的實現(xiàn),這是最容易設置的選項。例如:
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__
?文件夾中保存模擬實現(xiàn)來創(chuàng)建手動模擬??。這允許指定實現(xiàn),并且它可以跨測試文件使用。
// __mocks__/sound-player.js
// Import this named export into your test file:
export const mockPlaySoundFile = jest.fn();
const mock = jest.fn().mockImplementation(() => {
return {playSoundFile: mockPlaySoundFile};
});
export default mock;
導入所有實例共享的模擬和模擬方法:
// sound-player-consumer.test.js
import SoundPlayer, {mockPlaySoundFile} 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();
mockPlaySoundFile.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', () => {
const soundPlayerConsumer = new SoundPlayerConsumer();
const coolSoundFileName = 'song.mp3';
soundPlayerConsumer.playSomethingCool();
expect(mockPlaySoundFile).toHaveBeenCalledWith(coolSoundFileName);
});
?jest.mock(path, moduleFactory)
?接受一個模塊工廠參數(shù)。模塊工廠是一個返回模擬的函數(shù)。
為了模擬構造函數(shù),模塊工廠必須返回一個構造函數(shù)。換句話說,模塊工廠必須是一個返回函數(shù)的函數(shù)——高階函數(shù)(HOF)。
import SoundPlayer from './sound-player';
const mockPlaySoundFile = jest.fn();
jest.mock('./sound-player', () => {
return jest.fn().mockImplementation(() => {
return {playSoundFile: mockPlaySoundFile};
});
});
factory 參數(shù)的一個限制是,因為調用?jest.mock()
?被提升到文件的頂部,所以不可能先定義一個變量然后在工廠中使用它。以單詞“mock”開頭的變量是一個例外。由您來保證它們會按時初始化!例如,由于在變量聲明中使用了 'fake' 而不是 'mock',以下代碼將拋出一個范圍外錯誤:
// Note: this will fail
import SoundPlayer from './sound-player';
const fakePlaySoundFile = jest.fn();
jest.mock('./sound-player', () => {
return jest.fn().mockImplementation(() => {
return {playSoundFile: fakePlaySoundFile};
});
});
可以通過對現(xiàn)有的模擬調用?mockImplementation()
?來替換上述所有模擬,以更改單個測試或所有測試的實現(xiàn)。
對 jest.mock 的調用被提升到代碼的頂部??梢陨院笤?code style="font-size: 15px;">beforeAll()?指定一個模擬,方法時對現(xiàn)有模擬調用?mockImplementation()
?(或?mockImplementationOnce()
?), 而不是使用工廠參數(shù)。如果需要,這還允許在測試之間更改模擬:
import SoundPlayer from './sound-player';
import SoundPlayerConsumer from './sound-player-consumer';
jest.mock('./sound-player');
describe('When SoundPlayer throws an error', () => {
beforeAll(() => {
SoundPlayer.mockImplementation(() => {
return {
playSoundFile: () => {
throw new Error('Test error');
},
};
});
});
it('Should throw an error when calling playSomethingCool', () => {
const soundPlayerConsumer = new SoundPlayerConsumer();
expect(() => soundPlayerConsumer.playSomethingCool()).toThrow();
});
});
使用?jest.fn().mockImplementation()
?構建構造函數(shù)??模擬會使模擬看起來比實際更復雜。本節(jié)介紹了如何創(chuàng)建自己的模擬,來說明模擬的工作原理。
如果使用與?__mocks__
文件?夾中的模擬類相同的文件名定義 ES6 類,它將用作模擬。這個類將用于代替真正的類。這允許你為類注入測試實現(xiàn),但不提供監(jiān)視調用的方法。
對于人為的示例,模擬可能如下所示:
// __mocks__/sound-player.js
export default class SoundPlayer {
constructor() {
console.log('Mock SoundPlayer: constructor was called');
}
playSoundFile() {
console.log('Mock SoundPlayer: playSoundFile was called');
}
}
傳遞給的模塊工廠函數(shù)?jest.mock(path, moduleFactory)
?可以是返回函數(shù)*的 HOF。這將允許調用?new
?模擬。同樣,這允許你測試注入不同的行為,但不提供監(jiān)視調用的方法。
為了模擬構造函數(shù),模塊工廠必須返回一個構造函數(shù)。換句話說,模塊工廠必須是一個返回函數(shù)的函數(shù)——高階函數(shù)(HOF)。
jest.mock('./sound-player', () => {
return function () {
return {playSoundFile: () => {}};
};
});
注意:箭頭函數(shù)不起作用
請注意,模擬不能是箭頭函數(shù),因為newJavaScript 中不允許調用箭頭函數(shù)。所以這行不通:
jest.mock('./sound-player', () => {
return () => {
// Does not work; arrow functions can't be called with new
return {playSoundFile: () => {}};
};
});
這將拋出?TypeError: _soundPlayer2.default is not a constructor
?,除非代碼被轉換為 ES5,例如通過?@babel/preset-env
?. (ES5 沒有箭頭函數(shù)和類,所以兩者都將被轉換為普通函數(shù)。)
注入測試實現(xiàn)很有幫助,但您可能還想測試是否使用正確的參數(shù)調用了類構造函數(shù)和方法。
為了跟蹤對構造函數(shù)的調用,將 HOF 返回的函數(shù)替換為 Jest 模擬函數(shù)。用 來創(chuàng)建它jest.fn(),然后用 來指定它的實現(xiàn)?mockImplementation()
?。
import SoundPlayer from './sound-player';
jest.mock('./sound-player', () => {
// Works and lets you check for constructor calls:
return jest.fn().mockImplementation(() => {
return {playSoundFile: () => {}};
});
});
這將讓我們檢查模擬類的使用情況,使用?SoundPlayer.mock.calls
?:?expect(SoundPlayer).toHaveBeenCalled();
?或接近等效的:?expect(SoundPlayer.mock.calls.length).toEqual(1);
?
如果類不是模塊的默認導出,那么您需要返回一個對象,其鍵與類導出名稱相同。
import {SoundPlayer} from './sound-player';
jest.mock('./sound-player', () => {
// Works and lets you check for constructor calls:
return {
SoundPlayer: jest.fn().mockImplementation(() => {
return {playSoundFile: () => {}};
}),
};
});
我們的模擬類需要提供?playSoundFile
?在我們的測試期間將被調用的任何成員函數(shù)(在示例中),否則我們將在調用不存在的函數(shù)時出錯。但是我們可能還想監(jiān)視對這些方法的調用,以確保使用預期的參數(shù)調用它們。
每次在測試期間調用模擬構造函數(shù)時,都會創(chuàng)建一個新對象。為了監(jiān)視所有這些對象中的方法調用,我們填充?playSoundFile
?了另一個模擬函數(shù),并將對同一個模擬函數(shù)的引用存儲在我們的測試文件中,以便在測試期間可用。
import SoundPlayer from './sound-player';
const mockPlaySoundFile = jest.fn();
jest.mock('./sound-player', () => {
return jest.fn().mockImplementation(() => {
return {playSoundFile: mockPlaySoundFile};
// Now we can track calls to playSoundFile
});
});
與此等效的手動模擬將是:
// __mocks__/sound-player.js
// Import this named export into your test file
export const mockPlaySoundFile = jest.fn();
const mock = jest.fn().mockImplementation(() => {
return {playSoundFile: mockPlaySoundFile};
});
export default mock;
用法類似于模塊工廠函數(shù),不同之處在于您可以省略 from 的第二個參數(shù)?jest.mock()
?,并且你必須將模擬方法導入到你的測試文件中,因為它不再在那里定義。為此使用原始模塊路徑;不包括?__mocks__
?.
為了清除對模擬構造函數(shù)及其方法的調用記錄,我們mockClear()在?beforeEach()
?函數(shù)中調用:
beforeEach(() => {
SoundPlayer.mockClear();
mockPlaySoundFile.mockClear();
});
這是一個完整的測試文件,它使用模塊工廠參數(shù)來?jest.mock
?:
// sound-player-consumer.test.js
import SoundPlayerConsumer from './sound-player-consumer';
import SoundPlayer from './sound-player';
const mockPlaySoundFile = jest.fn();
jest.mock('./sound-player', () => {
return jest.fn().mockImplementation(() => {
return {playSoundFile: mockPlaySoundFile};
});
});
beforeEach(() => {
SoundPlayer.mockClear();
mockPlaySoundFile.mockClear();
});
it('The consumer should be able to call new() on SoundPlayer', () => {
const soundPlayerConsumer = new SoundPlayerConsumer();
// Ensure constructor created the object:
expect(soundPlayerConsumer).toBeTruthy();
});
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', () => {
const soundPlayerConsumer = new SoundPlayerConsumer();
const coolSoundFileName = 'song.mp3';
soundPlayerConsumer.playSomethingCool();
expect(mockPlaySoundFile.mock.calls[0][0]).toEqual(coolSoundFileName);
});
更多建議: