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

Funzioni Mock

Traduzione Beta Non Ufficiale

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

Le funzioni mock consentono di testare i collegamenti tra il codice sostituendo l'implementazione effettiva di una funzione, registrando le chiamate alla funzione (e i parametri passati in tali chiamate), catturando le istanze delle funzioni costruttore quando vengono istanziate con new, e permettendo la configurazione dei valori restituiti durante i test.

Esistono due modi per creare funzioni mock: creare una funzione mock da utilizzare nel codice di test, o scrivere un manual mock per sovrascrivere una dipendenza di modulo.

Utilizzo di una funzione mock

Immaginiamo di testare un'implementazione della funzione forEach, che richiama una callback per ogni elemento in un array fornito.

forEach.js
export function forEach(items, callback) {
for (const item of items) {
callback(item);
}
}

Per testare questa funzione, possiamo usare una funzione mock e ispezionare lo stato del mock per verificare che la callback venga richiamata come previsto.

forEach.test.js
import {forEach} from './forEach';

const mockCallback = jest.fn(x => 42 + x);

test('forEach mock function', () => {
forEach([0, 1], mockCallback);

// The mock function was called twice
expect(mockCallback.mock.calls).toHaveLength(2);

// The first argument of the first call to the function was 0
expect(mockCallback.mock.calls[0][0]).toBe(0);

// The first argument of the second call to the function was 1
expect(mockCallback.mock.calls[1][0]).toBe(1);

// The return value of the first call to the function was 42
expect(mockCallback.mock.results[0].value).toBe(42);
});

Proprietà .mock

Tutte le funzioni mock possiedono questa speciale proprietà .mock, che conserva i dati relativi a come la funzione è stata chiamata e cosa ha restituito. La proprietà .mock tiene traccia anche del valore di this per ogni chiamata, rendendo possibile ispezionarlo:

const myMock1 = jest.fn();
const a = new myMock1();
console.log(myMock1.mock.instances);
// > [ <a> ]

const myMock2 = jest.fn();
const b = {};
const bound = myMock2.bind(b);
bound();
console.log(myMock2.mock.contexts);
// > [ <b> ]

Questi membri mock sono molto utili nei test per verificare come queste funzioni vengono chiamate, istanziate o cosa hanno restituito:

// The function was called exactly once
expect(someMockFunction.mock.calls).toHaveLength(1);

// The first arg of the first call to the function was 'first arg'
expect(someMockFunction.mock.calls[0][0]).toBe('first arg');

// The second arg of the first call to the function was 'second arg'
expect(someMockFunction.mock.calls[0][1]).toBe('second arg');

// The return value of the first call to the function was 'return value'
expect(someMockFunction.mock.results[0].value).toBe('return value');

// The function was called with a certain `this` context: the `element` object.
expect(someMockFunction.mock.contexts[0]).toBe(element);

// This function was instantiated exactly twice
expect(someMockFunction.mock.instances.length).toBe(2);

// The object returned by the first instantiation of this function
// had a `name` property whose value was set to 'test'
expect(someMockFunction.mock.instances[0].name).toBe('test');

// The first argument of the last call to the function was 'test'
expect(someMockFunction.mock.lastCall[0]).toBe('test');

Valori restituiti mock

Le funzioni mock possono anche essere utilizzate per iniettare valori di test nel codice durante un test:

const myMock = jest.fn();
console.log(myMock());
// > undefined

myMock.mockReturnValueOnce(10).mockReturnValueOnce('x').mockReturnValue(true);

console.log(myMock(), myMock(), myMock(), myMock());
// > 10, 'x', true, true

Le funzioni mock sono particolarmente efficaci nel codice che utilizza uno stile funzionale di passaggio di continuazione. Codice scritto in questo stile evita la necessità di stub complessi che replicano il comportamento del componente reale che sostituiscono, preferendo invece iniettare valori direttamente nel test subito prima del loro utilizzo.

const filterTestFn = jest.fn();

// Make the mock return `true` for the first call,
// and `false` for the second call
filterTestFn.mockReturnValueOnce(true).mockReturnValueOnce(false);

const result = [11, 12].filter(num => filterTestFn(num));

console.log(result);
// > [11]
console.log(filterTestFn.mock.calls[0][0]); // 11
console.log(filterTestFn.mock.calls[1][0]); // 12

La maggior parte degli esempi reali coinvolge l'acquisizione di una funzione mock su un componente dipendente e la sua configurazione, ma la tecnica è identica. In questi casi, cerca di evitare la tentazione di implementare logica all'interno di funzioni non direttamente sottoposte a test.

Mocking di moduli

Supponiamo di avere una classe che recupera utenti dalla nostra API. La classe utilizza axios per chiamare l'API e restituisce l'attributo data contenente tutti gli utenti:

users.js
import axios from 'axios';

class Users {
static all() {
return axios.get('/users.json').then(resp => resp.data);
}
}

export default Users;

Per testare questo metodo senza effettivamente chiamare l'API (evitando così test lenti e fragili), possiamo usare la funzione jest.mock(...) per creare automaticamente il mock del modulo axios.

Dopo aver creato il mock del modulo, possiamo fornire un mockResolvedValue per .get che restituisce i dati su cui il nostro test deve verificare. In pratica, stiamo specificando che axios.get('/users.json') deve restituire una risposta simulata.

users.test.js
import axios from 'axios';
import Users from './users';

jest.mock('axios');

test('should fetch users', () => {
const users = [{name: 'Bob'}];
const resp = {data: users};
axios.get.mockResolvedValue(resp);

// or you could use the following depending on your use case:
// axios.get.mockImplementation(() => Promise.resolve(resp))

return Users.all().then(data => expect(data).toEqual(users));
});

Mocking parziale

Sottogruppi di un modulo possono essere mockati mentre il resto del modulo mantiene la sua implementazione effettiva:

foo-bar-baz.js
export const foo = 'foo';
export const bar = () => 'bar';
export default () => 'baz';
//test.js
import defaultExport, {bar, foo} from '../foo-bar-baz';

jest.mock('../foo-bar-baz', () => {
const originalModule = jest.requireActual('../foo-bar-baz');

//Mock the default export and named export 'foo'
return {
__esModule: true,
...originalModule,
default: jest.fn(() => 'mocked baz'),
foo: 'mocked foo',
};
});

test('should do a partial mock', () => {
const defaultExportResult = defaultExport();
expect(defaultExportResult).toBe('mocked baz');
expect(defaultExport).toHaveBeenCalled();

expect(foo).toBe('mocked foo');
expect(bar()).toBe('bar');
});

Implementazioni mock

Esistono casi in cui è utile andare oltre la specifica dei valori restituiti e sostituire completamente l'implementazione di una funzione mock. Questo può essere fatto con jest.fn o il metodo mockImplementationOnce sulle funzioni mock.

const myMockFn = jest.fn(cb => cb(null, true));

myMockFn((err, val) => console.log(val));
// > true

Il metodo mockImplementation è utile quando devi definire l'implementazione predefinita di una funzione mock creata da un altro modulo:

foo.js
module.exports = function () {
// some implementation;
};
test.js
jest.mock('../foo'); // this happens automatically with automocking
const foo = require('../foo');

// foo is a mock function
foo.mockImplementation(() => 42);
foo();
// > 42

Quando devi ricreare un comportamento complesso di una funzione mock dove chiamate multiple producono risultati diversi, utilizza il metodo mockImplementationOnce:

const myMockFn = jest
.fn()
.mockImplementationOnce(cb => cb(null, true))
.mockImplementationOnce(cb => cb(null, false));

myMockFn((err, val) => console.log(val));
// > true

myMockFn((err, val) => console.log(val));
// > false

Quando la funzione mock esaurisce le implementazioni definite con mockImplementationOnce, eseguirà l'implementazione predefinita impostata con jest.fn (se definita):

const myMockFn = jest
.fn(() => 'default')
.mockImplementationOnce(() => 'first call')
.mockImplementationOnce(() => 'second call');

console.log(myMockFn(), myMockFn(), myMockFn(), myMockFn());
// > 'first call', 'second call', 'default', 'default'

Per i metodi che vengono tipicamente concatenati (e che quindi devono sempre restituire this), disponiamo di un'API semplificata sotto forma di funzione .mockReturnThis() disponibile su tutti i mock:

const myObj = {
myMethod: jest.fn().mockReturnThis(),
};

// is the same as

const otherObj = {
myMethod: jest.fn(function () {
return this;
}),
};

Nomi dei mock

Puoi opzionalmente assegnare un nome alle tue funzioni mock, che verrà visualizzato al posto di 'jest.fn()' negli output degli errori di test. Usa .mockName() se vuoi identificare rapidamente la funzione mock che segnala un errore nel tuo output di test.

const myMockFn = jest
.fn()
.mockReturnValue('default')
.mockImplementation(scalar => 42 + scalar)
.mockName('add42');

Matcher Personalizzati

Infine, per semplificare la verifica di come le funzioni mock sono state chiamate, abbiamo aggiunto alcune funzioni matcher personalizzate:

// The mock function was called at least once
expect(mockFunc).toHaveBeenCalled();

// The mock function was called at least once with the specified args
expect(mockFunc).toHaveBeenCalledWith(arg1, arg2);

// The last call to the mock function was called with the specified args
expect(mockFunc).toHaveBeenLastCalledWith(arg1, arg2);

// All calls and the name of the mock is written as a snapshot
expect(mockFunc).toMatchSnapshot();

Questi matcher sono scorciatoie per le forme più comuni di ispezione della proprietà .mock. Puoi sempre eseguire queste verifiche manualmente se preferisci o se devi fare qualcosa di più specifico:

// The mock function was called at least once
expect(mockFunc.mock.calls.length).toBeGreaterThan(0);

// The mock function was called at least once with the specified args
expect(mockFunc.mock.calls).toContainEqual([arg1, arg2]);

// The last call to the mock function was called with the specified args
expect(mockFunc.mock.calls[mockFunc.mock.calls.length - 1]).toEqual([
arg1,
arg2,
]);

// The first arg of the last call to the mock function was `42`
// (note that there is no sugar helper for this specific of an assertion)
expect(mockFunc.mock.calls[mockFunc.mock.calls.length - 1][0]).toBe(42);

// A snapshot will check that a mock was invoked the same number of times,
// in the same order, with the same arguments. It will also assert on the name.
expect(mockFunc.mock.calls).toEqual([[arg1, arg2]]);
expect(mockFunc.getMockName()).toBe('a mock name');

Per l'elenco completo dei matcher, consulta la documentazione di riferimento.