Подтвердить что ты не робот

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

У меня есть следующая (упрощенная) часть React.

class SalesView extends Component<{}, State> {
  state: State = {
    salesData: null
  };

  componentDidMount() {
    this.fetchSalesData();
  }

  render() {
    if (this.state.salesData) {
      return <SalesChart salesData={this.state.salesData} />;
    } else {
      return <p>Loading</p>;
    }
  }

  async fetchSalesData() {
    let data = await new SalesService().fetchSalesData();
    this.setState({ salesData: data });
  }
}

При установке я извлекаю данные из API, которые я отвлек в классе под названием SalesService. Этот класс я хочу высмеять, а для метода fetchSalesData я хочу указать возвращаемые данные (в обещании).

Это более или менее то, как я хочу, чтобы мой тестовый пример выглядел следующим образом:

  • предопределить тестовые данные
  • импортировать SalesView
  • mock SalesService
  • настроить mockSalesService, чтобы вернуть обещание, которое возвращает предопределенные тестовые данные при разрешении

  • создать компонент

  • Await
  • проверить моментальный снимок

Тестирование взглядов SalesChart не является частью этого вопроса, я надеюсь решить это с помощью Enzyme. Я пробовал десятки вещей, чтобы издеваться над этим асинхронным вызовом, но, похоже, я не могу правильно это высмеять. Я нашел следующие примеры Jest, издевающихся в Интернете, но они, похоже, не охватывают это основное использование.

Мои вопросы:

  • Как должен выглядеть класс макета?
  • Где я должен разместить этот класс-макет?
  • Как мне импортировать этот класс mock?
  • Как я могу сказать, что этот mock-класс заменяет реальный класс?
  • Как настроить макетную реализацию определенной функции класса mock?
  • Как подождать в тестовом примере для обещания, которое нужно решить?

Один пример, который у меня есть, не работает, приведен ниже. Тест-бегун выходит из строя с ошибкой throw err;, а последняя строка в трассировке стека at process._tickCallback (internal/process/next_tick.js:188:7)

# __tests__/SalesView-test.js
import React from 'react';
import SalesView from '../SalesView';

jest.mock('../SalesService');
const salesServiceMock = require('../SalesService').default;

const weekTestData = [];

test('SalesView shows chart after SalesService returns data', async () => {
  salesServiceMock.fetchSalesData.mockImplementation(() => {
    console.log('Mock is called');
    return new Promise((resolve) => {
      process.nextTick(() => resolve(weekTestData));
    });
  });

  const wrapper = await shallow(<SalesView/>);
  expect(wrapper).toMatchSnapshot();
});
4b9b3361

Ответ 1

Иногда, когда тест трудно писать, он пытается сказать нам, что у нас есть проблема с дизайном.

Я думаю, что небольшой рефакторинг может сделать многое намного проще - сделайте SalesService соавтором вместо внутреннего.

Под этим я подразумеваю, вместо того, чтобы вызывать new SalesService() внутри вашего компонента, примите сервис продаж как опору кодом вызова. Если вы это сделаете, код вызова также может быть вашим тестом, и в этом случае все, что вам нужно сделать, - это mock самого SalesService и вернуть все, что захотите (используя синус или любую другую насмешливую библиотеку или даже просто создав ручная прокатка).

Ответ 2

Вы могли бы абстрагировать ключевое слово new с помощью метода SalesService.create(), а затем использовать jest.spyOn(object, methodName) для издевательства над реализацией.

import SalesService from '../SalesService ';

test('SalesView shows chart after SalesService returns data', async () => {

    const mockSalesService = {
        fetchSalesData: jest.fn(() => {
            return new Promise((resolve) => {
                process.nextTick(() => resolve(weekTestData));
            });
        })
    };

    const spy = jest.spyOn(SalesService, 'create').mockImplementation(() => mockSalesService);

    const wrapper = await shallow(<SalesView />);
    expect(wrapper).toMatchSnapshot();
    expect(spy).toHaveBeenCalled();
    expect(mockSalesService.fetchSalesData).toHaveBeenCalled();

    spy.mockReset();
    spy.mockRestore();
});

Ответ 3

Один "уродливый" способ, который я использовал в прошлом, - это сделать инъекцию зависимости бедных людей.

Это основано на том, что вы, возможно, не захотите использовать экземпляр SalesService каждый раз, когда вам это нужно, но вы хотите сохранить один экземпляр для каждого приложения, которое использует каждый. В моем случае SalesService потребовалась некоторая начальная конфигурация, которую я не хотел повторять каждый раз. [1]

Итак, у меня был файл services.ts, который выглядит так:

/// In services.ts
let salesService: SalesService|null = null;
export function setSalesService(s: SalesService) {
    salesService = s;
}
export function getSalesService() {
    if(salesService == null) throw new Error('Bad stuff');
    return salesService;
}

Тогда в моем приложении index.tsx или в каком-то подобном месте я бы:

/// In index.tsx
// initialize stuff
const salesService = new SalesService(/* initialization parameters */)
services.setSalesService(salesService);
// other initialization, including calls to React.render etc.

В компонентах вы можете просто использовать getSalesService для получения ссылки на один экземпляр SalesService для каждого приложения.

Когда придет время для тестирования, вам просто нужно выполнить некоторую настройку в обработчиках mocha (или других) before или beforeEach, чтобы вызвать setSalesService с макетным объектом.

Теперь, в идеале, вы хотите передать SalesService в качестве опоры для своего компонента, потому что это вход для него, и используя getSalesService, вы скрываете эту зависимость и, возможно, дорога. Но если вам это нужно в очень вложенном компоненте, или если вы используете маршрутизатор или что-то подобное, становится довольно громоздким передавать его в качестве опоры.

Возможно, вам также удастся использовать что-то вроде context, чтобы все было внутри React как бы.

"Идеальное" решение для этого было бы чем-то вроде инъекции зависимостей, но это не вариант с React AFAIK.


[1] Он также может помочь в предоставлении единой точки для сериализации вызовов удаленного обслуживания, которые могут потребоваться в какой-то момент.