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

Redux-архитектура в Kepler.gl строится вокруг предсказуемого состояния, где все изменения проходят через actions и reducers. Это делает тестирование логики ключевым этапом обеспечения стабильности визуализации, конфигурации слоёв и взаимодействий с картой.

Основные уровни тестирования Redux-части:

  • тестирование actions (создание и структура payload)
  • тестирование reducers (изменение состояния)
  • тестирование selectors (вычисляемые данные)
  • тестирование middleware и side-effects
  • интеграционные сценарии состояния Kepler.gl

Тестирование actions

Actions в Kepler.gl представляют собой чистые описания намерений. Их тестирование сводится к проверке корректности структуры возвращаемого объекта.

Типовой action:

export const addLayer = (config) => ({
  type: 'ADD_LAYER',
  payload: config
});

Тест проверяет:

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

Пример теста на Jest:

import { addLayer } from 'actions/layers';

describe('addLayer action', () => {
  test('should create correct action', () => {
    const config = { id: 'layer_1', type: 'point' };

    const action = addLayer(config);

    expect(action).toEqual({
      type: 'ADD_LAYER',
      payload: config
    });
  });
});

Особое внимание уделяется неизменяемости входных данных: action не должен мутировать переданный объект.


Тестирование reducers

Reducers — центральная часть Redux-логики Kepler.gl. Они управляют состоянием слоёв, фильтров, данных и UI-конфигурации.

Ключевые принципы тестирования reducers:

  • состояние должно оставаться иммутабельным
  • каждый action должен приводить к предсказуемому изменению state
  • неизвестные actions возвращают исходное состояние

Пример reducer:

const initialState = {
  layers: []
};

export default function layersReducer(state = initialState, action) {
  switch (action.type) {
    case 'ADD_LAYER':
      return {
        ...state,
        layers: [...state.layers, action.payload]
      };

    default:
      return state;
  }
}

Тестирование:

import layersReducer from 'reducers/layers';

describe('layersReducer', () => {
  test('should return initial state', () => {
    expect(layersReducer(undefined, {})).toEqual({
      layers: []
    });
  });

  test('should handle ADD_LAYER', () => {
    const initialState = { layers: [] };

    const action = {
      type: 'ADD_LAYER',
      payload: { id: 'layer_1' }
    };

    const nextState = layersReducer(initialState, action);

    expect(nextState).toEqual({
      layers: [{ id: 'layer_1' }]
    });
  });

  test('should not mutate state', () => {
    const state = { layers: [] };

    const action = {
      type: 'ADD_LAYER',
      payload: { id: 'layer_1' }
    };

    const nextState = layersReducer(state, action);

    expect(nextState).not.toBe(state);
    expect(state.layers.length).toBe(0);
  });
});

Проверка глубокой структуры состояния

Kepler.gl использует вложенные структуры: datasets, layers, interactionConfig, mapState. Это требует тестирования не только поверхностных изменений, но и глубокой неизменяемости.

Пример сценария:

const state = {
  mapState: {
    latitude: 0,
    longitude: 0,
    zoom: 10
  }
};

const action = {
  type: 'UPDATE_MAP_VIEW',
  payload: {
    latitude: 50,
    longitude: 30
  }
};

Тест:

expect(nextState.mapState).toEqual({
  latitude: 50,
  longitude: 30,
  zoom: 10
});

Проверяется частичное обновление без потери других полей.


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

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

Пример selector:

export const getVisibleLayers = (state) =>
  state.layers.filter(layer => layer.isVisible);

Тестирование selector:

import { getVisibleLayers } from 'selectors/layers';

describe('getVisibleLayers', () => {
  test('should return only visible layers', () => {
    const state = {
      layers: [
        { id: 1, isVisible: true },
        { id: 2, isVisible: false }
      ]
    };

    const result = getVisibleLayers(state);

    expect(result).toEqual([
      { id: 1, isVisible: true }
    ]);
  });
});

Selectors часто мемоизируются (reselect), поэтому дополнительно проверяется кэширование результатов при одинаковом входе.


Тестирование сложных reducers Kepler.gl

В Kepler.gl присутствуют комбинированные reducers, например:

  • visStateReducer
  • mapStateReducer
  • uiStateReducer

Они обрабатывают множество типов actions одновременно.

Пример:

test('visState should handle multiple layers actions', () => {
  let state = visStateReducer(undefined, {});

  state = visStateReducer(state, {
    type: 'ADD_LAYER',
    payload: { id: 'l1' }
  });

  state = visStateReducer(state, {
    type: 'UPDATE_LAYER_PROPS',
    payload: { id: 'l1', props: { opacity: 0.5 } }
  });

  expect(state.layers[0].opacity).toBe(0.5);
});

Здесь важна последовательность применения actions, так как состояние эволюционирует.


Тестирование middleware

Middleware в Redux-экосистеме Kepler.gl используется для:

  • логирования действий
  • синхронизации состояния карты
  • обработки асинхронных операций

Пример middleware:

const loggerMiddleware = store => next => action => {
  console.log(action.type);
  return next(action);
};

Тестирование middleware:

import configureMockStore from 'redux-mock-store';

const mockStore = configureMockStore([loggerMiddleware]);

test('middleware should pass action through', () => {
  const store = mockStore({});

  const action = { type: 'TEST_ACTION' };

  store.dispatch(action);

  const actions = store.getActions();
  expect(actions).toEqual([action]);
});

Асинхронная логика и thunk-тестирование

Kepler.gl может использовать thunk для загрузки данных и подготовки слоёв.

Пример async action:

export const loadData = () => async dispatch => {
  const data = await fetch('/data.json').then(res => res.json());

  dispatch({
    type: 'LOAD_DATA_SUCCESS',
    payload: data
  });
};

Тестирование:

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

const mockStore = configureMockStore([thunk]);

test('loadData dispatches success action', async () => {
  global.fetch = jest.fn(() =>
    Promise.resolve({
      json: () => Promise.resolve({ points: [] })
    })
  );

  const store = mockStore({});

  await store.dispatch(loadData());

  const actions = store.getActions();

  expect(actions[0]).toEqual({
    type: 'LOAD_DATA_SUCCESS',
    payload: { points: [] }
  });
});

Проверка неизменяемости состояния

Redux-архитектура Kepler.gl требует строгого соблюдения immutability. Для этого применяются:

  • deep freeze в тестах
  • сравнение ссылок объектов
  • snapshot-тестирование

Пример:

import deepFreeze from 'deep-freeze';

test('reducer must not mutate state', () => {
  const state = deepFreeze({
    layers: []
  });

  const action = {
    type: 'ADD_LAYER',
    payload: { id: '1' }
  };

  const nextState = layersReducer(state, action);

  expect(nextState.layers).not.toBe(state.layers);
});

Snapshot-тестирование Redux-состояния

Snapshot применяется для сложных структур Kepler.gl, где ручная проверка всех полей затруднена.

test('visState snapshot', () => {
  const state = visStateReducer(undefined, {
    type: 'INIT'
  });

  expect(state).toMatchSnapshot();
});

Снимки позволяют отслеживать непреднамеренные изменения структуры layers, filters и interactionConfig.


Тестирование комбинированного состояния приложения

Kepler.gl объединяет несколько reducers в rootReducer. Проверка взаимодействия между ними важна для стабильности UI.

Пример сценария:

  • добавление слоя
  • изменение фильтра
  • обновление mapState
test('combined redux flow', () => {
  let state = rootReducer(undefined, { type: '@@INIT' });

  state = rootReducer(state, {
    type: 'ADD_LAYER',
    payload: { id: 'l1' }
  });

  state = rootReducer(state, {
    type: 'SET_FILTER',
    payload: { id: 'f1', value: [0, 10] }
  });

  expect(state.visState.layers.length).toBe(1);
  expect(state.visState.filters.length).toBe(1);
});