Unit тесты слоев

Слой в Deck.gl представляет собой изолированную единицу визуализации, объединяющую логику подготовки данных, управление атрибутами, взаимодействие с WebGL и жизненный цикл обновлений. При модульном тестировании основное внимание сосредоточено не на фактическом рендеринге пикселей, а на проверке корректности вычислений, трансформаций данных и поведения слоя при изменении входных параметров.

Ключевой особенностью является то, что слой не существует в вакууме: он взаимодействует с WebGL-контекстом, шейдерами и системой управления атрибутами. Это делает полноценный unit-тест невозможным без изоляции графической среды.

Основные проверяемые аспекты:

  • корректная инициализация слоя из props
  • предсказуемость жизненного цикла (initialize, updateState, finalize)
  • корректность генерации атрибутов через AttributeManager
  • реакция на изменение входных данных
  • поведение picking-логики
  • стабильность вычислений в pure-функциях слоя

Архитектура слоя и тестируемые точки

Каждый слой Deck.gl наследуется от базового класса Layer и реализует набор методов, которые формируют единый жизненный цикл.

Ключевые тестируемые точки:

  • constructor / props normalization — нормализация входных параметров
  • initializeState — первичная инициализация WebGL-структур
  • updateState — реакция на изменение props и state
  • draw — формирование команд рендера (без реального GPU-рендеринга)
  • getPickingInfo — обработка интерактивного выбора объектов
  • finalizeState — очистка ресурсов

Важно, что unit-тесты не должны зависеть от фактического WebGL-рендера, а лишь проверять корректность вызовов и состояния внутренних структур.


Изоляция WebGL-контекста

Основная проблема тестирования слоёв заключается в наличии WebGL-зависимостей. В unit-тестах используется замена реального контекста на заглушку.

Типовые подходы:

Mock WebGL context

Создание минимального объекта, имитирующего поведение WebGL:

const glMock = {
  createBuffer: jest.fn(() => ({})),
  bindBuffer: jest.fn(),
  bufferData: jest.fn(),
  enable: jest.fn(),
  disable: jest.fn(),
  useProgram: jest.fn()
};

headless-gl

Использование headless-gl позволяет эмулировать WebGL1/WebGL2 окружение:

import {createGLContext} from '@luma.gl/test-utils';

const gl = createGLContext({width: 1, height: 1});

Подмена DeckGL context

В тестах часто создаётся минимальный контейнер слоя без реального canvas:

const layer = new MyLayer({
  data: []
});

layer.context = {
  gl: glMock,
  deck: {}
};

Инструментарий тестирования

Jest как основа

Deck.gl слои обычно тестируются через Jest, поскольку он поддерживает:

  • мокирование модулей
  • snapshot testing
  • асинхронные тесты жизненного цикла

@deck.gl/test-utils

Библиотека предоставляет утилиты для создания тестового окружения:

  • создание mock-слоёв
  • симуляция жизненного цикла
  • проверка attribute manager

Пример:

import {testLayer} from '@deck.gl/test-utils';

test('MyLayer renders correctly', t => {
  testLayer({
    Layer: MyLayer,
    props: {data: []},
    onBeforeUpdate: ({layer}) => {
      t.ok(layer instanceof MyLayer);
    }
  });
});

Тестирование инициализации слоя

Инициализация слоя — ключевая точка проверки корректности конфигурации.

Проверяется:

  • нормализация props
  • установка дефолтных значений
  • создание AttributeManager
test('initializes with default props', () => {
  const layer = new MyLayer({data: null});

  expect(layer.props.data).toEqual([]);
  expect(layer.state).toBeDefined();
});

Особое внимание уделяется immutable-поведению props: слой не должен мутировать входные данные.


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

Метод updateState отвечает за реакцию слоя на изменения данных и состояния.

Типовые сценарии:

  • изменение props.data
  • изменение props.radius, props.color
  • изменение viewport или контекста
test('updateState responds to data change', () => {
  const layer = new MyLayer({data: [1, 2, 3]});

  layer.updateState({
    props: {data: [1, 2, 3, 4]},
    oldProps: {data: [1, 2, 3]}
  });

  expect(layer.state.needsUpdate).toBe(true);
});

Важный аспект — контроль флагов обновления:

  • this.setState
  • this.invalidateAttribute

AttributeManager и проверка атрибутов

AttributeManager является центральным компонентом подготовки данных для GPU.

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

  • проверку создания атрибутов
  • корректность buffer size
  • вызов update функций
  • пересчёт при изменении данных
test('attribute updates on data change', () => {
  const layer = new MyLayer({data: [1, 2]});

  const manager = layer.state.attributeManager;

  const updateSpy = jest.spyOn(manager, 'invalidateAll');

  layer.updateState({
    props: {data: [1, 2, 3]}
  });

  expect(updateSpy).toHaveBeenCalled();
});

Также проверяется, что accessor-функции корректно трансформируют данные:

const getPosition = d => d.position;

expect(getPosition({position: [10, 20]})).toEqual([10, 20]);

Тестирование жизненного цикла слоя

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

Основные шаги:

  • создание слоя
  • initializeState
  • updateState
  • draw
  • finalizeState
test('layer lifecycle executes in correct order', () => {
  const layer = new MyLayer({data: []});

  const initSpy = jest.spyOn(layer, 'initializeState');
  const updateSpy = jest.spyOn(layer, 'updateState');

  layer.initializeState();

  layer.updateState({
    props: {data: [1, 2, 3]}
  });

  expect(initSpy).toHaveBeenCalled();
  expect(updateSpy).toHaveBeenCalled();
});

Тестирование draw-команды

Метод draw не тестируется как рендер пикселей, но проверяется:

  • формирование uniforms
  • вызов шейдерных программ
  • корректность передаваемых моделей
test('draw passes correct uniforms', () => {
  const layer = new MyLayer({data: []});

  const model = {
    setUniforms: jest.fn()
  };

  layer.state.model = model;

  layer.draw({
    uniforms: {opacity: 0.5}
  });

  expect(model.setUniforms).toHaveBeenCalledWith(
    expect.objectContaining({opacity: 0.5})
  );
});

Picking и интерактивность

Метод getPickingInfo отвечает за обработку объектов при взаимодействии.

Тестируются:

  • корректность идентификации объекта
  • преобразование данных в pickable формат
  • возврат metadata
test('picking returns correct info', () => {
  const layer = new MyLayer({data: [{id: 1}]});

  const info = layer.getPickingInfo({
    object: {id: 1}
  });

  expect(info.object.id).toBe(1);
});

Особое внимание уделяется стабильности идентификаторов объектов.


Snapshot тестирование слоёв

Snapshot testing используется для фиксации:

  • структуры props
  • состояния слоя
  • конфигурации attribute manager
test('layer snapshot', () => {
  const layer = new MyLayer({
    data: [1, 2, 3],
    radius: 10
  });

  expect(layer).toMatchSnapshot();
});

Это позволяет отслеживать непреднамеренные изменения API слоя.


Тестирование кастомных слоёв

При создании кастомных слоёв основное внимание уделяется:

  • корректной реализации getShaders
  • правильной работе initializeState
  • управлению buffer lifecycle
class CustomLayer extends Layer {
  initializeState() {
    this.state = {
      model: null
    };
  }
}

Тесты проверяют, что слой не нарушает контракт базового класса:

test('custom layer respects base contract', () => {
  const layer = new CustomLayer({data: []});

  expect(typeof layer.draw).toBe('function');
  expect(layer.state).toBeDefined();
});

Типовые ошибки в unit-тестах слоёв

Часто встречающиеся проблемы:

  • тестирование реального WebGL вместо логики слоя
  • зависимость от порядка выполнения тестов
  • отсутствие изоляции state между тестами
  • проверка приватных GPU-эффектов вместо публичного API слоя
  • использование нестабильных данных без фиксации входных значений

Изоляция состояния между тестами

Каждый слой должен создаваться заново, без переиспользования экземпляров.

beforeEach(() => {
  layer = new MyLayer({data: []});
});

Это предотвращает утечки state между тестами AttributeManager и моделей.


Проверка детерминированности вычислений

Слои должны давать одинаковый результат при одинаковых входных данных.

test('deterministic output', () => {
  const layer1 = new MyLayer({data: [1, 2, 3]});
  const layer2 = new MyLayer({data: [1, 2, 3]});

  expect(layer1.props.data).toEqual(layer2.props.data);
});

Моделирование Attribute invalidation

Система invalidation позволяет минимизировать перерасчёт данных.

test('invalidates attributes on prop change', () => {
  const layer = new MyLayer({data: [1, 2]});

  layer.updateState({
    props: {data: [1, 2, 3]}
  });

  expect(layer.state.attributeManager.needsRedraw).toBe(true);
});

Проверка интеграции с deck context

Deck context передаёт слою глобальные параметры:

  • viewport
  • device pixel ratio
  • gl context
test('layer receives deck context', () => {
  const layer = new MyLayer({data: []});

  layer.context = {
    viewport: {},
    gl: {}
  };

  expect(layer.context.gl).toBeDefined();
});