ES6-Klassen-Mocks
Diese Seite wurde von PageTurner AI übersetzt (Beta). Nicht offiziell vom Projekt unterstützt. Fehler gefunden? Problem melden →
Jest kann verwendet werden, um ES6-Klassen zu mocken, die in Dateien importiert werden, die du testen möchtest.
ES6-Klassen sind Konstruktorfunktionen mit syntaktischem Zucker. Daher muss jeder Mock für eine ES6-Klasse entweder eine Funktion oder eine tatsächliche ES6-Klasse sein (die wiederum eine Funktion ist). Du kannst sie also mit Mock-Funktionen nachbilden.
Beispiel einer ES6-Klasse
Wir verwenden ein konstruiertes Beispiel einer Klasse, die Sounddateien abspielt (SoundPlayer), und einer verbrauchenden Klasse, die diese verwendet (SoundPlayerConsumer). Wir werden SoundPlayer in unseren Tests f ür SoundPlayerConsumer mocken.
export default class SoundPlayer {
constructor() {
this.foo = 'bar';
}
playSoundFile(fileName) {
console.log('Playing sound file ' + fileName);
}
}
import SoundPlayer from './sound-player';
export default class SoundPlayerConsumer {
constructor() {
this.soundPlayer = new SoundPlayer();
}
playSomethingCool() {
const coolSoundFileName = 'song.mp3';
this.soundPlayer.playSoundFile(coolSoundFileName);
}
}
Die 4 Methoden zum Erstellen eines ES6-Klassen-Mocks
Automatischer Mock
Der Aufruf von jest.mock('./sound-player') liefert einen nützlichen "automatischen Mock", mit dem du Aufrufe des Klassenkonstruktors und aller seiner Methoden überwachen kannst. Er ersetzt die ES6-Klasse durch einen Mock-Konstruktor und alle Methoden durch Mock-Funktionen, die stets undefined zurückgeben. Methodenaufrufe werden in theAutomaticMock.mock.instances[index].methodName.mock.calls gespeichert.
Wenn du Pfeilfunktionen in deinen Klassen verwendest, werden sie nicht Teil des Mocks sein. Der Grund dafür ist, dass Pfeilfunktionen nicht im Prototypen des Objekts vorhanden sind – sie sind lediglich Eigenschaften, die auf eine Funktion verweisen.
Wenn du die Implementierung der Klasse nicht ersetzen musst, ist dies die einfachste Einrichtungsoption. Beispiel:
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);
});
Manueller Mock
Erstelle einen manuellen Mock, indem du eine Mock-Implementierung im __mocks__-Ordner speicherst. Dies ermöglicht dir, die Implementierung festzulegen, und er kann testdateiübergreifend verwendet werden.
// Import this named export into your test file:
export const mockPlaySoundFile = jest.fn();
const mock = jest.fn().mockImplementation(() => {
return {playSoundFile: mockPlaySoundFile};
});
export default mock;
Importiere den Mock und die von allen Instanzen gemeinsam genutzte Mock-Methode:
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);
});
Aufruf von jest.mock() mit dem Modulfabrik-Parameter
jest.mock(path, moduleFactory) erwartet ein Modulfabrik-Argument. Eine Modulfabrik ist eine Funktion, die den Mock zurückgibt.
Um Konstruktorfunktionen zu mocken, muss die Modulfabrik eine Konstruktorfunktion zurückgeben. Sie muss also eine Funktion höherer Ordnung sein.
import SoundPlayer from './sound-player';
const mockPlaySoundFile = jest.fn();
jest.mock('./sound-player', () => {
return jest.fn().mockImplementation(() => {
return {playSoundFile: mockPlaySoundFile};
});
});
Da Aufrufe von jest.mock() an den Anfang der Datei gehoistet werden, verhindert Jest den Zugriff auf nicht im Gültigkeitsbereich liegende Variablen. Standardmäßig kannst du nicht zuerst eine Variable definieren und sie dann in der Fabrik verwenden. Jest deaktiviert diese Prüfung für Variablen, die mit mock beginnen. Es liegt jedoch weiterhin bei dir, sicherzustellen, dass sie rechtzeitig initialisiert werden. Beachte die Temporal Dead Zone.
Beispielsweise wird folgendes einen Fehler wegen nicht im Gültigkeitsbereich liegender Variable auslösen, da fake statt mock in der Variablendeklaration verwendet wird:
// Note: this will fail
import SoundPlayer from './sound-player';
const fakePlaySoundFile = jest.fn();
jest.mock('./sound-player', () => {
return jest.fn().mockImplementation(() => {
return {playSoundFile: fakePlaySoundFile};
});
});
Folgendes wird einen ReferenceError auslösen, obwohl mock in der Variablendeklaration verwendet wird, da mockSoundPlayer nicht in eine Pfeilfunktion eingebettet ist und somit nach dem Hoisting vor der Initialisierung darauf zugegriffen wird:
import SoundPlayer from './sound-player';
const mockSoundPlayer = jest.fn().mockImplementation(() => {
return {playSoundFile: mockPlaySoundFile};
});
// results in a ReferenceError
jest.mock('./sound-player', () => {
return mockSoundPlayer;
});
Ersetzen des Mocks mit mockImplementation() oder mockImplementationOnce()
Du kannst alle oben genannten Mocks ersetzen, um die Implementierung für einen einzelnen Test oder alle Tests zu ändern, indem du mockImplementation() auf den vorhandenen Mock aufrufst.
Aufrufe von jest.mock werden an den Anfang des Codes verschoben. Du kannst einen Mock später festlegen, z.B. in beforeAll(), indem du mockImplementation() (oder mockImplementationOnce()) auf den vorhandenen Mock aufrufst, anstatt den Factory-Parameter zu verwenden. Dies ermöglicht es auch, den Mock bei Bedarf zwischen Tests zu ändern:
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();
});
});
Vertiefung: Mock-Konstruktorfunktionen verstehen
Das Erstellen deiner eigenen Konstruktorfunktions-Mocks mit jest.fn().mockImplementation() lässt Mocks komplexer erscheinen, als sie sind. Dieser Abschnitt zeigt, wie du eigene Mocks erstellst, um das Prinzip zu verdeutlichen.
Manueller Mock als alternative ES6-Klasse
Wenn du im __mocks__-Ordner eine ES6-Klasse mit identischem Dateinamen wie die gemockte Klasse definierst, dient sie als Mock. Diese Klasse ersetzt die echte Klasse. Dies ermöglicht das Einschleusen einer Testimplementierung, bietet aber keine Möglichkeit, Aufrufe zu überwachen.
Für unser Beispiel könnte der Mock so aussehen:
export default class SoundPlayer {
constructor() {
console.log('Mock SoundPlayer: constructor was called');
}
playSoundFile() {
console.log('Mock SoundPlayer: playSoundFile was called');
}
}
Mock mit Modulfabrik-Parameter
Die an jest.mock(path, moduleFactory) übergebene Modulfabrik-Funktion kann eine Funktion höherer Ordnung (HOF) sein, die eine Funktion zurückgibt*. Dies ermöglicht new-Aufrufe auf dem Mock. Auch hier kannst du Testverhalten injizieren, aber keine Aufrufe überwachen.
* Modulfabrik-Funktion muss Funktion zurückgeben
Um Konstruktorfunktionen zu mocken, muss die Modulfabrik eine Konstruktorfunktion zurückgeben. Sie muss also eine Funktion höherer Ordnung sein.
jest.mock('./sound-player', () => {
return function () {
return {playSoundFile: () => {}};
};
});
Der Mock kann keine Pfeilfunktion sein, weil new in JavaScript nicht auf Pfeilfunktionen aufgerufen werden darf. Daher funktioniert dies nicht:
jest.mock('./sound-player', () => {
return () => {
// Does not work; arrow functions can't be called with new
return {playSoundFile: () => {}};
};
});
Dies wirft TypeError: _soundPlayer2.default is not a constructor, es sei denn, der Code wird nach ES5 transpiliert (z.B. via @babel/preset-env). (ES5 hat weder Pfeilfunktionen noch Klassen – beide werden zu normalen Funktionen transpiliert.)
Spezifische Klassenmethoden mocken
Angenommen, du möchtest die Methode playSoundFile in der Klasse SoundPlayer mocken oder überwachen. Ein einfaches Beispiel:
// 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();
});
Statische Methoden, Getter und Setter
Nehmen wir an, unsere Klasse SoundPlayer hat eine Getter-Methode foo und eine statische Methode 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 kannst sie einfach mocken/überwachen, hier ein Beispiel:
// 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();
});
Nutzungsverfolgung (Überwachung des Mocks)
Das Einschleusen von Testimplementierungen ist hilfreich, aber oft möchtest du auch prüfen, ob Klassenkonstruktor und -methoden mit korrekten Parametern aufgerufen werden.
Überwachung des Konstruktors
Um Konstruktoraufrufe zu verfolgen, ersetze die von der HOF zurückgegebene Funktion durch eine Jest-Mockfunktion. Erzeuge sie mit jest.fn() und definiere ihre Implementierung mit mockImplementation().
import SoundPlayer from './sound-player';
jest.mock('./sound-player', () => {
// Works and lets you check for constructor calls:
return jest.fn().mockImplementation(() => {
return {playSoundFile: () => {}};
});
});
So können wir Nutzungsstatistiken unseres gemockten Klassen via SoundPlayer.mock.calls prüfen: expect(SoundPlayer).toHaveBeenCalled(); oder ähnlich: expect(SoundPlayer.mock.calls.length).toBeGreaterThan(0);
Mocken nicht-standardmäßiger Klasseneexporte
Wenn die Klasse nicht der Standardexport des Moduls ist, musst du ein Objekt zurückgeben, dessen Schlüssel dem Klassennamen im Export entspricht.
import {SoundPlayer} from './sound-player';
jest.mock('./sound-player', () => {
// Works and lets you check for constructor calls:
return {
SoundPlayer: jest.fn().mockImplementation(() => {
return {playSoundFile: () => {}};
}),
};
});
Überwachung von Klassenmethoden
Unsere gemockte Klasse muss alle Member-Funktionen (im Beispiel playSoundFile) bereitstellen, die während unserer Tests aufgerufen werden, sonst erhalten wir einen Fehler für den Aufruf einer nicht existierenden Funktion. Wahrscheinlich möchten wir jedoch auch die Aufrufe dieser Methoden überwachen, um sicherzustellen, dass sie mit den erwarteten Parametern aufgerufen wurden.
Bei jedem Aufruf der Mock-Konstruktorfunktion während der Tests wird ein neues Objekt erstellt. Um Methodenaufrufe in all diesen Objekten zu überwachen, belegen wir playSoundFile mit einer weiteren Mock-Funktion und speichern eine Referenz auf dieselbe Mock-Funktion in unserer Testdatei, damit sie während der Tests verfügbar ist.
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
});
});
Das manuelle Mock-Äquivalent hierfür wäre:
// Import this named export into your test file
export const mockPlaySoundFile = jest.fn();
const mock = jest.fn().mockImplementation(() => {
return {playSoundFile: mockPlaySoundFile};
});
export default mock;
Die Verwendung ähnelt der Modul-Factory-Funktion, mit dem Unterschied, dass Sie das zweite Argument von jest.mock() weglassen können und die gemockte Methode in Ihre Testdatei importieren müssen, da sie dort nicht mehr definiert ist. Verwenden Sie dafür den ursprünglichen Modulpfad; schließen Sie __mocks__ nicht ein.
Bereinigung zwischen Tests
Um den Aufrufdatensatz der Mock-Konstruktorfunktion und ihrer Methoden zu löschen, rufen wir mockClear() in der beforeEach()-Funktion auf:
beforeEach(() => {
SoundPlayer.mockClear();
mockPlaySoundFile.mockClear();
});
Vollständiges Beispiel
Hier ist eine vollständige Testdatei, die den Modul-Factory-Parameter für jest.mock verwendet:
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);
});