Тестирование связанных компонентов с помощью фермента

Я учусь использовать этот курс тестирования для тестирования подключенных компонентов, настраивая store factory помощник по тестированию, который «создает хранилище для тестирования, соответствующее конфигурации нашего хранилища». Ниже вы можете увидеть мой подключенный образец компонента, а также код, используемый для настройки тестов, в котором я создаю подключенную неглубокую ферментную оболочку моего образца компонента. Однако похоже, что начальное состояние, которое я передаю образцу компонента, в данном случае {jotto: 'foo'} не передается моему образцу компонента при создании этой неглубокой оболочки. Я что-то делаю не так и как мне правильно воссоздать необходимую конфигурацию хранилища при запуске ферментных тестов? Спасибо!

Пример компонента:

import React from 'react';
import {connect} from 'react-redux';

const SampleComponent = (props) => {
  console.info(props);
  return (
    <div>This is a sample component!</div>
  );
};

const mapStateToProps = (state) => ({
  jotto: state.jotto,
});

export default connect(mapStateToProps)(SampleComponent);

редуктор:

import * as jottoActionTypes from 'actionTypes/jottoActionTypes';

export const initialState = {
  isSuccess: false,
};

const jotto = (state = initialState, action) => {
  switch (action.type) {
    case jottoActionTypes.CORRECT_GUESS:
      return {
        ...state,
        isSuccess: true,
      };
    default:
      return state;
  }
};

export default jotto;

корневой редуктор:

import {combineReducers} from 'redux';
import {connectRouter} from 'connected-react-router';
import jotto from 'reducers/jottoReducer';

export default (historyObject) => combineReducers({
  jotto,
  router: connectRouter(historyObject),
});

Настройка теста:

import React from 'react';
import {shallow} from 'enzyme';
import {createStore} from 'redux';
import rootReducer from 'reducers/rootReducer';
import SampleComponent from './sampleComponent';

export const storeFactory = (initialState) => createStore(rootReducer, initialState);

const store = storeFactory({jotto: 'foo'});
const wrapper = shallow(<SampleComponent store = {store} />).dive();
console.info(wrapper.debug());

// Result:
      { store:
         { dispatch: [Function: dispatch],
           subscribe: [Function: subscribe],
           getState: [Function: getState],
           replaceReducer: [Function: replaceReducer],
           [Symbol(observable)]: [Function: observable] },
        jotto: undefined,
        dispatch: [Function: dispatch],
        storeSubscription:
         Subscription {
           store:
            { dispatch: [Function: dispatch],
              subscribe: [Function: subscribe],
              getState: [Function: getState],
              replaceReducer: [Function: replaceReducer],
              [Symbol(observable)]: [Function: observable] },
           parentSub: undefined,
           onStateChange: [Function: bound onStateChange],
           unsubscribe: [Function: unsubscribe],
           listeners:
            { clear: [Function: clear],
              notify: [Function: notify],
              get: [Function: get],
              subscribe: [Function: subscribe] } } }
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Навигация по приложениям React: Исчерпывающее руководство по React Router
Навигация по приложениям React: Исчерпывающее руководство по React Router
React Router стала незаменимой библиотекой для создания одностраничных приложений с навигацией в React. В этой статье блога мы подробно рассмотрим...
Массив зависимостей в React
Массив зависимостей в React
Все о массиве Dependency и его связи с useEffect.
0
0
2 457
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Просто напомню об этом курсе Udemy... это не самый лучший инструмент для обучения. Инструктор подходит к тестированию, используя data attributes, которые не нужны для jest и enzyme тестирования (они также загромождают DOM неиспользуемыми атрибутами).

Кроме того, ее опыт работы с кодом находится на уровне новичка, и она делает довольно много ошибок и странных вариантов кода. Тем не менее, узнайте из него все, что можно, и начните изучать тесты, созданные теми, кто поддерживает популярные пакеты npm (наиболее хорошо документированные и популярные пакеты будут содержать тесты, которые научат вас более практичному подходу к unit и integration тестированию).

В любом случае, я отвлекся, у вас есть два варианта тестирования container:

  1. exportclass/pure function, shallow или mount оберните его и обновите поддельными реквизитами (очень просто, меньше головной боли и чаще делается)
  2. Оберните свой компонент в редукс <Provider> и react-router-dom <MemoryRouter>, а затем mount (может стать очень сложным, поскольку требует полуглубокого понимания: фермента и того, как он интерпретирует DOM при монтировании компонента, действия редукса/ потока редуктора, как создавать фиктивные реализации и/или фиктивные файлы и как правильно обрабатывать действия на основе promise).

Рабочие примеры (щелкните вкладку Tests, чтобы запустить тесты; найдите .tests.js в каталогах, указанных ниже):

Edit Testing Redux Component


Примечание. Codesandbox в настоящее время имеет некоторые ограничения тестирования, как указано ниже, поэтому, пожалуйста, настройте его для своего локального проекта.

контейнеры/Dashboard/__tests__/UnconnectedDashboard.test.js (вы можете так же легко mount обернуть этот несвязанный компонент, чтобы утверждать против его глубоко вложенных дочерних узлов)

import { Dashboard } from "../index.js";

/* 
   codesandbox doesn't currently support mocking, so it's making real
   calls to the API; as a result, the lifecycle methods have been
   disabled to prevent this, and that's why I'm manually calling
   componentDidMount.
*/

const getCurrentProfile = jest.fn();

const fakeUser = {
  id: 1,
  name: "Leanne Graham",
  username: "Bret",
  email: "[email protected]",
  address: {
    street: "Kulas Light",
    suite: "Apt. 556",
    city: "Gwenborough",
    zipcode: "92998-3874",
    geo: {
      lat: "-37.3159",
      lng: "81.1496"
    }
  },
  phone: "1-770-736-8031 x56442",
  website: "hildegard.org",
  company: {
    name: "Romaguera-Crona",
    catchPhrase: "Multi-layered client-server neural-net",
    bs: "harness real-time e-markets"
  }
};

const initialProps = {
  getCurrentProfile,
  currentUser: {},
  isLoading: true
};

describe("Unconnected Dashboard Component", () => {
  let wrapper;
  beforeEach(() => {
    wrapper = shallow(<Dashboard {...initialProps} />);
    wrapper.instance().componentDidMount();
  });

  afterEach(() => wrapper.unmount());

  it("initially renders a spinnner", () => {
    expect(getCurrentProfile).toHaveBeenCalled();
    expect(wrapper.find("Spinner")).toHaveLength(1);
  });

  it("displays the current user", () => {
    wrapper.setProps({ currentUser: fakeUser, isLoading: false });
    expect(getCurrentProfile).toHaveBeenCalled();
    expect(wrapper.find("DisplayUser")).toHaveLength(1);
  });

  it("displays a signup message if no users exist", () => {
    wrapper.setProps({ isLoading: false });
    expect(getCurrentProfile).toHaveBeenCalled();
    expect(wrapper.find("DisplaySignUp")).toHaveLength(1);
  });
});

контейнеры/Dashboard/__tests__/ConnectedDashboard.test.js

import Dashboard from "../index";
// import { getCurrentProfile } from "../../../actions/profileActions";
import * as types from "../../../types";

/* 
  codesandbox doesn't currently support mocking, so it's making real
  calls to the API; however, actions like getCurrentProfile, should be
  mocked as shown below -- in your case, you wouldn't need to use
  a promise, but instead just mock the "guessedWord" action and return
  store.dispatch({ ... })
*/

const fakeUser = {
  id: 1,
  name: "Leanne Graham",
  username: "Bret",
  email: "[email protected]",
  address: {
    street: "Kulas Light",
    suite: "Apt. 556",
    city: "Gwenborough",
    zipcode: "92998-3874",
    geo: {
      lat: "-37.3159",
      lng: "81.1496"
    }
  },
  phone: "1-770-736-8031 x56442",
  website: "hildegard.org",
  company: {
    name: "Romaguera-Crona",
    catchPhrase: "Multi-layered client-server neural-net",
    bs: "harness real-time e-markets"
  }
};

const flushPromises = () => new Promise(resolve => setImmediate(resolve));

describe("Connected Dashboard Component", () => {
  let store;
  let wrapper;
  beforeEach(() => {
    store = createStoreFactory();
    wrapper = mount(
      <Provider store = {store}>
        <MemoryRouter>
          <Dashboard />
        </MemoryRouter>
      </Provider>
    );
  });

  afterEach(() => wrapper.unmount());

  it("initially displays a spinner", () => {
    expect(wrapper.find("Spinner")).toHaveLength(1);
  });

  it("displays the current user after a successful API call", async () => {
    /* 
      getCurrentProfile.mockImplementationOnce(() => new Promise(resolve => {
        resolve(
          store.dispatch({
            type: types.SET_SIGNEDIN_USER,
            payload: fakeUser
          })
        );
      });

      await flushPromises();
      wrapper.update();

      expect(wrapper.find("DisplayUser")).toHaveLength(1);
    */

    store.dispatch({
      type: types.SET_SIGNEDIN_USER,
      payload: fakeUser
    });

    wrapper.update();

    expect(wrapper.find("DisplayUser")).toHaveLength(1);
  });

  it("displays a signup message if no users exist", async () => {
    /* 
      getCurrentProfile.mockImplementationOnce(() => new Promise((resolve,reject) => {
        reject(
          store.dispatch({
            type: types.FAILED_SIGNEDIN_USER
          })
        );
      });

      await flushPromises();
      wrapper.update();

      expect(wrapper.find("DisplaySignUp")).toHaveLength(1);
    */

    store.dispatch({
      type: types.FAILED_SIGNEDIN_USER
    });

    wrapper.update();

    expect(wrapper.find("DisplaySignUp")).toHaveLength(1);
  });
});

Спасибо, Мэтт! Какую альтернативу вы бы предложили вместо использования атрибутов данных? Есть ли у вас какие-либо предложения по пакетам для обзора, которые имеют хорошие принципы тестирования, и какие-либо другие ресурсы, блоги, курсы, на которые можно взглянуть? Я скажу в ее защиту, что мне очень нравится курс и то, как она подходит к предмету, и я определенно многому учусь, но ценю любые отзывы и другие точки зрения!

Jimmy 09.04.2019 19:37

Вы держите пари. Вместо data attributes вы можете искать по: component (Spinner), element.className (div.sk-fading-circle), className (.sk-fading-circle), ..и так далее. Пример списка см. здесь: airbnb.io/enzyme/docs/api/ReactWrapper/find.html. Что касается пакетов, я использовал этот, чтобы начать свое обучение: github.com/JedWatson/react-select/blob/master/src/__tests__/‌​…

Matt Carlotta 09.04.2019 19:47

Мы с коллегой говорили о ее практиках, и хотя она научит основам тестирования, она также научит некоторым вредным привычкам (например, использованию refs для извлечения input значений, когда вы должны использовать state для управления формами: reactjs.org/docs/forms.html#управляемые-компоненты). Тем не менее, я настоятельно рекомендую этот курс в качестве альтернативы: udemy.com/react-2-е издание

Matt Carlotta 09.04.2019 19:57

Я считаю атрибут data-test более надежным, чем выбор элементов на основе имени класса или даже имени элемента. Другие разработчики могут не знать, что имя класса используется для целей тестирования, и имена элементов могут быть изменены, если вводятся стилизованные компоненты. С атрибутами data-test ясно, для чего используется атрибут, и нет необходимости его менять. Также вы можете использовать npmjs.com/package/babel-plugin-react-remove-properties, чтобы удалить атрибуты проверки данных из производственной сборки.

callum.bennett 13.11.2019 11:41
Ответ принят как подходящий

Решение: я забыл параметр браузера для своего корневого редуктора, поскольку использовал connected-react-router.

import rootReducer from 'reducers/rootReducer';
import {createBrowserHistory} from 'history';


export const storeFactory = (initialState) => createStore(rootReducer(createBrowserHistory()), initialState);

Это исправило мою проблему!

callum.bennett 13.11.2019 11:42

Другие вопросы по теме

Похожие вопросы

Реагировать | Facebook JS API: код ошибки 100 при попытке загрузить несколько изображений в канал моей страницы
Почему мои ссылки на реагирующий маршрутизатор работают в ящике, но не в заголовке
Как обрабатывать условные операторы в стилизованном компоненте
Как многократно отображать один и тот же компонент с одним и тем же реквизитом?
Реакция: ввод формы поиска не очищается от состояния путем установки состояния ввода в пустой массив
React Native — создание динамического массива из реквизита для React Native Picker
Как исправить «Ошибку типа: невозможно прочитать карту свойств неопределенного?»
Запрос HTTP POST от клиентского приложения реагирования на сервер spring-boot возвращает ошибку 400
UseRef "относится к значению, но здесь используется как тип."
Тестирование асинхронной функции, передаваемой в качестве реквизита от родителя к дочернему