Vai al contenuto principale
{ "message": "Versione: 30.0", "description": "" }

Mock di Classi ES6

Traduzione Beta Non Ufficiale

Questa pagina è stata tradotta da PageTurner AI (beta). Non ufficialmente approvata dal progetto. Hai trovato un errore? Segnala problema →

Jest può essere utilizzato per simulare classi ES6 importate nei file che desideri testare.

Le classi ES6 sono funzioni costruttore con zucchero sintattico. Pertanto, qualsiasi mock per una classe ES6 deve essere una funzione o una classe ES6 effettiva (che è, a sua volta, un'altra funzione). Puoi quindi simularle utilizzando le funzioni mock.

Esempio di una Classe ES6

Utilizzeremo un esempio artificioso di una classe che riproduce file audio, SoundPlayer, e una classe consumer che utilizza questa classe, SoundPlayerConsumer. Simuleremo SoundPlayer nei nostri test per 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);
}
}

I 4 modi per creare un mock di classe ES6

Mock automatico

Chiamare jest.mock('./sound-player') restituisce un utile "mock automatico" che puoi usare per monitorare le chiamate al costruttore della classe e a tutti i suoi metodi. Sostituisce la classe ES6 con un costruttore mock e tutti i suoi metodi con funzioni mock che restituiscono sempre undefined. Le chiamate ai metodi vengono salvate in theAutomaticMock.mock.instances[index].methodName.mock.calls.

nota

Se utilizzi funzioni freccia nelle tue classi, queste non faranno parte del mock. Il motivo è che le funzioni freccia non sono presenti sul prototipo dell'oggetto, ma sono semplicemente proprietà che contengono un riferimento a una funzione.

Se non devi sostituire l'implementazione della classe, questa è l'opzione più semplice da configurare. Per esempio:

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

Mock manuale

Crea un mock manuale salvando un'implementazione mock nella cartella __mocks__. Questo ti permette di specificare l'implementazione, che può essere utilizzata in più file di test.

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

Importa il mock e il metodo mock condiviso da tutte le istanze:

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

Chiamare jest.mock() con il parametro module factory

jest.mock(path, moduleFactory) accetta un argomento module factory. Un module factory è una funzione che restituisce il mock.

Per mockare una funzione costruttore, la factory deve restituire una funzione costruttore. In altre parole, deve essere una funzione che restituisce un'altra funzione (HOF).

import SoundPlayer from './sound-player';
const mockPlaySoundFile = jest.fn();
jest.mock('./sound-player', () => {
return jest.fn().mockImplementation(() => {
return {playSoundFile: mockPlaySoundFile};
});
});
{ "message": "attenzione", "description": "The default label used for the Caution admonition (:::caution)" }

Poiché le chiamate a jest.mock() vengono sollevate in cima al file, Jest impedisce l'accesso a variabili fuori dallo scope. Di default, non puoi prima definire una variabile e poi usarla nel factory. Jest disabilita questo controllo per variabili che iniziano con la parola mock. Tuttavia, spetta a te garantire che vengano inizializzate per tempo. Tieni presente la Temporal Dead Zone.

Ad esempio, il seguente codice genererà un errore di scope fuori range a causa dell'uso di fake invece di mock nella dichiarazione della variabile.

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

Il seguente codice genererà un ReferenceError nonostante usi mock nella dichiarazione, poiché mockSoundPlayer non è racchiuso in una funzione freccia e quindi viene acceduto prima dell'inizializzazione dopo l'hoisting.

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

Sostituire il mock usando mockImplementation() o mockImplementationOnce()

Puoi sostituire tutti i mock sopra descritti per modificare l'implementazione, per un singolo test o per tutti i test, chiamando mockImplementation() sul mock esistente.

Le chiamate a jest.mock vengono elevate (hoisted) all'inizio del codice. È possibile specificare un mock successivamente, ad esempio in beforeAll(), chiamando mockImplementation() (o mockImplementationOnce()) sul mock esistente invece di usare il parametro factory. Questo consente anche di modificare il mock tra i test, se necessario:

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

Approfondimento: comprendere le funzioni costruttore mock

Costruire un mock di funzione costruttore con jest.fn().mockImplementation() può apparire più complesso di quanto sia in realtà. Questa sezione mostra come creare mock personalizzati per illustrare il funzionamento.

Mock manuale che è un'altra classe ES6

Se si definisce una classe ES6 con lo stesso nome file della classe da mockare nella cartella __mocks__, essa fungerà da mock. Questa classe sostituirà quella reale, permettendo di inserire un'implementazione di test, ma senza tracciare le chiamate.

Per l'esempio proposto, il mock potrebbe essere così:

__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 usando il parametro module factory

La funzione factory passata a jest.mock(path, moduleFactory) può essere una HOF (Higher-Order Function) che restituisce una funzione*. Ciò permetterà di chiamare new sul mock. Anche qui è possibile inserire comportamenti diversi per i test, ma senza tracciare le chiamate.

* La funzione factory deve restituire una funzione

Per mockare una funzione costruttore, la factory deve restituire una funzione costruttore. In altre parole, deve essere una funzione che restituisce un'altra funzione (HOF).

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

Il mock non può essere una arrow function perché in JavaScript non è possibile chiamare new su di essa. Questo esempio NON funzionerà:

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

Genererà TypeError: _soundPlayer2.default is not a constructor, a meno che il codice non sia transpilato in ES5 (ad esempio con @babel/preset-env). L'ES5 non ha arrow function né classi, quindi entrambe verrebbero convertite in funzioni standard.

Mockare uno specifico metodo di una classe

Supponiamo di voler mockare o spiare il metodo playSoundFile nella classe SoundPlayer. Ecco un esempio semplice:

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

Metodi statici, getter e setter

Immaginiamo che la nostra classe SoundPlayer abbia un getter foo e un metodo statico 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';
}
}

È possibile mockarli/spiarli facilmente, come in questo esempio:

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

Tenere traccia dell'utilizzo (spiare il mock)

Inserire un'implementazione di test è utile, ma spesso serve anche verificare che costruttore e metodi vengano chiamati con i parametri corretti.

Spiare il costruttore

Per tracciare le chiamate al costruttore, sostituire la funzione restituita dalla HOF con una mock function di Jest. Crearla con jest.fn() e specificare l'implementazione con mockImplementation().

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

Questo permetterà di ispezionare l'uso della classe mockata tramite SoundPlayer.mock.calls: expect(SoundPlayer).toHaveBeenCalled(); o equivalentemente expect(SoundPlayer.mock.calls.length).toBeGreaterThan(0);

Mockare esportazioni di classe non default

Se la classe non è un'esportazione default del modulo, bisogna restituire un oggetto con una chiave identica al nome dell'esportazione di classe.

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

Spiare i metodi della classe

La nostra classe mockata dovrà fornire tutte le funzioni membro (playSoundFile nell'esempio) che verranno chiamate durante i test, altrimenti otterremo un errore per aver chiamato una funzione inesistente. Tuttavia probabilmente vorremo anche monitorare le chiamate a questi metodi per verificare che siano stati invocati con i parametri attesi.

Un nuovo oggetto verrà creato ogni volta che la funzione costruttrice mockata viene chiamata durante i test. Per monitorare le chiamate ai metodi in tutti questi oggetti, popoliamo playSoundFile con un'altra funzione mock e memorizziamo un riferimento a questa stessa funzione mock nel nostro file di test, così da renderla disponibile durante i test.

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

L'equivalente mock manuale di questo sarebbe:

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

L'utilizzo è simile alla funzione factory del modulo, con la differenza che puoi omettere il secondo argomento da jest.mock() e devi importare il metodo mockato nel tuo file di test poiché non è più definito lì. Usa il percorso del modulo originale per questo; non includere __mocks__.

Pulizia tra i test

Per cancellare la registrazione delle chiamate alla funzione costruttrice mockata e ai suoi metodi, chiamiamo mockClear() nella funzione beforeEach():

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

Esempio completo

Ecco un file di test completo che utilizza il parametro factory del modulo per 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);
});