Моки и стабы

В экосистеме MUI (Material-UI) при разработке интерфейсов на React часто возникает необходимость изолированного тестирования компонентов без зависимости от внешних API, глобального состояния или сложных контекстов. Для этого используются моки (mocks) и стабы (stubs). Они позволяют создавать предсказуемую среду для компонентов, упрощают юнит-тесты и повышают стабильность кода.


Различие между моками и стабами

  • Мок — это объект или функция, полностью имитирующая поведение настоящего элемента, включая контроль за вызовами, аргументами и результатами. Основная цель мока — проверка взаимодействия между компонентами или функциями.

  • Стаб — это объект или функция, возвращающая заранее определённые данные без внутренней логики. Стабы упрощают работу компонента, предоставляя фиксированные ответы для тестирования.

Пример различия:

// Стаб: возвращает фиксированный результат
const fetchUserStub = () => ({ id: 1, name: 'Alice' });

// Мок: отслеживает вызовы и параметры
const fetchUserMock = jest.fn(() => ({ id: 1, name: 'Alice' }));

fetchUserMock();
expect(fetchUserMock).toHaveBeenCalled();

Использование моков для компонентов MUI

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

  • Тем оформления (ThemeProvider)
  • Контекстов (LocalizationProvider, SnackbarProvider)
  • API-запросов (axios, fetch)
  • Колбэков (onClick, onChange)

Пример мока темы

import { createTheme, ThemeProvider } from '@mui/material/styles';
import Button from '@mui/material/Button';

const themeMock = createTheme({
  palette: {
    primary: { main: '#1976d2' },
    secondary: { main: '#dc004e' },
  },
});

function MockedButton() {
  return (
    <ThemeProvider theme={themeMock}>
      <Button color="primary">Нажми меня</Button>
    </ThemeProvider>
  );
}

В этом примере themeMock используется для тестирования компонента Button в конкретной цветовой палитре без зависимости от глобальной темы приложения.


Моки для асинхронных операций

Компоненты MUI часто используют Dialogs, Autocomplete, Select и другие элементы, которые взаимодействуют с асинхронными источниками данных. Для тестирования их поведения применяются асинхронные моки.

import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import Autocomplete from '@mui/material/Autocomplete';
import TextField from '@mui/material/TextField';

const fetchOptionsMock = jest.fn(() =>
  Promise.resolve([{ label: 'Option 1' }, { label: 'Option 2' }])
);

test('Autocomplete загружает опции', async () => {
  render(
    <Autocomplete
      options={[]}
      getOptionLabel={(option) => option.label}
      renderInput={(params) => <TextField {...params} label="Выберите опцию" />}
      onInputCha nge={async (_, value) => {
        const options = await fetchOptionsMock();
        // обновление состояния
      }}
    />
  );

  await userEvent.type(screen.getByLabelText(/выберите опцию/i), 'O');
  expect(fetchOptionsMock).toHaveBeenCalled();
});

Здесь fetchOptionsMock выступает моком асинхронной функции, что позволяет тесту работать предсказуемо и не зависеть от реального API.


Стабы для тестирования пользовательских взаимодействий

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

const optionsStub = [
  { id: 1, label: 'Option A' },
  { id: 2, label: 'Option B' }
];

<Autocomplete
  options={optionsStub}
  getOptionLabel={(option) => option.label}
  renderInput={(params) => <TextField {...params} label="Выбор" />}
/>

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


Комбинирование моков и стабов

Часто эффективнее использовать сочетание моков и стабов:

  • Стабы предоставляют фиксированные данные для компонента.
  • Моки отслеживают взаимодействия с этими данными.

Пример для компонента Dialog с кнопкой отправки:

import { Dialog, DialogActions, Button } from '@mui/material';

const onSubmitM ock = jest.fn();
const dataStub = { name: 'Alice', email: 'alice@example.com' };

function MockedDialog() {
  return (
    <Dialog open>
      <DialogActions>
        <Button onCl ick={() => onSubmitMock(dataStub)}>Отправить</Button>
      </DialogActions>
    </Dialog>
  );
}

// Тест
userEvent.click(screen.getByText(/отправить/i));
expect(onSubmitMock).toHaveBeenCalledWith(dataStub);

Здесь dataStub гарантирует фиксированный ввод, а onSubmitMock позволяет проверить вызов функции с нужными параметрами.


Интеграция с Jest и Testing Library

MUI и React-компоненты удобно тестировать через Jest и React Testing Library:

  • jest.fn() создаёт моки функций.
  • jest.mock() позволяет подменять модули, например axios или @mui/material компоненты.
  • screen и userEvent из Testing Library взаимодействуют с DOM, позволяя проверять реакцию компонентов на действия пользователя.
jest.mock('axios', () => ({
  get: jest.fn(() => Promise.resolve({ data: [{ id: 1, name: 'Alice' }] }))
}));

Такой подход позволяет полностью изолировать тестируемый компонент от внешних зависимостей.


Практические рекомендации

  • Использовать моки для функций, методов, обработчиков событий и внешних API, где важно отслеживать вызовы.
  • Использовать стабы для предсказуемых данных, чтобы тест не зависел от сложной логики.
  • Для сложных компонентов MUI комбинировать стабы и моки, особенно при работе с асинхронными операциями.
  • Всегда учитывать контексты MUI (ThemeProvider, LocalizationProvider), чтобы компонент рендерился корректно даже в тестовой среде.
  • Асинхронные операции мокать с помощью jest.fn().mockResolvedValue(), чтобы тесты были стабильными и быстрыми.