Плавное тестирование компонентов React с несколькими контекстами.

Кредиты: Фото Тимы Мирошниченко

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

Это очень интересный топик, так как многие разработчики, даже старшие, иногда не знают с чего начать (как вы видите, начало — это проблема), или как мы можем использовать утилиты или помощники, чтобы уменьшить кодовую таблицу в наших компонентах, особенно когда я хочу протестировать компоненты, обернутые в несколько Context Providers. Нужно ли мне повторять свои действия в каждом тестовом файле? Надеюсь, это облегчит вам жизнь, давайте приступим!…. Мы, конечно же, будем использовать библиотеку тестирования react.

Проблема

У нас есть приложение, которое имеет некоторый Context, и наши компоненты потребляют эти значения Context, теперь нам нужно протестировать эти компоненты, и мы хотим определенно передать таможенные значения нашим компонентам Providers, чтобы попытаться подтвердить результаты в наших модульных тестах.

Первоначальное решение

Изначально, вы можете подумать, что давайте экспортируем наш Provider, передадим пользовательские значения и т.д. и будем ожидать каких-то результатов, и да, и нет, это проблема по следующим причинам

  • Повторять все время во всех файлах, но добавлять контекстный провайдер со значениями
  • Если вам нужно вывести компонент, который вы хотите протестировать с более чем одним Context, это может стать трудночитаемым и очень шаблонным.

Давайте рассмотрим простой пример контекста


const initialState = {
  name: "alex",
  age: 39
};

const MyContext = React.createContext(initialState);

export const useMyContext = () => React.useContext(MyContext);

const reducer = (currentState, newState) => ({ ...currentState, ...newState });

export const MyContextProvider = ({ children }) => {
  const [state, setState] = React.useReducer(reducer, initialState);
  return (
    <MyContext.Provider value={{ state, setState }}>
      {children}
    </MyContext.Provider>
  );
};
Вход в полноэкранный режим Выход из полноэкранного режима

BTW вы можете сделать это круче, но деструктурировать Provider из Context в одной строке, обратите внимание на крутой useReducer :), но по сути это то же самое, поэтому вы будете использовать этот Context как:

export default function App() {
  return (
    <MyContextProvider>
      <Component />
    </MyContextProvider>
  );
}
Войти в полноэкранный режим Выйти из полноэкранного режима

И в компоненте вы можете использовать ваш Context, используя пользовательский хук, который вы уже объявили в файле Context, что-то вроде:

function Component() {
  const { state, setState } = useMyContext();
  return (
    <div>
      <input
        value={state.name}
        onChange={(e) => setState({ name: e.target.value })}
      />
      Name: {state.name}, Last Name: {state.lastName}
    </div>
  );
}
Войти в полноэкранный режим Выйти из полноэкранного режима

Теперь вы хотите протестировать этот компонент, верно? Что вы делаете? Экспортируем Context, чтобы снова объявить обертку в моем тесте и передать пользовательские значения, переходим в наш файл Context и экспортируем наш контекст

export const MyContext = React.createContext(initialState);
Войти в полноэкранный режим Выйти из полноэкранного режима

Теперь в своем тесте вы будете делать что-то вроде

import { render } from '@testing-library/react';

const renderComponent() {
  return (
    render(
      <MyContext.Provider value={{ mockState, mockFnc}}>
        <Component>
      </MyContext.Provider>
    )
  )
}
// ... test
Войти в полноэкранный режим Выйти из полноэкранного режима

Это хорошо, если ваш компонент использует только один Context, но если вы используете несколько? И даже если один, вам нужно сделать это во всех ваших тестах.

Решение: пользовательский рендеринг

Давайте создадим пользовательский метод рендеринга, который возвращает наш компонент, обернутый в столько Контекстов, сколько мы хотим, с таким количеством значений Провайдера, которое мы хотим!

// /testUtils/index.js
// custom render
import { render as rtlRender } from '@testing-library/react';

// our custom render
export const render = (ui, renderOptions) => {
    try {
        return rtlRender(setupComponent(ui, renderOptions));
    } catch (error: unknown) {
        throw new Error('Render rest util error');
    }
};
Вход в полноэкранный режим Выйти из полноэкранного режима

Этот метод утилиты будет ожидать params, компонент, называемый ui, и опции, и он будет использовать метод setupComponent для рендеринга представления как обычный компонент react, давайте закончим!

// /testUtils/index.js
// import all the Context you will use in the app
import {MyContext} from './MyContext'
import {MyContext1} from './MyContext'
import {MyContext2} from './MyContext'

const CONTEXT_MAP = {
  MyContext,
  MyContext1,
  MyContext2
}

const setupComponent = (ui, renderOptions) => {
  const { withContext } = renderOptions;

  if (withContext == null) return ui;

  return (
      <>
          {withContext.reduceRight((acc, { context, contextValue }) => {
              const Ctx = CONTEXT_MAP[context];
              return <Ctx.Provider value={contextValue}>{acc}</Ctx.Provider>;
          }, ui)}
      </>
  );
};
Вход в полноэкранный режим Выход из полноэкранного режима

Уменьшая вправо, вы гарантируете, что первый Контекст, который вы передадите, будет отрисован первым, хорошо, да? Финальный файл выглядит так:

// /testUtils/index.js
// import all the context you will use in the app
import { render as rtlRender } from '@testing-library/react';
import {MyContext} from './MyContext'
import {MyContext1} from './MyContext'
import {MyContext2} from './MyContext'

const CONTEXT_MAP = {
  MyContext,
  MyContext1,
  MyContext2
}

const setupComponent = (ui, renderOptions) => {
  const { withContext } = renderOptions;

  if (withContext == null) return ui;

  return (
      <>
          {withContext.reduceRight((acc, { context, contextValue }) => {
              const Ctx = CONTEXT_MAP[context];
              return <Ctx.Provider value={contextValue}>{acc}</Ctx.Provider>;
          }, ui)}
      </>
  );
};

// our custom render
export const render = (ui, renderOptions) => {
    try {
        return rtlRender(setupComponent(ui, renderOptions));
    } catch (error: unknown) {
        throw new Error('Render rest util error');
    }
};
Вход в полноэкранный режим Выход из полноэкранного режима

Теперь тот же тест будет выглядеть следующим образом:

import { render } from './testUtils';

const renderComponent() {
  return (
    render(
        <Component/>,
        [{context: "MyContext", contextValue: {name: 'Max', lastName: "Smith"}}]
    )
  )
}

// test ...
Войти в полноэкранный режим Выход из полноэкранного режима

Самое интересное, что в массив Contexts вы можете передать столько, сколько хотите, следуя формату {context, contextValue}, конечно, рекомендуется использовать typescript, но это сделает статью длиннее, но теперь вы поняли идею, если у вас возникнут проблемы с превращением этого в TS, дайте мне знать, я могу помочь. Вот и все, ребята, дайте мне знать, если вы используете какой-либо другой трюк или делаете это с помощью другого подхода. Счастливого кодирования!

Оцените статью
Procodings.ru
Добавить комментарий