web-dev-qa-db-de.com

Ein Modul kann nicht mit Scherz und Testfunktionsaufrufen gespielt werden

Ich erstelle ein Projekt mithilfe der create-app-Komponente , die eine neue App mit Build-Skripts (babel, webpack, jest) konfiguriert.

Ich habe eine React-Komponente geschrieben, die ich testen möchte. Die Komponente erfordert eine weitere Javascript-Datei, die eine Funktion verfügbar macht.

Meine search.js-Datei

export {
  search,
}

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

Meine Reaktionskomponente:

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}
            />
  }
}

Ich möchte in meinen Unit-Tests überprüfen, ob die Methode search mit den richtigen Argumenten aufgerufen wurde.

Meine Tests sehen so aus:

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

Ich hatte schon eine schwierige Zeit, um herauszufinden, wie jest.mock funktioniert, da Variablen mock vorangestellt werden muss.

Wenn ich jedoch Argumente für die Methode search testen möchte, muss ich die gespielte Funktion in meinen Tests verfügbar machen.

Wenn ich den spöttischen Teil umwandle, verwenden Sie eine Variable:

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

Ich erhalte diesen Fehler:

TypeError: (0, _search.search) ist keine Funktion

Was auch immer ich versuche, auf jest.fn zuzugreifen und die Argumente zu testen, ich kann es nicht zum Laufen bringen.

Was mache ich falsch?

19
ghusse

Das Problem

Der Grund, warum Sie diesen Fehler erhalten, hat damit zu tun, wie verschiedene Operationen ausgeführt werden.

Obwohl Sie in Ihrem ursprünglichen Code nur SearchContainernach importieren, indem Sie mockSearch einen Wert zuweisen und mock von Jest aufrufen, weisen die specs darauf hin: Before instantiating a module, all of the modules it requested must be available.

Zum Zeitpunkt, zu dem SearchContainer importiert wird, und wiederum search importiert, ist Ihre Variable mockSearch noch undefiniert.

Man könnte das seltsam finden, denn es scheint auch, search.js ist noch nicht verspottet, und so würde Spott nicht funktionieren. Glücklicherweise stellt (babel-) jest sicher, dass Aufrufe von mock und ähnlichen Funktionen sogar höher angehoben werden als Importe, so dass das Spotteln funktioniert.

Trotzdem wird die Zuweisung von mockSearch, auf die von der Mock-Funktion verwiesen wird, nicht mit dem Aufruf mock angehoben. Die Reihenfolge der relevanten Operationen ist also etwa wie folgt:

  1. Legen Sie eine Scheinfabrik für ./search.js fest.
  2. Importieren Sie alle Abhängigkeiten, die die Modellfabrik aufrufen, um eine Funktion für die Komponente bereitzustellen
  3. Weisen Sie mockSearch einen Wert zu.

Wenn Schritt 2 auftritt, ist die an die Komponente übergebene search-Funktion undefiniert, und die Zuweisung in Schritt 3 ist zu spät, um dies zu ändern.

Lösung

Wenn Sie die Mock-Funktion als Teil des mock-Aufrufs erstellen (sodass auch diese angehoben wird), hat sie einen gültigen Wert, wenn er vom Komponentenmodul importiert wird, wie Ihr frühes Beispiel zeigt.

Wie Sie bereits gesagt haben, beginnt das Problem, wenn Sie die gespielte Funktion in Ihren Tests verfügbar machen möchten. Es gibt eine naheliegende Lösung für dieses Problem: Importieren Sie das Modul, das Sie bereits verspottet haben, separat.

Da Sie jetzt wissen, dass Spott vor dem Import tatsächlich vorkommt, wäre dies ein trivialer Ansatz:

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

Vielleicht möchten Sie Folgendes ersetzen:

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

Mit:

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

Dies sollte keinen funktionalen Unterschied ausmachen, könnte jedoch dazu führen, dass das, was Sie tun, etwas expliziter wird (und sollte allen helfen, die ein Typüberprüfungssystem wie Flow verwenden), so dass Sie nicht glauben, dass Sie versucht, Scheinfunktionen aufzurufen auf der ursprünglichen search).

Zusätzliche Anmerkung

All dies ist nur dann unbedingt erforderlich, wenn Sie den Standardexport eines Moduls selbst simulieren müssen. Ansonsten (wie @publicJorn darauf hinweist), können Sie das betreffende Mitglied in den Tests einfach neu zuweisen, wie:

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

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

Wenn Sie einen Api-Aufruf mit einer Antwort verspotten, denken Sie an async () => Ihren Test und warten auf das Wrapper-Update. Meine Seite hat den typischen ComponentDidMount => API-Aufruf aufrufen => positive Antwort hat einen Status gesetzt ... Der Status im Wrapper wurde jedoch nicht aktualisiert ... async und warten darauf, dass das in diesem Beispiel behoben wird ... Dieses Beispiel dient der Kürze ...

...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