Unit тесты

Архитектура Kepler.gl построена вокруг связки React + Redux, где значительная часть логики вынесена в чистые редьюсеры и селекторы. Это делает библиотеку удобной для юнит-тестирования, поскольку основная бизнес-логика отделена от UI и внешних зависимостей вроде WebGL и Mapbox.

Тестирование в таком проекте обычно делится на несколько уровней:

  • тестирование редьюсеров (состояние приложения)
  • тестирование селекторов (вычисляемые данные)
  • тестирование экшенов и middleware
  • тестирование React-компонентов
  • изоляция внешних зависимостей (Mapbox, deck.gl, window APIs)

Ключевой принцип — максимальная изоляция логики от графического слоя карты.


Подготовка тестового окружения

Для юнит-тестирования Kepler.gl чаще всего используется связка:

  • Jest как тестовый раннер
  • React Testing Library или Enzyme (в старых конфигурациях)
  • babel-jest для трансформации ES6/JSX
  • мокирование mapbox-gl, deck.gl, react-map-gl

Особенность Kepler.gl — невозможность запускать реальные WebGL контексты в unit-тестах, поэтому любые визуальные зависимости заменяются заглушками.

Пример базовой настройки моков:

jest.mock('mapbox-gl', () => ({
  Map: () => ({
    on: jest.fn(),
    remove: jest.fn(),
    addControl: jest.fn()
  }),
  NavigationControl: jest.fn()
}));

Также часто мокируется deck.gl:

jest.mock('@deck.gl/react', () => ({
  DeckGL: () => null
}));

Такая стратегия позволяет тестировать только логику, не затрагивая рендеринг WebGL сцены.


Тестирование Redux-редьюсеров

Редьюсеры — наиболее стабильная часть Kepler.gl, и именно они покрываются юнит-тестами в первую очередь.

Типичный редьюсер отвечает за:

  • добавление датасетов
  • обновление слоёв
  • управление фильтрами
  • синхронизацию UI состояния

Пример теста редьюсера загрузки данных:

import keplerGlReducer from 'kepler.gl/reducers';
import {actionTypes} from 'kepler.gl/actions';

test('should handle ADD_DATASET', () => {
  const initialState = undefined;

  const action = {
    type: actionTypes.ADD_DATASET,
    payload: {
      datasets: [{
        data: [{lat: 1, lng: 2}],
        info: {id: 'test'}
      }]
    }
  };

  const state = keplerGlReducer(initialState, action);

  expect(state.datasets).toHaveProperty('test');
});

Редьюсеры Kepler.gl часто вложенные, поэтому важно проверять не только верхний уровень состояния, но и глубоко вложенные структуры.


Тестирование селекторов

Селекторы в Kepler.gl используются для вычисления производных данных: подготовка данных для визуализации, фильтрация, агрегации.

Они особенно важны, поскольку напрямую влияют на производительность и корректность отображения карты.

Пример селектора:

import {filteredLayerDataSelector} from 'kepler.gl/selectors';

test('should return filtered data based on time filter', () => {
  const state = {
    keplerGl: {
      map: {
        datasets: {
          test: {
            data: [
              {time: 1},
              {time: 10}
            ]
          }
        },
        filters: [{
          field: 'time',
          value: [0, 5]
        }]
      }
    }
  };

  const result = filteredLayerDataSelector(state);

  expect(result.test.data.length).toBe(1);
});

Селекторы часто мемоизированы, поэтому в тестах важно учитывать:

  • неизменяемость входных данных
  • повторное использование ссылок
  • отсутствие побочных эффектов

Тестирование экшенов и middleware

Экшены в Kepler.gl могут быть как синхронными, так и асинхронными (например, загрузка данных, обработка файлов, взаимодействие с UI состоянием).

Тестирование экшенов обычно включает проверку:

  • корректности создаваемого action object
  • побочных эффектов middleware
  • правильной последовательности диспатчей

Пример теста экшена:

import {addDataToMap} from 'kepler.gl/actions';

test('addDataToMap should create correct action', () => {
  const action = addDataToMap({
    datasets: [{info: {id: 'test'}, data: []}]
  });

  expect(action.type).toBe('ADD_DATA_TO_MAP');
  expect(action.payload.datasets[0].info.id).toBe('test');
});

Middleware тестируется через mock store:

import configureStore from 'redux-mock-store';
import thunk from 'redux-thunk';

const mockStore = configureStore([thunk]);

test('dispatches ADD_DATASET after async action', async () => {
  const store = mockStore({});

  await store.dispatch(async (dispatch) => {
    dispatch({type: 'ADD_DATASET'});
  });

  const actions = store.getActions();
  expect(actions[0].type).toBe('ADD_DATASET');
});

Тестирование React-компонентов

UI-часть Kepler.gl включает компоненты управления слоями, фильтрами, панелью настроек и самой картой.

Основная сложность тестирования компонентов — зависимость от контекста Redux и внешних провайдеров.

Пример тестирования компонента с React Testing Library:

import {render} from '@testing-library/react';
import LayerPanel from 'kepler.gl/components';

test('LayerPanel renders without crashing', () => {
  const props = {
    layers: [],
    datasets: {}
  };

  const {container} = render(<LayerPanel {...props} />);
  expect(container).toBeDefined();
});

Для компонентов Kepler.gl часто требуется оборачивание в Provider:

import {Provider} from 'react-redux';
import configureStore from 'redux-mock-store';

const store = configureStore([])({keplerGl: {}});

render(
  <Provider store={store}>
    <LayerPanel />
  </Provider>
);

Мокирование WebGL и визуальных зависимостей

Kepler.gl использует Mapbox GL и deck.gl, которые невозможно запускать в стандартной тестовой среде Jest без эмуляции.

Основные подходы:

1. Полное заглушение рендера

jest.mock('react-map-gl', () => ({
  StaticMap: () => null,
  MapGL: () => null
}));

2. Частичное мокирование API Mapbox

global.URL.createObjectURL = jest.fn();
global.fetch = jest.fn(() =>
  Promise.resolve({
    json: () => Promise.resolve({})
  })
);

3. Замена WebGL контекста

HTMLCanvasElement.prototype.getContext = () => ({
  clearColor: jest.fn(),
  enable: jest.fn(),
  viewport: jest.fn()
});

Такая изоляция позволяет запускать тесты в CI без GPU окружения.


Тестирование структуры состояния Redux

Kepler.gl имеет сложную структуру state tree:

  • datasets
  • layers
  • filters
  • interaction
  • uiState

Проверка корректной инициализации состояния часто важнее, чем тестирование отдельных UI действий.

import keplerGlReducer from 'kepler.gl/reducers';

test('initial state structure is correct', () => {
  const state = keplerGlReducer(undefined, {type: '@@INIT'});

  expect(state).toHaveProperty('visState');
  expect(state).toHaveProperty('mapState');
  expect(state).toHaveProperty('uiState');
});

Тестирование кастомных утилит Kepler.gl

В библиотеке присутствуют утилиты для обработки данных:

  • парсинг CSV/JSON
  • нормализация координат
  • агрегация значений
  • трансформация форматов дат

Пример теста утилиты обработки координат:

import {processGeojson} from 'kepler.gl/utils';

test('processGeojson should normalize features', () => {
  const geojson = {
    type: 'FeatureCollection',
    features: [
      {
        type: 'Feature',
        geometry: {type: 'Point', coordinates: [10, 20]}
      }
    ]
  };

  const result = processGeojson(geojson);

  expect(result.features[0].geometry.coordinates).toEqual([10, 20]);
});

Организация тестов в проекте Kepler.gl

Структура тестов обычно зеркалирует структуру исходного кода:

/reducers/__tests__
/selectors/__tests__
/actions/__tests__
/components/__tests__
/utils/__tests__

Такой подход упрощает навигацию и поддержку тестового покрытия.

Дополнительно часто выделяется отдельная папка для:

  • integration tests
  • snapshot tests UI
  • mock fixtures (datasets, geojson, csv samples)

Snapshot-тестирование компонентов

Snapshot-тесты применяются для UI-компонентов, где важно сохранить стабильность визуальной структуры.

import renderer from 'react-test-renderer';

test('LayerManager snapshot', () => {
  const tree = renderer
    .create(<LayerManager layers={[]} />)
    .toJSON();

  expect(tree).toMatchSnapshot();
});

Однако в Kepler.gl snapshot-тесты требуют осторожности из-за частых изменений UI и зависимостей от состояния Redux.


Изоляция состояния при тестировании

Kepler.gl активно использует иммутабельные структуры, поэтому важно избегать мутаций исходного state в тестах.

Практика:

  • использовать deep clone для фикстур
  • избегать повторного использования объектов состояния
  • создавать фабрики тестовых данных
const createMockState = () => ({
  keplerGl: {
    map: {
      datasets: {}
    }
  }
});

Ошибки и типичные проблемы при тестировании Kepler.gl

  • попытка запускать реальные Mapbox GL контексты в Jest
  • отсутствие моков для window.URL и FileReader
  • нарушение иммутабельности state в тестах
  • нестабильные snapshot-тесты UI
  • неверная конфигурация Redux store

Корректная изоляция зависимостей является ключевым фактором стабильности тестов и их воспроизводимости.