web-dev-qa-db-fra.com

Impossible de se moquer d'un module avec plaisanterie et de tester les appels de fonction

Je crée un projet avec create-app-composant , qui configure une nouvelle application avec des scripts de construction (babel, webpack, jest).

J'ai écrit un composant React que j'essaie de tester. Le composant nécessite un autre fichier javascript, exposant une fonction.

Mon fichier search.js

export {
  search,
}

function search(){
  // does things
  return Promise.resolve('foo')
}

Mon composant de réaction:

import React from 'react'
import { search } from './search.js'
import SearchResults from './SearchResults'

export default SearchContainer {
  constructor(){
    this.state = {
      query: "hello world"
    }
  }

  componentDidMount(){
    search(this.state.query)
      .then(result => { this.setState({ result, })})
  }

  render() {
    return <SearchResults 
            result={this.state.result}
            />
  }
}

Dans mes tests unitaires, je veux vérifier que la méthode search a été appelée avec les arguments corrects.

Mes tests ressemblent à ça:

import React from 'react';
import { shallow } from 'enzyme';
import should from 'should/as-function';

import SearchResults from './SearchResults';

let mockPromise;

jest.mock('./search.js', () => {
  return { search: jest.fn(() => mockPromise)};
});

import SearchContainer from './SearchContainer';

describe('<SearchContainer />', () => {
  it('should call the search module', () => {
    const result = { foo: 'bar' }
    mockPromise = Promise.resolve(result);
    const wrapper = shallow(<SearchContainer />);

    wrapper.instance().componentDidMount();

    mockPromise.then(() => {
      const searchResults = wrapper.find(SearchResults).first();
      should(searchResults.prop('result')).equal(result);
    })    
  })
});

J'ai déjà eu du mal à comprendre comment faire fonctionner jest.mock, car cela nécessite que les variables soient préfixées par mock.

Mais si je veux tester les arguments de la méthode search, je dois rendre la fonction simulée disponible dans mes tests.

Si je transforme la partie moqueuse, utiliser une variable:

const mockSearch = jest.fn(() => mockPromise)
jest.mock('./search.js', () => {
  return { search: mockSearch};
});

Je reçois cette erreur:

TypeError: (0, _search.search) n'est pas une fonction

Quoi que j'essaye d'avoir accès au jest.fn et de tester les arguments, je ne peux pas le faire fonctionner.

Qu'est-ce que je fais mal?

19
ghusse

Le problème

La raison pour laquelle vous obtenez cette erreur est liée à la manière dont diverses opérations sont hissées.

Même si dans votre code d'origine, vous importez uniquement SearchContaineraprès que ait affecté une valeur à mockSearch et appelé mock de jest, les specs indiquent que: Before instantiating a module, all of the modules it requested must be available.

Par conséquent, au moment où SearchContainer est importé et, à son tour, importe search, votre variable mockSearch est toujours indéfinie.

On pourrait trouver cela étrange, car cela semblerait aussi impliquer que search.js ne soit pas encore moqué, et que se moquer ne fonctionnerait pas du tout. Heureusement, (babel-) jest veille à ce que les appels à mock et fonctions similaires soient encore plus élevés que supérieurs par rapport aux importations, de sorte que les moqueries fonctionnent.

Néanmoins, l'attribution de mockSearch, qui est référencée par la fonction fictive, ne sera pas levée avec l'appel mock. Ainsi, l'ordre des opérations pertinentes sera quelque chose comme:

  1. Définir une maquette d'usine pour ./search.js
  2. Importer toutes les dépendances, ce qui appellera l’usine fictive pour une fonction à donner au composant
  3. Attribuer une valeur à mockSearch

Lorsque l'étape 2 se produit, la fonction search transmise au composant sera indéfinie et l'affectation à l'étape 3 est trop tardive pour changer cela.

Solution

Si vous créez la fonction fictive dans le cadre de l'appel mock (de sorte qu'elle soit également levée), sa valeur sera valide lors de son importation par le module de composant, comme le montre votre exemple précédent.

Comme vous l'avez souligné, le problème commence lorsque vous souhaitez que la fonction simulée soit disponible dans vos tests. Il y a une solution évidente à cela: importer séparément le module que vous avez déjà imité.

Puisque vous savez maintenant que la plaisanterie se moque avant les importations, une approche triviale serait:

import { search } from './search.js'; // This will actually be the mock

jest.mock('./search.js', () => {
  return { search: jest.fn(() => mockPromise) };
});

[...]

beforeEach(() => {
  search.mockClear();
});

it('should call the search module', () => {
  [...]

  expect(search.mock.calls.length).toBe(1);
  expect(search.mock.calls[0]).toEqual(expectedArgs);
});

En fait, vous voudrez peut-être remplacer:

import { search } from './search.js';

Avec:

const { search } = require.requireMock('./search.js');

Cela ne devrait pas faire de différence fonctionnelle, mais pourrait rendre ce que vous faites un peu plus explicite (et devrait aider toute personne utilisant un système de vérification de type tel que Flow, afin qu'il ne pense pas que vous essayez d'appeler des fonctions fictives sur l'original search).

Remarque additionnelle

Tout cela n’est strictement nécessaire que si l’exploitation par défaut d’un module lui-même est ce que vous devez simuler. Autrement (comme le souligne @publicJorn), vous pouvez simplement réaffecter le membre pertinent spécifique dans les tests, comme suit:

import * as search from './search.js';

beforeEach(() => {
  search.search = jest.fn(() => mockPromise);
});
37
Tomty

Lorsque vous vous moquez d'un appel d'API avec une réponse, rappelez-vous de async () => votre test et attendez la mise à jour de l'encapsuleur. Le composant typique de ma page était le composantDidMount => l'appel d'API => la réponse positive a défini un état ... mais l'état de l'encapsuleur n'a pas été mis à jour ... asynchrone et attend une correction qui ... cet exemple est pour la brièveté ...

...otherImports etc...
const SomeApi = require.requireMock('../../api/someApi.js');

jest.mock('../../api/someApi.js', () => {
    return {
        GetSomeData: jest.fn()
    };
});

beforeEach(() => {
    // Clear any calls to the mocks.
    SomeApi.GetSomeData.mockClear();
});

it("renders valid token", async () => {
        const responseValue = {data:{ 
            tokenIsValid:true}};
        SomeApi.GetSomeData.mockResolvedValue(responseValue);
        let wrapper = shallow(<MyPage {...props} />);
        expect(wrapper).not.toBeNull();
        await wrapper.update();
        const state = wrapper.instance().state;
        expect(state.tokenIsValid).toBe(true);

    });
0
swandog