Моки и стабы API

При разработке приложений, использующих HERE Technologies JavaScript API, значительная часть логики оказывается связанной с внешними сервисами: загрузкой тайлов, геокодированием, маршрутизацией, поиском объектов и авторизацией. Это делает прямое выполнение тестов нестабильным из-за сетевых задержек, квот, токенов доступа и изменчивости данных.

Моки и стабы становятся ключевым инструментом изоляции прикладной логики от инфраструктурных зависимостей. В тестовой среде они заменяют реальные HTTP-запросы и объекты карты предсказуемыми заглушками.


Проблематика интеграции с HERE Maps API

В стандартной архитектуре HERE Maps JavaScript API используется объектная модель:

  • H.service.Platform — инициализация платформы
  • H.Map — отображение карты
  • H.service.GeocodingService — геокодирование
  • H.service.RoutingService — построение маршрутов
  • H.map.Object и производные — визуальные сущности

Каждый из этих компонентов зависит от внешних данных и сети.

Основные проблемы при тестировании:

  • нестабильность сетевых запросов
  • необходимость валидного API key
  • зависимость от тарифных ограничений
  • различие ответов API во времени
  • невозможность офлайн-исполнения
  • сложность воспроизведения редких сценариев (ошибки, пустые ответы)

Разделение моков и стабов в контексте картографических API

Стабы (stubs)

Стабы заменяют функции API фиксированным поведением. В контексте HERE Maps они применяются для:

  • геокодирования
  • маршрутизации
  • поиска объектов
  • получения тайлов

Стаб возвращает заранее подготовленный ответ без логики.

Моки (mocks)

Моки расширяют стабы, добавляя:

  • проверку вызовов
  • контроль параметров
  • фиксацию количества обращений
  • валидацию последовательности вызовов

Моки применяются для проверки взаимодействия с API, а не только результата.


Моки и стабирование H.service.Platform

Базовый объект платформы часто подменяется на уровне инициализации.

Пример стабированной платформы

class PlatformStub {
  constructor() {}

  getSearchService() {
    return {
      geocode: (params, success) => {
        success({
          items: [
            {
              position: { lat: 51.5074, lng: -0.1278 },
              address: { label: "London" }
            }
          ]
        });
      }
    };
  }

  getRoutingService() {
    return {
      calculateRoute: (params, success) => {
        success({
          routes: [
            {
              sections: [
                {
                  travelSummary: {
                    duration: 3600,
                    length: 10000
                  }
                }
              ]
            }
          ]
        });
      }
    };
  }
}

Данный подход позволяет полностью исключить HTTP-слой из тестов.


Подмена геокодирования

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

Стаб геокодера

const GeocodingServiceStub = {
  geocode: (query, onSuccess) => {
    const responses = {
      "Berlin": {
        items: [
          {
            title: "Berlin",
            position: { lat: 52.52, lng: 13.405 }
          }
        ]
      },
      "unknown": {
        items: []
      }
    };

    onSuccess(responses[query.q] || responses["unknown"]);
  }
};

Использование такой схемы позволяет тестировать:

  • обработку пустых результатов
  • выбор первого результата
  • fallback-логики интерфейса

Стабирование маршрутизации

Маршрутизация требует более сложной структуры данных, поскольку возвращает многосекционные маршруты.

Фиксированный маршрут

const RoutingServiceStub = {
  calculateRoute: (params, onSuccess, onError) => {
    const route = {
      routes: [
        {
          sections: [
            {
              polyline: "encoded-polyline",
              travelSummary: {
                duration: 1800,
                length: 5000
              }
            }
          ]
        }
      ]
    };

    onSuccess(route);
  }
};

Имитация ошибки маршрута

const RoutingServiceErrorStub = {
  calculateRoute: (params, onSuccess, onError) => {
    onError({
      message: "ROUTE_NOT_FOUND"
    });
  }
};

Такой подход используется для проверки устойчивости UI к ошибкам сервиса.


Моки карты H.Map

Объект карты часто требует DOM и WebGL, что делает его сложным для unit-тестов.

Базовый мок карты

class MapMock {
  constructor(container, options) {
    this.container = container;
    this.options = options;
    this.objects = [];
    this.zoomLevel = options.zoom;
  }

  setCenter(coords) {
    this.center = coords;
  }

  setZoom(zoom) {
    this.zoomLevel = zoom;
  }

  addObject(obj) {
    this.objects.push(obj);
  }

  removeObject(obj) {
    this.objects = this.objects.filter(o => o !== obj);
  }
}

Такой мок позволяет тестировать:

  • добавление маркеров
  • изменение центра карты
  • изменение уровня масштаба

без рендеринга графики.


Подмена объектов маркеров и слоёв

Маркерные слои и объекты часто стабируются отдельно.

Мок маркера

class MarkerMock {
  constructor(coords, options) {
    this.coords = coords;
    this.options = options;
  }

  setGeometry(coords) {
    this.coords = coords;
  }
}

Мок слоя

class LayerMock {
  constructor() {
    this.objects = [];
  }

  addObject(obj) {
    this.objects.push(obj);
  }

  removeObject(obj) {
    this.objects = this.objects.filter(o => o !== obj);
  }
}

Использование Jest для стабов HERE Maps

В тестовых фреймворках стабирование часто реализуется через подмену модулей.

Пример с Jest

jest.mock("h-maps-sdk", () => {
  return {
    Platform: class {
      getSearchService() {
        return {
          geocode: (params, cb) =>
            cb({
              items: [{ position: { lat: 10, lng: 10 } }]
            })
        };
      }
    }
  };
});

Подобная схема позволяет полностью контролировать поведение SDK.


Sinon для мокирования сервисов

Sinon применяется для создания spy-объектов и контроля вызовов.

Пример spy на routing service

import sinon from "sinon";

const routingService = {
  calculateRoute: () => {}
};

const spy = sinon.spy(routingService, "calculateRoute");

routingService.calculateRoute({ from: "A", to: "B" });

console.log(spy.calledOnce);

Использование spy позволяет проверять:

  • количество вызовов
  • параметры вызова
  • порядок вызовов

Mock Service Worker для HTTP-уровня

При использовании REST API HERE можно применять MSW для перехвата запросов.

Пример handler для геокодинга

import { rest } from "msw";

export const handlers = [
  rest.get("/geocode", (req, res, ctx) => {
    return res(
      ctx.json({
        items: [
          {
            title: "Mock City",
            position: { lat: 0, lng: 0 }
          }
        ]
      })
    );
  })
];

MSW обеспечивает тестирование без изменения бизнес-кода.


Стратегии построения тестовой архитектуры

Изоляция слоёв

Разделение системы на уровни:

  • UI слой
  • слой работы с картой
  • сервисный слой API
  • транспортный слой HTTP

Каждый слой получает собственные стабы.


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

Фиксирование структуры ответов HERE API:

  • геокодинг: items[]
  • маршрутизация: routes[]
  • поиск: results[]

Любые моки должны строго соответствовать контракту.


Фиксация данных

Использование стабильных наборов данных:

  • координаты городов
  • маршруты между точками
  • адресные результаты

Это снижает вероятность ложных падений тестов.


Ошибочные сценарии и их моделирование

Для повышения устойчивости приложения стабами моделируются:

  • таймауты
  • пустые ответы
  • ошибки авторизации
  • превышение квоты
  • некорректные координаты

Пример таймаута

const TimeoutStub = {
  geocode: (params, success, error) => {
    setTimeout(() => {
      error({ message: "TIMEOUT" });
    }, 0);
  }
};

Подмена тайлов и визуальных слоёв

Карты используют тайлы, которые можно стабировать на уровне URL.

Пример заглушки тайлового сервера

const tileStub = (x, y, z) => {
  return `data:image/png;base64,FAKE_TILE_${x}_${y}_${z}`;
};

Такой подход используется для UI-тестов без сетевой нагрузки.


Интеграция моков в CI/CD

В непрерывной интеграции стабирование обеспечивает:

  • детерминированные тесты
  • отсутствие внешних зависимостей
  • ускорение прогонов
  • воспроизводимость ошибок

Обычно конфигурация включает:

  • отключение реального API ключа
  • включение mock layer
  • фиксацию seed данных

Архитектурные паттерны для мокирования HERE Maps

Dependency Injection

API оборачивается в сервис:

class MapService {
  constructor(platform) {
    this.platform = platform;
  }

  geocode(query) {
    return new Promise(resolve => {
      this.platform.getSearchService().geocode(query, resolve);
    });
  }
}

Это позволяет подменять platform в тестах.


Adapter Layer

Создание промежуточного слоя между HERE API и приложением:

  • нормализация ответов
  • скрытие специфики SDK
  • унификация структур данных

Repository Pattern

Изоляция источника данных:

  • MapRepository
  • RouteRepository
  • GeoRepository

Каждый репозиторий получает стабированный транспорт.


Типовые ошибки при мокировании

  • чрезмерное копирование структуры API без абстракции
  • отсутствие синхронизации моков с реальными контрактами
  • смешивание моков и бизнес-логики
  • использование моков в интеграционных тестах вместо unit-тестов
  • неполное покрытие ошибок API

Практика организации тестовой базы

Структура обычно включает:

  • __mocks__/here/
  • stubs/geocoding.js
  • stubs/routing.js
  • fixtures/cities.json
  • fixtures/routes.json

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


Поведение асинхронных API в стабах

HERE Maps активно использует callback-ориентированные API, что требует имитации асинхронности.

Асинхронный стаб

const asyncGeocodeStub = {
  geocode: (params, success) => {
    Promise.resolve().then(() => {
      success({
        items: [{ position: { lat: 1, lng: 1 } }]
      });
    });
  }
};

Это позволяет сохранять поведение, близкое к реальному SDK.