Я унаследовал приличное количество интеграционных тестов, которые симулируют события мыши / касания для элементов DOM и периодически утверждают состояние приложения во время выполнения результирующего действия.
Общей проблемой в этих тестах является отказдождитесь полного завершения асинхронного поведения, приводящего к непреднамеренным побочным эффектам.
Примите во внимание следующее:
const LoadState = {
Pending: 'pending',
Loaded: 'loaded',
Failed: 'failed'
};
const App = {
state: {
loadState: LoadState.Pending,
renderAnimationFrameId: null
},
ui: {
loadState: document.getElementById('load-state'),
loadButton: document.getElementById('load-button')
},
initialize() {
this._load = this._load.bind(this);
this.ui.loadButton.addEventListener('click', this._load);
},
render() {
// Throttle rendering to framerate.
if (this.state.renderAnimationFrameId) {
return;
}
this.state.renderAnimationFrameId = requestAnimationFrame(() => {
this.ui.loadState.textContent = this.state.loadState;
this.state.renderAnimationFrameId = null;
});
},
_load() {
window.fetch(new Request('flowers.jpg')).then(({ ok }) => {
this.state.loadState = ok ? LoadState.Loaded : LoadState.Failed;
}).catch(() => {
this.state.loadState = LoadState.Failed;
}).finally(() => {
this.render();
});
}
};
App.initialize();
App.render();
<input id='load-button' type="button" value="Load Data" />
<div id='load-state'>
Pending
</div>
Предполагая ложную реализацию fetch
, я хотел бы убедиться, что load-button
правильно реагирует на нажатие и что App
корректно обновляется после этого.Примерно так написано довольно интуитивно:
window.fetch = Promise.resolve(true);
describe('App', () => {
App.initialize();
it('should load', (done) => {
App.ui.loadButton.dispatchEvent(new Event('click'));
// Wait for event to update state after fetch resolution.
setTimeout(() => {
expect(App.state.loadState === LoadState.Loaded);
done();
});
});
it('should render', (done) => {
App.render();
// Wait for throttled render
setTimeout(() => {
expect(App.state.renderAnimationFrameId === null);
expect(App.ui.loadState.textContent === LoadState.Loaded);
done();
});
});
});
Эти тесты вносят небольшую ошибку.Первый тест частично ожидал, но не полностью ожидал, чтобы асинхронная логика приложения установилась. Тест проходит, потому что state.loadState
является правильным, но вызов render
все еще в полете.Второй тест может показывать ошибочные результаты из-за этого.
Мне интересно, какие соответствующие ответы могут быть на это.Некоторые предлагаемые решения включают в себя:
Mock setTimeout
, setInterval
, requestAnimationFrame
и ставят в очередь их обратные вызовы вместо выполнения асинхронно.Разрешить синхронное воспроизведение обратных вызовов.Очень волшебное и подверженное ошибкам решение, обещания по-прежнему асинхронные и должны обрабатываться отдельно
Переписать производственный код, чтобы обеспечить публичное раскрытие всех ожидающих обещаний.Это делает использование async
и await
гораздо более сложным и интуитивно понятным.
Шпион setTimeout
, setInterval
, requestAnimationFrame
и Promise
, попытайтесь сделать выводкакое асинхронное поведение все еще ожидается в результате запуска метода, определите асинхронное разрешение путем опроса шпионов.Это менее сложно, чем выше, но условия гонки и сторонний код становятся более сложными для отладки, и неясно, как будет работать использование await
или yield
.
Может быть, естьесть другие варианты?По сути, неясно, должно ли приложение быть ответственным за правильное раскрытие асинхронной функциональности, или это бремя полностью ложится на среду тестирования.Если в среде тестирования имеет смысл полностью контролировать асинхронное поведение или предпочтителен строго реакционный подход?
TL; DR: обработчики событий мыши / касания являются асинхронными и не нуждаются в раскрытии своих внутренних объектов.Как правильно реагировать на потребности тестирования?