Тестирование компонентов React, которые извлекают данные с помощью хуков - PullRequest
0 голосов
/ 07 марта 2019

My React-приложение имеет компонент, который выбирает данные для отображения с удаленного сервера. В эпоху до хуков componentDidMount() было идеальным местом. Но теперь я хотел использовать крючки для этого.

const App = () => {
  const [ state, setState ] = useState(0);
  useEffect(() => {
    fetchData().then(setState);
  });
  return (
    <div>... data display ...</div>
  );
};

А мой тест с использованием Jest и Enzyme выглядит так:

import React from 'react';
import { mount } from 'enzyme';
import App from './App';
import { act } from 'react-test-renderer';

jest.mock('./api');

import { fetchData } from './api';

describe('<App />', () => {
  it('renders without crashing', (done) => {
    fetchData.mockImplementation(() => {
      return Promise.resolve(42);
    });
    act(() => mount(<App />));
    setTimeout(() => {
      // expectations here
      done();
    }, 500);
  });  
});

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

console.error node_modules/react-dom/cjs/react-dom.development.js:506
    Warning: An update to App inside a test was not wrapped in act(...).

    When testing, code that causes React state updates should be wrapped into act(...):

    act(() => {
    /* fire events that update state */
    });
    /* assert on the output */

    This ensures that you're testing the behavior the user would see in the browser. Learn more at (redacted)
        in App (created by WrapperComponent)
        in WrapperComponent

Единственное обновление компонента App происходит из-за обратного вызова Promise. Как я могу гарантировать, что это произойдет в блоке act? Документы ясно предлагают, чтобы утверждения происходили вне блока act. Кроме того, размещение их внутри не меняет предупреждение.

Ответы [ 3 ]

1 голос
/ 10 апреля 2019

Фермент не поддерживает хуки, так как это относительно новая функция: https://github.com/airbnb/enzyme/issues/2011

Может быть, вы можете использовать обычный Jest? Также не беспокойтесь о предупреждении, оно должно исчезнуть после выпуска React 16.9.0 (см. Этот запрос на извлечение https://github.com/facebook/react/pull/14853)

0 голосов
/ 23 июня 2019

Я создал примеры для тестирования асинхронных хуков.

https://github.com/oshri6688/react-async-hooks-testing

CommentWithHooks.js

import { getData } from "services/dataService";

const CommentWithHooks = () => {
  const [data, setData] = useState(null);
  const [isLoading, setIsLoading] = useState(true);

  const fetchData = () => {
    setIsLoading(true);

    getData()
      .then(data => {
        setData(data);
      })
      .catch(err => {
        setData("No Data");
      })
      .finally(() => {
        setIsLoading(false);
      });
  };

  useEffect(() => {
    fetchData();
  }, []);

  return (
    <div>
      {isLoading ? (
        <span data-test-id="loading">Loading...</span>
      ) : (
        <span data-test-id="data">{data}</span>
      )}

      <button
        style={{ marginLeft: "20px" }}
        data-test-id="btn-refetch"
        onClick={fetchData}
      >
        refetch data
      </button>
    </div>
  );
};

CommentWithHooks.test.js:

import React from "react";
import { mount } from "enzyme";
import { act } from "react-dom/test-utils";
import MockPromise from "testUtils/MockPromise";
import CommentWithHooks from "./CommentWithHooks";
import { getData } from "services/dataService";

jest.mock("services/dataService", () => ({
  getData: jest.fn(),
}));

let getDataPromise;

getData.mockImplementation(() => {
  getDataPromise = new MockPromise();

  return getDataPromise;
});

describe("CommentWithHooks", () => {
  beforeEach(() => {
    jest.clearAllMocks();
  });

  it("when fetching data successed", async () => {
    const wrapper = mount(<CommentWithHooks />);
    const button = wrapper.find('[data-test-id="btn-refetch"]');
    let loadingNode = wrapper.find('[data-test-id="loading"]');
    let dataNode = wrapper.find('[data-test-id="data"]');

    const data = "test Data";

    expect(loadingNode).toHaveLength(1);
    expect(loadingNode.text()).toBe("Loading...");

    expect(dataNode).toHaveLength(0);

    expect(button).toHaveLength(1);
    expect(button.prop("onClick")).toBeInstanceOf(Function);

    await getDataPromise.resolve(data);

    wrapper.update();

    loadingNode = wrapper.find('[data-test-id="loading"]');
    dataNode = wrapper.find('[data-test-id="data"]');

    expect(loadingNode).toHaveLength(0);

    expect(dataNode).toHaveLength(1);
    expect(dataNode.text()).toBe(data);
  });

testUtils/MockPromise.js:

import { act } from "react-dom/test-utils";

const createMockCallback = callback => (...args) => {
  let result;

  if (!callback) {
    return;
  }

  act(() => {
    result = callback(...args);
  });

  return result;
};

export default class MockPromise {
  constructor() {
    this.promise = new Promise((resolve, reject) => {
      this.promiseResolve = resolve;
      this.promiseReject = reject;
    });
  }

  resolve(...args) {
    this.promiseResolve(...args);

    return this;
  }

  reject(...args) {
    this.promiseReject(...args);

    return this;
  }

  then(...callbacks) {
    const mockCallbacks = callbacks.map(callback =>
      createMockCallback(callback)
    );

    this.promise = this.promise.then(...mockCallbacks);

    return this;
  }

  catch(callback) {
    const mockCallback = createMockCallback(callback);

    this.promise = this.promise.catch(mockCallback);

    return this;
  }

  finally(callback) {
    const mockCallback = createMockCallback(callback);

    this.promise = this.promise.finally(mockCallback);

    return this;
  }
}
0 голосов
/ 20 марта 2019

У меня была точно такая же проблема, и в итоге я написал библиотеку, которая решает эту проблему, высмеивая все стандарты React Hooks.

По сути, act() - это синхронная функция, такая как useEffect, ноuseEffect выполняет асинхронную функцию.Нет никакого способа, которым act () мог бы «ждать», чтобы это выполнить.Запусти и забудь!

Статья здесь: https://medium.com/@jantoine/another-take-on-testing-custom-react-hooks-4461458935d4

Библиотека здесь: https://github.com/antoinejaussoin/jooks

Чтобы проверить свой код, сначала нужно извлечь свою логику (выборкуи т. д.) в отдельный пользовательский хук: что-то вроде:

const useFetchData = () => {
  const [ state, setState ] = useState(0);
  useEffect(() => {     
    fetchData().then(setState);
  });
  return state;
}

Тогда, используя Jooks, ваш тест будет выглядеть так:

import init from 'jooks';
[...]
describe('Testing my hook', () => {
  const jooks = init(() => useFetchData());

  // Mock your API call here, by returning 'some mocked value';

  it('Should first return 0', () => {
    const data = jooks.run();
    expect(data).toBe(0);
  });

  it('Then should fetch the data and return it', async () => {
    await jooks.mount(); // Fire useEffect etc.
    const data = jooks.run();
    expect(data).toBe('some mocked value');
  });
});

...