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

Работа с геоданными в JavaScript редко ограничивается вызовом одной функции. В реальных приложениях операции над геометрией выстраиваются в цепочки: фильтрация объектов, вычисление расстояний, построение буферных зон, пересечения слоёв, агрегация результатов. Библиотека Turf.js предоставляет набор низкоуровневых функций, каждая из которых решает локальную задачу, но ценность появляется именно при их композиции. Интеграционное тестирование в таком контексте становится ключевым инструментом проверки корректности всей геопространственной логики, а не отдельных примитивов.


Особенности интеграционного тестирования геоалгоритмов

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

Ключевые сложности:

  • Накопление погрешности вычислений при последовательных операциях
  • Нестабильность геометрий при работе с плавающей точкой
  • Различие проекций и координатных систем
  • Проблемные случаи на границах (антимеридиан, полюса)
  • Непредсказуемое поведение при самопересечениях и невалидных полигонах

Интеграционные тесты должны учитывать, что результат — это не просто число или объект, а пространственная структура с топологией.


Организация тестовых сценариев

Типичная интеграция функций Turf.js включает последовательность операций:

  • создание геометрий (polygon, point, lineString)
  • пространственные операции (buffer, intersect, union)
  • вычисления (area, distance, centroid)
  • фильтрация (booleanPointInPolygon, booleanWithin)

Пример цепочки геообработки

import * as turf from "@turf/turf";

const cityBoundary = turf.polygon([
  [
    [30.1, 59.9],
    [30.5, 59.9],
    [30.5, 60.1],
    [30.1, 60.1],
    [30.1, 59.9]
  ]
]);

const schools = turf.featureCollection([
  turf.point([30.2, 60.0]),
  turf.point([30.4, 60.05]),
  turf.point([30.6, 60.0])
]);

const bufferZone = turf.buffer(cityBoundary, 2, { units: "kilometers" });

const schoolsInZone = schools.features.filter((school) =>
  turf.booleanPointInPolygon(school, bufferZone)
);

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


Подготовка тестовых данных

Геопространственные тесты требуют заранее подготовленных GeoJSON-фикстур. В отличие от обычных unit-тестов, синтетические данные часто недостаточны.

Используются три типа наборов:

1. Геометрически простые случаи

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

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

2. Реалистичные данные

  • границы городов
  • дорожные сети
  • полигоны водоёмов

Используются для проверки приближённого поведения к реальному миру.

3. Крайние случаи

  • самопересекающиеся полигоны
  • мультиполигоны с дырками
  • объекты на 180° меридиане

Эти данные выявляют ошибки, которые не проявляются в стандартных сценариях.


Проверка стабильности цепочек операций

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

Пример теста на стабильность буфера

import * as turf from "@turf/turf";

test("buffer + simplify сохраняет площадь в допустимой погрешности", () => {
  const poly = turf.polygon([[
    [0, 0],
    [0, 1],
    [1, 1],
    [1, 0],
    [0, 0]
  ]]);

  const buffered = turf.buffer(poly, 5, { units: "kilometers" });
  const simplified = turf.simplify(buffered, { tolerance: 0.01 });

  const originalArea = turf.area(buffered);
  const simplifiedArea = turf.area(simplified);

  expect(Math.abs(originalArea - simplifiedArea)).toBeLessThan(originalArea * 0.05);
});

Здесь проверяется не точное совпадение, а допустимое отклонение. Это принципиально важно для геоалгоритмов.


Интеграция пространственных предикатов

Многие сценарии строятся вокруг логических операций:

  • внутри ли точка полигона
  • пересекаются ли линии
  • содержит ли одна геометрия другую

Пример сложного предиката

const river = turf.lineString([
  [30.1, 59.95],
  [30.5, 60.05]
]);

const bridges = turf.featureCollection([
  turf.point([30.2, 59.98]),
  turf.point([30.3, 60.01])
]);

const zone = turf.buffer(river, 1, { units: "kilometers" });

const validBridges = bridges.features.filter((b) =>
  turf.booleanPointInPolygon(b, zone)
);

Интеграционный тест должен проверять согласованность буферной зоны с предикатом включения. Ошибка на любом этапе приводит к ложным результатам фильтрации.


Работа с погрешностями и допусками

Геометрические вычисления почти всегда содержат численные ошибки. Поэтому тестирование должно опираться на допуски.

Подходы к проверке:

  • сравнение площадей с epsilon
  • сравнение координат с округлением
  • проверка топологии вместо точных значений
function almostEqual(a, b, epsilon = 1e-6) {
  return Math.abs(a - b) < epsilon;
}

Особенно важно это при работе с:

  • intersect
  • difference
  • union

Эти операции могут возвращать геометрии с незначительными вариациями координат.


Проверка топологической корректности

Интеграционные тесты должны учитывать не только форму, но и структуру геометрии.

Типовые проверки:

  • отсутствие самопересечений
  • корректность замыкания полигонов
  • отсутствие пустых геометрий после операций

Пример проверки:

const result = turf.intersect(polyA, polyB);

expect(result).not.toBeNull();
expect(result.geometry.type).toBe("Polygon");

const coords = result.geometry.coordinates[0];
expect(coords[0]).toEqual(coords[coords.length - 1]);

Сценарии с несколькими слоями данных

Реальные приложения часто работают с несколькими слоями:

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

Пример многослойной интеграции

const coverageArea = turf.union(regionA, regionB);

const filteredPOI = poi.features.filter((p) =>
  turf.booleanPointInPolygon(p, coverageArea)
);

const routesInArea = routes.features.map((r) =>
  turf.lineIntersect(r, coverageArea)
);

Интеграционные тесты в таких сценариях проверяют согласованность всех слоёв после трансформаций.


Проверка работы с большими наборами данных

При увеличении количества объектов возникают новые классы проблем:

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

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

  • измерение времени выполнения
  • контроль памяти
  • сравнение агрегированных результатов
console.time("large dataset");

const result = points.features.map((p) =>
  turf.nearestPoint(p, referencePoints)
);

console.timeEnd("large dataset");

Обработка некорректных геометрий

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

Примеры проблем:

  • незамкнутые полигоны
  • дублирующиеся координаты
  • NaN значения
  • перепутанный порядок координат

Тест должен проверять поведение системы:

  • выбрасывается ли ошибка
  • возвращается ли null
  • происходит ли автоматическая коррекция
const invalidPoly = turf.polygon([[
  [0, 0],
  [1, 1],
  [1, 0]
]]);

expect(() => turf.area(invalidPoly)).not.toThrow();

Проверка поведения на границах координатной системы

Особое внимание требуется к:

  • переходу через 180° долготы
  • полярным областям
  • глобальным буферам
const datelinePoly = turf.polygon([[
  [179, 10],
  [-179, 10],
  [-179, 20],
  [179, 20],
  [179, 10]
]]);

const buffered = turf.buffer(datelinePoly, 100, { units: "kilometers" });

expect(buffered).toBeDefined();

Такие тесты часто выявляют ошибки, которые не проявляются в локальных сценариях.


Композиция тестов как геоалгоритм

Интеграционные тесты для Turf.js фактически повторяют саму природу библиотеки: последовательную трансформацию геометрий.

Типичная структура теста:

  • входные GeoJSON данные
  • цепочка Turf-функций
  • нормализация результата
  • проверка топологии и метрик

Чем сложнее цепочка операций, тем важнее контроль промежуточных состояний, особенно при работе с буферами, пересечениями и объединениями слоёв.