Статьи

Модульное тестирование кода JavaScript, когда Ajax мешает

Предыдущая часть этой статьи здесь , но она не обязательна для понимания текущей темы.

Модульное тестирование — один из лучших инструментов, которые у меня есть в моей коробке, чтобы помочь в разработке моего кода. Тем не менее, я не всегда нахожусь в среде, где модульное тестирование легко: зрелые фреймворки JavaScript и изоляция тестов представляют те же проблемы других языков: глобальное состояние и внешние взаимодействия (плюс асинхронное поведение).

Одним из очевидных внешних взаимодействий являются Ajax-вызовы, выполняемые любой библиотекой, которую вы используете. Эта статья рассказывает о том, как мы получили возможность тестировать клиентский код JavaScript, заглушая вызовы Ajax; хотя затем мы перешли к сквозному подходу к тестированию по другой причине, по крайней мере, это ноу-хау будет сохранено здесь и не будет потеряно.

механика

JavaScript является своего рода открытым языком в смысле открытых классов Ruby. Вы можете заменить практически любой метод, предоставляемый общедоступным API:

jQuery.val = function() { ... };

Эта способность означает, что если мы знаем, какая библиотека вызывает вызываемый тестируемый код, мы можем протестировать ее, даже если она не была разработана с помощью Dependency Injection, как мы предпочитаем в PHP или Java. Поскольку части кода могут даже не находиться под нашим контролем, это часто единственный выбор.

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

Заглушка библиотеки звонков

Первый выбор для тестирования кода, который выполняет вызовы Ajax, — это заглушка используемого библиотечного метода. Если вы находитесь в 0,01% людей, которые непосредственно создают экземпляры объектов XMLHttpRequest, легко превратить это создание в метод, который вы можете позже заглушить.

Для jQuery целью является метод jQuery.ajax. В случае Ext JS это Ext.Ajax.request. Однако в последнем случае Ext.Ajax.request вызывается с большим количеством различных опций другими компонентами Ext JS, что будет сложно написать заглушку общего назначения.

Механика следующая:

  • сохранить старый метод в переменной ( var oldAjax = jQuery.ajax; )
  • Замените его закрытием, которое сохраняет обратный вызов в переменной. Это замыкание может проверять свои аргументы (становится своего рода насмешкой).
  • Используйте обратный вызов, чтобы передать обратно готовый ответ , написанный в буквальном виде JSON, XML или что-либо, что вы возвращаете со стороны сервера.
  • Восстановите старый метод (фаза разрыва) с новым назначением.

Заглушка XMLHttpRequest

Альтернативный способ заглушить вызовы Ajax — это остановить создание XMLHttpRequest. Поскольку XMLHttpRequest — это просто глобальный объект класса Function, его можно заменить. Более того, вам не нужно делать это вручную: Sinon — это библиотека JavaScript, которая делает именно то, что нам нужно . Это работает также в Проводнике, так как проверяет также ActiveXObject; он обеспечивает Api более высокого уровня, так что вам не нужно переписывать весь объект и все его методы.

Sinon предоставляет различные методы тестирования, которые позволяют использовать более одного рабочего процесса: список соответствий между URL-адресами и ответами или отдельные ожидания. Это просто файл .js, поэтому он будет работать с любой используемой средой тестирования:

var fakeXhr = sinon.useFakeXMLHttpRequest();
var requests = [];

fakeXhr.onCreate = function (xhr) {
    requests.push(xhr);
};

jQuery.ajax({ url: "/my/page" });
// this should go in an always executed tearDown method
fakeXhr.restore();

assertEquals("/my/page", this.requests[0].url);

Когда нужно смоделировать несколько вызовов Ajax, Api немного меняется:

var server = sinon.fakeServer.create();
server.respondWith("responseText content"); // also configurable by URL

var called = false;
callback = function(text) {
    assertEquals("responseText content", text);
    called = true;
    
};
jQuery.ajax({ url: "/my/page", success: callback });
server.respond();
// should go in the tearDown phase
server.restore();

assertTrue(called);

Вызов server.respond () устраняет асихронность теста: после этого вы уверены, что были вызваны функции обработчика Ajax. Вы можете продолжить делать утверждения или продолжить тестирование.

Ext JS в настоящее время требует большего шага, так как он проверяет с помощью опроса, завершен ли запрос, а не полагается на обратные вызовы (не спрашивайте меня, почему): setInterval также должен быть заглушен.

var oldSetInterval = setInterval;
callbacks = [];
setInterval = function (callback, delay) {
    callbacks.push(callback);
};
// stub and call code under test as before...
setInterval = oldSetInterval;
for (i in callbacks) {
    callbacks[i].call();
}
// assertions...

На этот раз callbacks [i] .call () избегает асинхронности: обработчики Ext, которые должны проверять текст и код ответа, вызываются сразу после выполнения Ajax-заглушки.
Игра с setTimeout, setInterval и другими временными функциями может быть опасной, если среда тестирования не поддерживает ее; это может вызывать их для внутренних целей, например, чтобы избежать бесконечного теста. Эта проблема была главной в jsTestDriver, но была решена в 2009 году : теперь jsTestDriver создает свои собственные копии setInterval и других чувствительных функций перед началом запуска тестовых случаев. Если вы используете jsTestDriver, вы можете играть с этими глобальными функциями.

Выводы

Тестирование кода JavaScript всегда возможно, и когда задействовано больше логики, чем Ajax-вызов, заглушка помогает изолировать тестируемую систему. Тестирование с использованием реальных вызовов Ajax невозможно даже в моей любимой среде jsTestDriver, поскольку код подается с веб-сервера на локальном хосте, а не из реального приложения. Действительно, решения, описанные в этой статье, подходят для модульного тестирования, а не сквозного или функционального.