Hopp til hovedinnhold
Versjon: 30.0

ES6-klassemocks

Unofficial Beta Translation

This page was AI-translated by PageTurner (beta). Not officially endorsed by the project. Found an error? Report issue →

Jest kan brukes til å mocke ES6-klasser som importeres inn i filer du vil teste.

ES6-klasser er konstruktørfunksjoner med litt syntaktisk sukker. Derfor må enhver mock for en ES6-klasse enten være en funksjon eller en faktisk ES6-klasse (som igjen er en annen funksjon). Du kan derfor mocke dem ved å bruke mockfunksjoner.

Et ES6-klasseeksempel

Vi bruker et konstruert eksempel med en klasse som spiller lydfiler, SoundPlayer, og en forbrukerklasse som bruker denne klassen, SoundPlayerConsumer. Vi vil mocke SoundPlayer i testene våre for 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);
}
}

De 4 måtene å lage en ES6-klassemock på

Automatisk mock

Når du kaller jest.mock('./sound-player'), får du en nyttig "automatisk mock" du kan bruke til å overvåpe kall til klassekonstruktøren og alle dens metoder. Den erstatter ES6-klassen med en mockkonstruktør og erstatter alle metodene med mockfunksjoner som alltid returnerer undefined. Metodekall lagres i theAutomaticMock.mock.instances[index].methodName.mock.calls.

notat

Hvis du bruker pilfunksjoner i klassene dine, vil de ikke være en del av mocken. Grunnen er at pilfunksjoner ikke finnes på objektets prototype – de er bare egenskaper som holder en referanse til en funksjon.

Hvis du ikke trenger å erstatte klassenes implementasjon, er dette det enkleste alternativet å sette opp. For eksempel:

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]).toBe(coolSoundFileName);
// Equivalent to above check:
expect(mockPlaySoundFile).toHaveBeenCalledWith(coolSoundFileName);
expect(mockPlaySoundFile).toHaveBeenCalledTimes(1);
});

Manuell mock

Opprett en manuell mock ved å lagre en mockimplementasjon i __mocks__-mappen. Dette lar deg spesifisere implementasjonen, og den kan brukes på tvers av testfiler.

__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;

Importer mocken og mockemetoden som deles av alle instanser:

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);
});

Å kalle jest.mock() med modulfabrikkparameteren

jest.mock(path, moduleFactory) tar et modulfabrikk-argument. En modulfabrikk er en funksjon som returnerer mocken.

For å mocke en konstruktørfunksjon må fabrikkfunksjonen for modulen returnere en konstruktørfunksjon. Med andre ord må fabrikkfunksjonen være en funksjon som returnerer en funksjon - en høyereordens funksjon (HOF).

import SoundPlayer from './sound-player';
const mockPlaySoundFile = jest.fn();
jest.mock('./sound-player', () => {
return jest.fn().mockImplementation(() => {
return {playSoundFile: mockPlaySoundFile};
});
});
advarsel

Siden kall til jest.mock() heves til toppen av filen, forhindrer Jest tilgang til variabler utenfor scope. Som standard kan du ikke definere en variabel først og deretter bruke den i fabrikken. Jest vil deaktivere denne sjekken for variabler som starter med ordet mock. Du må likevel forsikre deg om at de initialiseres i tide. Vær oppmerksom på Temporal Dead Zone.

For eksempel vil følgende kaste en feil om scope på grunn av bruk av fake i stedet for mock i variabeldeklarasjonen.

// Note: this will fail
import SoundPlayer from './sound-player';
const fakePlaySoundFile = jest.fn();
jest.mock('./sound-player', () => {
return jest.fn().mockImplementation(() => {
return {playSoundFile: fakePlaySoundFile};
});
});

Følgende vil kaste en ReferenceError til tross for bruk av mock i variabeldeklarasjonen, fordi mockSoundPlayer ikke er pakket inn i en pilfunksjon og dermed nås før initialisering etter heving.

import SoundPlayer from './sound-player';
const mockSoundPlayer = jest.fn().mockImplementation(() => {
return {playSoundFile: mockPlaySoundFile};
});
// results in a ReferenceError
jest.mock('./sound-player', () => {
return mockSoundPlayer;
});

Erstatte mocken med mockImplementation() eller mockImplementationOnce()

Du kan erstatte alle ovenstående mocks for å endre implementasjonen, enten for en enkelt test eller alle tester, ved å kalle mockImplementation() på den eksisterende mocken.

Kall til jest.mock heises til toppen av koden. Du kan angi en mock senere, f.eks. i beforeAll(), ved å kalle mockImplementation() (eller mockImplementationOnce()) på den eksisterende mocken i stedet for å bruke fabrikkparameteren. Dette lar deg også endre mocken mellom tester om nødvendig:

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();
});
});

I dybden: Forstå mock-konstruktørfunksjoner

Å bygge din egen konstruktørfunksjonsmock med jest.fn().mockImplementation() kan få mocks til å virke mer kompliserte enn de egentlig er. Denne delen viser hvordan du kan lage dine egne mocks for å illustrere hvordan mocking fungerer.

Manuel mock som en annen ES6-klasse

Hvis du definerer en ES6-klasse med samme filnavn som den mockede klassen i __mocks__-mappen, vil denne tjene som mock. Denne klassen vil bli brukt i stedet for den ekte klassen. Dette lar deg injisere en testimplementasjon for klassen, men gir ikke mulighet til å spore kall.

For vårt konstruerte eksempel kan mocken se slik ut:

__mocks__/sound-player.js
export default class SoundPlayer {
constructor() {
console.log('Mock SoundPlayer: constructor was called');
}

playSoundFile() {
console.log('Mock SoundPlayer: playSoundFile was called');
}
}

Mock ved hjelp av fabrikkparameter for modulen

Fabrikkfunksjonen for modulen som sendes til jest.mock(path, moduleFactory) kan være en høyereordens funksjon (HOF) som returnerer en funksjon*. Dette vil tillate å kalle new på mocken. Igjen, dette lar du injisere ulik oppførsel for testing, men gir ikke mulighet til å spore kall.

* Fabrikkfunksjonen for modulen må returnere en funksjon

For å mocke en konstruktørfunksjon må fabrikkfunksjonen for modulen returnere en konstruktørfunksjon. Med andre ord må fabrikkfunksjonen være en funksjon som returnerer en funksjon - en høyereordens funksjon (HOF).

jest.mock('./sound-player', () => {
return function () {
return {playSoundFile: () => {}};
};
});
notat

Mocken kan ikke være en pilfunksjon fordi kallet new på en pilfunksjon ikke er tillatt i JavaScript. Så dette vil ikke fungere:

jest.mock('./sound-player', () => {
return () => {
// Does not work; arrow functions can't be called with new
return {playSoundFile: () => {}};
};
});

Dette vil kaste TypeError: _soundPlayer2.default is not a constructor, med mindre koden transpileres til ES5, f.eks. av @babel/preset-env. (ES5 har verken pilfunksjoner eller klasser, så begge vil bli transpilert til vanlige funksjoner.)

Mocking av en spesifikk metode i en klasse

La oss si at du ønsker å mocke eller spore metoden playSoundFile i klassen SoundPlayer. Et enkelt eksempel:

// your jest test file below
import SoundPlayer from './sound-player';
import SoundPlayerConsumer from './sound-player-consumer';

const playSoundFileMock = jest
.spyOn(SoundPlayer.prototype, 'playSoundFile')
.mockImplementation(() => {
console.log('mocked function');
}); // comment this line if just want to "spy"

it('player consumer plays music', () => {
const player = new SoundPlayerConsumer();
player.playSomethingCool();
expect(playSoundFileMock).toHaveBeenCalled();
});

Statiske metoder, gettere og settere

La oss anta at klassen vår SoundPlayer har en getter-metode foo og en statisk metode brand

export default class SoundPlayer {
constructor() {
this.foo = 'bar';
}

playSoundFile(fileName) {
console.log('Playing sound file ' + fileName);
}

get foo() {
return 'bar';
}
static brand() {
return 'player-brand';
}
}

Du kan enkelt mocke/spore på dem, her er et eksempel:

// your jest test file below
import SoundPlayer from './sound-player';

const staticMethodMock = jest
.spyOn(SoundPlayer, 'brand')
.mockImplementation(() => 'some-mocked-brand');

const getterMethodMock = jest
.spyOn(SoundPlayer.prototype, 'foo', 'get')
.mockImplementation(() => 'some-mocked-result');

it('custom methods are called', () => {
const player = new SoundPlayer();
const foo = player.foo;
const brand = SoundPlayer.brand();

expect(staticMethodMock).toHaveBeenCalled();
expect(getterMethodMock).toHaveBeenCalled();
});

Holde styr på bruk (sporing av mocken)

Å injisere en testimplementasjon er nyttig, men du vil sannsynligvis også ønske å teste om klassekonstruktøren og metodene blir kalt med riktige parametere.

Sporing av konstruktøren

For å spore kall til konstruktøren, erstatt funksjonen returnert av HOF med en Jest-mockfunksjon. Opprett den med jest.fn(), og spesifiser deretter implementasjonen med mockImplementation().

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

Dette lar oss undersøke bruken av vår mockede klasse ved hjelp av SoundPlayer.mock.calls: expect(SoundPlayer).toHaveBeenCalled(); eller nesten ekvivalent: expect(SoundPlayer.mock.calls.length).toBeGreaterThan(0);

Mocking av ikke-standard klasseeksporteringer

Hvis klassen ikke er standardeksporten fra modulen, må du returnere et objekt med nøkkelen som samsvarer med klasseeksportnavnet.

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

Sporing av metodene i klassen vår

Vår mockede klasse må tilby alle medlemsfunksjoner (som playSoundFile i eksemplet) som vil bli kalt under testingen, ellers får vi en feilmelding for å kalle en ikke-eksisterende funksjon. Men vi vil sannsynligvis også ønske å overvåke kall til disse metodene for å verifisere at de ble kalt med forventede parametere.

Et nytt objekt opprettes hver gang mock-konstruktørfunksjonen kalles under tester. For å overvåke metodekall i alle disse objektene, populerer vi playSoundFile med en annen mock-funksjon og lagrer en referanse til denne mock-funksjonen i testfilen vår, slik at den er tilgjengelig under testing.

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
});
});

Det manuelle mock-ekvivalentet av dette ville være:

__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;

Bruken er lik modulfabrikkfunksjonen, bortsett fra at du kan utelate det andre argumentet i jest.mock(), og du må importere den mockede metoden inn i testfilen din siden den ikke lenger er definert der. Bruk den originale modulbanen for dette - ikke inkluder __mocks__.

Rydde opp mellom tester

For å nullstille kallhistorikken til mock-konstruktørfunksjonen og dens metoder, kaller vi mockClear() i beforeEach()-funksjonen:

beforeEach(() => {
SoundPlayer.mockClear();
mockPlaySoundFile.mockClear();
});

Komplett eksempel

Her er en komplett testfil som bruker modulfabrikkparameteren til jest.mock:

sound-player-consumer.test.js
import SoundPlayer from './sound-player';
import SoundPlayerConsumer from './sound-player-consumer';

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]).toBe(coolSoundFileName);
});