Слой в Deck.gl представляет собой изолированную единицу визуализации, объединяющую логику подготовки данных, управление атрибутами, взаимодействие с WebGL и жизненный цикл обновлений. При модульном тестировании основное внимание сосредоточено не на фактическом рендеринге пикселей, а на проверке корректности вычислений, трансформаций данных и поведения слоя при изменении входных параметров.
Ключевой особенностью является то, что слой не существует в вакууме: он взаимодействует с WebGL-контекстом, шейдерами и системой управления атрибутами. Это делает полноценный unit-тест невозможным без изоляции графической среды.
Основные проверяемые аспекты:
initialize,
updateState, finalize)AttributeManagerКаждый слой Deck.gl наследуется от базового класса Layer
и реализует набор методов, которые формируют единый жизненный цикл.
Ключевые тестируемые точки:
Важно, что unit-тесты не должны зависеть от фактического WebGL-рендера, а лишь проверять корректность вызовов и состояния внутренних структур.
Основная проблема тестирования слоёв заключается в наличии WebGL-зависимостей. В unit-тестах используется замена реального контекста на заглушку.
Типовые подходы:
Создание минимального объекта, имитирующего поведение WebGL:
const glMock = {
createBuffer: jest.fn(() => ({})),
bindBuffer: jest.fn(),
bufferData: jest.fn(),
enable: jest.fn(),
disable: jest.fn(),
useProgram: jest.fn()
};
Использование headless-gl позволяет эмулировать
WebGL1/WebGL2 окружение:
import {createGLContext} from '@luma.gl/test-utils';
const gl = createGLContext({width: 1, height: 1});
В тестах часто создаётся минимальный контейнер слоя без реального canvas:
const layer = new MyLayer({
data: []
});
layer.context = {
gl: glMock,
deck: {}
};
Deck.gl слои обычно тестируются через Jest, поскольку он поддерживает:
Библиотека предоставляет утилиты для создания тестового окружения:
Пример:
import {testLayer} from '@deck.gl/test-utils';
test('MyLayer renders correctly', t => {
testLayer({
Layer: MyLayer,
props: {data: []},
onBeforeUpdate: ({layer}) => {
t.ok(layer instanceof MyLayer);
}
});
});
Инициализация слоя — ключевая точка проверки корректности конфигурации.
Проверяется:
test('initializes with default props', () => {
const layer = new MyLayer({data: null});
expect(layer.props.data).toEqual([]);
expect(layer.state).toBeDefined();
});
Особое внимание уделяется immutable-поведению props: слой не должен мутировать входные данные.
Метод updateState отвечает за реакцию слоя на изменения
данных и состояния.
Типовые сценарии:
props.dataprops.radius, props.colortest('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.setStatethis.invalidateAttributeAttributeManager является центральным компонентом подготовки данных для GPU.
Unit-тестирование включает:
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]);
Жизненный цикл слоя включает последовательность событий, которые должны быть детерминированы.
Основные шаги:
initializeStateupdateStatedrawfinalizeStatetest('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 не тестируется как рендер пикселей, но
проверяется:
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})
);
});
Метод getPickingInfo отвечает за обработку объектов при
взаимодействии.
Тестируются:
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 testing используется для фиксации:
test('layer snapshot', () => {
const layer = new MyLayer({
data: [1, 2, 3],
radius: 10
});
expect(layer).toMatchSnapshot();
});
Это позволяет отслеживать непреднамеренные изменения API слоя.
При создании кастомных слоёв основное внимание уделяется:
getShadersinitializeStateclass 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();
});
Часто встречающиеся проблемы:
Каждый слой должен создаваться заново, без переиспользования экземпляров.
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);
});
Система 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 передаёт слою глобальные параметры:
test('layer receives deck context', () => {
const layer = new MyLayer({data: []});
layer.context = {
viewport: {},
gl: {}
};
expect(layer.context.gl).toBeDefined();
});