Интеграционное тестирование

Особенности архитектуры Kepler.gl, влияющие на тестирование

Kepler.gl построен как надстройка над стеком React + Redux + deck.gl + Mapbox GL, где ключевая логика визуализации распределена между несколькими слоями:

  • UI-компоненты React отвечают за взаимодействие пользователя
  • Redux-слой хранит состояние карты (datasets, layers, filters, interaction)
  • deck.gl выполняет рендеринг геопространственных слоёв
  • Mapbox GL обеспечивает базовую подложку карты и управление viewport

Такая архитектура делает интеграционное тестирование критически важным, поскольку изолированные unit-тесты не отражают поведение системы в реальном сценарии: данные проходят через Redux, трансформируются в визуальные слои и только затем отображаются.


Цели интеграционного тестирования

Интеграционные тесты Kepler.gl направлены на проверку связей между:

  • Redux store и React-компонентами
  • состоянием visState и визуализацией слоёв
  • действиями пользователя и изменением карты
  • загрузкой данных и их преобразованием в геослои
  • взаимодействием фильтров и результирующей выборкой

Ключевая задача — убедиться, что цепочка “данные → состояние → визуализация” работает целостно.


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

Для тестирования Kepler.gl требуется эмуляция нескольких браузерных и графических API.

Основной стек тестирования

  • Jest — выполнение тестов
  • React Testing Library — работа с компонентами
  • redux-mock-store или реальный Redux store
  • jest-canvas-mock — эмуляция Canvas/WebGL
  • identity-obj-proxy — обработка CSS модулей

Установка зависимостей:

npm install --save-dev jest @testing-library/react @testing-library/jest-dom
npm install --save-dev jest-canvas-mock redux-mock-store

Инициализация окружения Jest

// setupTests.js
import '@testing-library/jest-dom';
import 'jest-canvas-mock';

Мокирование Mapbox GL и WebGL

Kepler.gl активно использует WebGL через deck.gl, поэтому без моков тесты падают.

Базовый мок Mapbox GL

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

Мок canvas контекста

HTMLCanvasElement.prototype.getContext = () => {
  return {
    fillRect: jest.fn(),
    clearRect: jest.fn(),
    getImageData: jest.fn(),
    putImageData: jest.fn(),
    createImageData: jest.fn(),
    setTransform: jest.fn(),
    drawImage: jest.fn()
  };
};

Интеграция Redux store в тестах

Kepler.gl хранит состояние в структуре:

  • visState — слои, фильтры, взаимодействие
  • mapState — положение камеры
  • uiState — интерфейс

Создание тестового store:

import { createStore, combineReducers } from 'redux';
import keplerGlReducer from 'kepler.gl/reducers';

const reducers = combineReducers({
  keplerGl: keplerGlReducer
});

export function configureTestStore(initialState) {
  return createStore(reducers, initialState);
}

Тестирование загрузки данных

Один из ключевых сценариев — проверка добавления датасета и его обработки.

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

const sampleData = {
  datasets: {
    info: {
      label: 'Test Data',
      id: 'test'
    },
    data: {
      fields: [
        { name: 'lat', type: 'real' },
        { name: 'lng', type: 'real' }
      ],
      rows: [
        [55.75, 37.61],
        [59.93, 30.31]
      ]
    }
  }
};

test('добавление данных в Kepler.gl store', () => {
  const store = configureTestStore();

  store.dispatch(addDataToMap(sampleData));

  const state = store.getState();
  const dataset = state.keplerGl.map.visState.datasets;

  expect(Object.keys(dataset)).toContain('test');
});

Проверка создания слоёв

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

test('создание слоя после добавления данных', () => {
  const store = configureTestStore();

  store.dispatch(addDataToMap(sampleData));

  const layers = store.getState().keplerGl.map.visState.layers;

  expect(layers.length).toBeGreaterThan(0);
  expect(layers[0]).toHaveProperty('config');
});

Интеграция фильтров и состояние visState

Фильтры — одна из самых сложных частей системы, так как они влияют на GPU-рендеринг через deck.gl.

import { addFilter, setFilter } from 'kepler.gl/actions';

test('добавление и изменение фильтра', () => {
  const store = configureTestStore();

  store.dispatch(addDataToMap(sampleData));

  store.dispatch(addFilter('test'));

  store.dispatch(setFilter(0, 'value', [10, 20]));

  const filters = store.getState().keplerGl.map.visState.filters;

  expect(filters.length).toBe(1);
  expect(filters[0].value).toEqual([10, 20]);
});

Проверка взаимодействия viewport и mapState

Kepler.gl синхронизирует положение карты через mapState.

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

test('обновление viewport карты', () => {
  const store = configureTestStore();

  store.dispatch(updateMap({
    latitude: 55.75,
    longitude: 37.61,
    zoom: 10
  }));

  const mapState = store.getState().keplerGl.map.mapState;

  expect(mapState.latitude).toBe(55.75);
  expect(mapState.zoom).toBe(10);
});

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

Интеграционный тест React-компонента включает проверку подключения store и рендера.

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

test('рендер KeplerGl компонента', () => {
  const store = configureTestStore();

  const { container } = render(
    <KeplerGl
      id="map"
      width={800}
      height={600}
      mapboxApiAccessToken="test-token"
    />, 
    { wrapper: ({ children }) => (
      <Provider store={store}>{children}</Provider>
    )}
  );

  expect(container.querySelector('canvas')).toBeTruthy();
});

Тестирование взаимодействия слоёв (Layer interaction)

Слои Kepler.gl могут перекрываться и взаимодействовать через picking system deck.gl.

test('наличие интерактивности слоёв', () => {
  const store = configureTestStore();

  store.dispatch(addDataToMap(sampleData));

  const layers = store.getState().keplerGl.map.visState.layers;

  const layer = layers[0];

  expect(layer.config).toHaveProperty('visConfig');
  expect(layer.config.isVisible).toBe(true);
});

Проверка экспорта данных

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

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

test('экспорт конфигурации карты', () => {
  const store = configureTestStore();

  store.dispatch(addDataToMap(sampleData));

  const config = store.getState().keplerGl.map;

  const exported = exportToJson(config);

  expect(exported).toHaveProperty('version');
  expect(exported).toHaveProperty('config');
});

End-to-End тестирование сценариев

Интеграционного уровня тестов часто недостаточно для проверки полной цепочки взаимодействий, поэтому используются E2E инструменты.

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

describe('Kepler.gl E2E', () => {
  it('загрузка данных и отображение слоя', () => {
    cy.visit('/');

    cy.get('input[type="file"]').attachFile('data.csv');

    cy.get('.layer-panel').should('exist');

    cy.get('canvas').should('be.visible');
  });
});

Тестирование производительности рендера

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

test('обработка большого датасета', () => {
  const store = configureTestStore();

  const largeDataset = {
    datasets: {
      info: { id: 'big' },
      data: {
        fields: [{ name: 'lat' }, { name: 'lng' }],
        rows: Array.from({ length: 10000 }, (_, i) => [i % 90, i % 180])
      }
    }
  };

  store.dispatch(addDataToMap(largeDataset));

  const layers = store.getState().keplerGl.map.visState.layers;

  expect(layers.length).toBeGreaterThan(0);
});

Тестирование синхронизации состояния UI и Redux

UI Kepler.gl зависит от синхронизации uiState и visState.

test('синхронизация UI состояния', () => {
  const store = configureTestStore();

  store.dispatch({
    type: 'TOGGLE_SIDE_PANEL'
  });

  const uiState = store.getState().keplerGl.map.uiState;

  expect(uiState).toBeDefined();
});

Проверка кастомных расширений и плагинов

Kepler.gl позволяет расширять поведение через плагины и кастомные слои.

test('регистрация кастомного слоя', () => {
  const customLayer = {
    name: 'CustomLayer',
    type: 'custom'
  };

  const store = configureTestStore();

  store.dispatch({
    type: 'ADD_LAYER',
    layer: customLayer
  });

  const layers = store.getState().keplerGl.map.visState.layers;

  expect(layers.find(l => l.name === 'CustomLayer')).toBeTruthy();
});