Нельзя издеваться над модулем со шуткой и вызовами тестовых функций

Я создаю проект с помощью create-app-component, который настраивает новое приложение со сценариями сборки (babel, webpack, jest).

Я написал компонент React, который я пытаюсь проверить. Компонент требует другой файл javascript, отображающий функцию.

Мой файл search.js

export {
  search,
}

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

Мой реактивный компонент:

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

В моих модульных тестах я хочу проверить, вызван ли search метода правильными аргументами.

Мои тесты выглядят примерно так:

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

У меня уже было трудное время, чтобы выяснить, как сделать jest.mock работу, потому что она требует переменных, которые будут префиксом mock.

Но если я хочу проверить аргументы метода search, мне нужно сделать издеваемую функцию доступной в моих тестах.

Если я преобразую насмешку, используйте переменную:

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

Я получаю эту ошибку:

TypeError: (0, _search.search) не является функцией

Независимо от того, что я пытаюсь получить доступ к jest.fn и проверять аргументы, я не могу заставить его работать.

Что я делаю не так?

Ответ 1

Проблема

Причина, по которой вы получаете эту ошибку, связана с тем, как поднимаются различные операции.

Несмотря на то, что в исходном коде вы импортируете SearchContainer только после назначения значения mockSearch и вызова jest mock, спецификации указывают, что: Before instantiating a module, all of the modules it requested must be available.

Поэтому во время SearchContainer и, в свою очередь, импортирует search, ваша переменная mockSearch по-прежнему не определена.

Это может показаться странным, поскольку, похоже, это также означает, что search.js еще не насмехается, и поэтому издевательство не будет работать вообще. К счастью, (babel-) шутка позволяет поднимать вызовы, чтобы mock и подобные функции, даже выше, чем импорт, так что издевательство будет работать.

Тем не менее, назначение mockSearch, на которое ссылается функция mock, не будет поднято с помощью mock вызова. Таким образом, порядок соответствующих операций будет выглядеть примерно так:

  1. Настройте фабрику макетов для ./search.js
  2. Импортируйте все зависимости, которые будут вызывать фабрику макета для функции, чтобы предоставить компонент
  3. Назначить значение mockSearch

Когда выполняется шаг 2, функция search переданная компоненту, будет неопределенной, а назначение на шаге 3 слишком поздно, чтобы изменить это.

Решение

Если вы создадите функцию макета как часть mock вызова (например, чтобы он был поднят), он будет иметь допустимое значение, когда он будет импортирован модулем компонента, как показывает ваш ранний пример.

Как вы указали, проблема начинается, когда вы хотите сделать издеваемую функцию доступной в ваших тестах. Существует одно очевидное решение: отдельно импортируйте модуль, который вы уже издевались.

Так как теперь вы знаете, что шутка на самом деле происходит до импорта, тривиальный подход:

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

Фактически, вы можете захотеть заменить:

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

С:

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

Это не должно приводить к какой-либо функциональной разнице, но может сделать то, что вы делаете немного более явным (и должно помочь любому, кто использует систему проверки типов, например Flow, поэтому не думает, что вы пытаетесь вызвать макетные функции при первоначальном search).

Дополнительное примечание

Все это строго необходимо, только если вам нужно высмеивать по умолчанию экспорт самого модуля. В противном случае (как указывает @publicJorn), вы можете просто повторно назначить конкретного соответствующего члена в тестах, например:

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

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

Ответ 2

При издевательстве вызова api с ответом помните async() => ваш тест и ожидайте обновления оболочки. Моя страница сделала типичный компонентDidMount => make API call => положительный ответ, установил какое-то состояние... однако состояние в оболочке не обновилось... async и ждут исправления... этот пример для краткости...

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

    });