Unit-тесты для карт

Архитектура приложений с картографической составляющей почти всегда включает несколько уровней логики: инициализация карты, управление слоями, работа с объектами (маркер, полигон, линия), взаимодействие с геокодингом и маршрутизацией. Несмотря на то что сама карта является внешним визуальным компонентом, значительная часть бизнес-логики может и должна быть покрыта юнит-тестами.

Ключевая сложность тестирования HERE Maps API заключается в том, что большинство объектов завязаны на браузерный контекст и нативные визуальные рендереры WebGL. Это делает прямое тестирование невозможным без изоляции зависимостей.


Разделение логики и API-клиента

Первый шаг к тестируемости — отделение бизнес-логики от прямых вызовов HERE Maps API.

Антипаттерн: прямое использование API

// плохо тестируемый код
const platform = new H.service.Platform({
  apikey: 'YOUR_API_KEY'
});

const map = new H.Map(
  document.getElementById('map'),
  platform.createDefaultLayers().vector.normal.map,
  {
    center: { lat: 52.5, lng: 13.4 },
    zoom: 10
  }
);

Такой код невозможно протестировать без браузера и реального DOM.


Абстракция слоя карты

Правильный подход — создание обёртки над API.

export class MapService {
  constructor(mapInstance) {
    this.map = mapInstance;
  }

  setCenter(lat, lng) {
    this.map.setCenter({ lat, lng });
  }

  addMarker(marker) {
    this.map.addObject(marker);
  }

  removeObject(object) {
    this.map.removeObject(object);
  }
}

Теперь тестируется не HERE Maps, а собственная логика взаимодействия с ним.


Использование mock-объектов

Юнит-тесты должны исключать реальные зависимости от HERE Maps SDK.

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

export function createMockMap() {
  return {
    setCenter: jest.fn(),
    addObject: jest.fn(),
    removeObject: jest.fn(),
    getObjects: jest.fn(() => [])
  };
}

Тестирование управления центром карты

import { MapService } from './MapService';
import { createMockMap } from './mocks/createMockMap';

test('setCenter вызывает метод карты с корректными координатами', () => {
  const mockMap = createMockMap();
  const service = new MapService(mockMap);

  service.setCenter(10, 20);

  expect(mockMap.setCenter).toHaveBeenCalledWith({
    lat: 10,
    lng: 20
  });
});

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

В HERE Maps маркер часто создаётся через H.map.Marker, но в тестах он заменяется простым объектом.

function createMockMarker() {
  return {
    setGeometry: jest.fn(),
    getGeometry: jest.fn()
  };
}

Логика добавления маркера

test('addMarker добавляет объект на карту', () => {
  const map = createMockMap();
  const service = new MapService(map);
  const marker = createMockMarker();

  service.addMarker(marker);

  expect(map.addObject).toHaveBeenCalledWith(marker);
});

Тестирование бизнес-логики поверх карты

Часто карта используется как визуальный слой, а вся логика координат находится отдельно.

export function calculateCenter(points) {
  const sum = points.reduce(
    (acc, p) => {
      acc.lat += p.lat;
      acc.lng += p.lng;
      return acc;
    },
    { lat: 0, lng: 0 }
  );

  return {
    lat: sum.lat / points.length,
    lng: sum.lng / points.length
  };
}

Тестирование вычислений

test('calculateCenter возвращает среднее значение координат', () => {
  const points = [
    { lat: 0, lng: 0 },
    { lat: 10, lng: 10 }
  ];

  expect(calculateCenter(points)).toEqual({
    lat: 5,
    lng: 5
  });
});

Такая логика не зависит от HERE Maps и полностью покрывается юнит-тестами.


Изоляция геокодинга

HERE Geocoding API также не должен вызываться напрямую в юнит-тестах.

Обёртка сервиса

export class GeocodingService {
  constructor(client) {
    this.client = client;
  }

  async geocode(query) {
    const result = await this.client.geocode({ q: query });
    return result.items;
  }
}

Mock клиента

const mockClient = {
  geocode: jest.fn()
};

Тест

test('geocode возвращает items из ответа API', async () => {
  mockClient.geocode.mockResolvedValue({
    items: [{ title: 'Test location' }]
  });

  const service = new GeocodingService(mockClient);

  const result = await service.geocode('Berlin');

  expect(result).toEqual([{ title: 'Test location' }]);
});

Тестирование маршрутизации

Маршрутизация в HERE Maps часто строится через H.service.RoutingService.

Обёртка маршрута

export class RoutingServiceWrapper {
  constructor(routingService) {
    this.routingService = routingService;
  }

  async calculateRoute(start, end) {
    return new Promise((resolve, reject) => {
      this.routingService.calculateRoute(
        {
          origin: start,
          destination: end,
          return: 'polyline'
        },
        resolve,
        reject
      );
    });
  }
}

Mock маршрутизатора

const mockRoutingService = {
  calculateRoute: jest.fn()
};

Тестирование маршрута

test('calculateRoute вызывает routingService с правильными параметрами', async () => {
  mockRoutingService.calculateRoute.mockImplementation((params, cb) => {
    cb({ routes: ['route1'] });
  });

  const wrapper = new RoutingServiceWrapper(mockRoutingService);

  const result = await wrapper.calculateRoute('A', 'B');

  expect(result).toEqual({ routes: ['route1'] });
});

Проверка управления слоями карты

Карты часто используют слои: трафик, спутник, пользовательские данные.

export class LayerManager {
  constructor(map) {
    this.map = map;
    this.layers = new Map();
  }

  addLayer(name, layer) {
    this.layers.set(name, layer);
    this.map.addLayer(layer);
  }

  removeLayer(name) {
    const layer = this.layers.get(name);
    if (layer) {
      this.map.removeLayer(layer);
      this.layers.delete(name);
    }
  }
}

Тест слоя

test('addLayer добавляет слой и сохраняет его', () => {
  const map = {
    addLayer: jest.fn(),
    removeLayer: jest.fn()
  };

  const manager = new LayerManager(map);
  const layer = {};

  manager.addLayer('traffic', layer);

  expect(map.addLayer).toHaveBeenCalledWith(layer);
});

Подмена глобального объекта H

Если часть кода всё же использует глобальный H, его можно замокать.

global.H = {
  map: {
    Marker: jest.fn()
  },
  service: {
    Platform: jest.fn()
  }
};

Работа с jsdom

Для тестов, где требуется DOM-узел карты:

document.body.innerHTML = `
  <div id="map"></div>
`;
test('контейнер карты существует', () => {
  const container = document.getElementById('map');
  expect(container).not.toBeNull();
});

Проблемные зоны тестирования HERE Maps

1. WebGL-рендеринг

Невозможно тестировать в юнит-тестах, выносится на e2e уровень.

2. Асинхронные загрузки тайлов

Не покрываются unit-тестами, заменяются mock-слоями.

3. Визуальные изменения

Проверяются через snapshot/e2e, но не через unit.


Стратегия покрытия

Юнит-тесты эффективно покрывают:

  • преобразование координат
  • управление объектами карты
  • бизнес-логику поверх геоданных
  • работу сервисных обёрток
  • взаимодействие с mock API

Не покрываются:

  • рендеринг карты
  • визуальные артефакты
  • производительность WebGL
  • поведение браузерного движка карты