Эта статья была рецензирована Марком Брауном и MarcTowler . Спасибо всем рецензентам SitePoint за то, что сделали контент SitePoint как можно лучше!
Один из самых больших камней преткновения при написании модульных тестов — что делать, когда у вас нетривиальный код.
В реальных проектах код часто делает все, что затрудняет тестирование. Ajax-запросы, таймеры, даты, доступ к другим функциям браузера … или, если вы используете Node.js, базы данных всегда интересны, как и доступ к сети или файлу.
Все это сложно проверить, потому что вы не можете контролировать их в коде. Если вы используете Ajax, вам нужен сервер для ответа на запрос, чтобы тесты прошли успешно. Если вы используете setTimeout
, ваш тест должен будет ждать. С базами данных или сетями это одно и то же — вам нужна база данных с правильными данными или сетевой сервер.
Реальная жизнь не так проста, как многие учебные пособия по тестированию. Но знаете ли вы, что есть решение?
Используя Sinon, мы можем сделать тестирование нетривиальным кодом тривиальным!
Давайте узнаем, как.
Что делает Синон таким важным и полезным?
Проще говоря, Sinon позволяет вам заменить сложные части ваших тестов чем-то, что делает тестирование простым.
При тестировании фрагмента кода вы не хотите, чтобы на него влияло что-либо вне теста. Если что-то внешнее влияет на тест, тест становится намного более сложным и может случайно произойти.
Если вы хотите проверить код, совершающий Ajax-вызов, как вы можете это сделать? Вам нужно запустить сервер и убедиться, что он дает точный ответ, необходимый для вашего теста. Он сложен в настройке и затрудняет написание и запуск модульных тестов.
А что если ваш код зависит от времени? Допустим, он ждет одну секунду, прежде чем что-то делать. Что теперь? Вы можете использовать setTimeout
в своем тесте, чтобы подождать одну секунду, но это замедляет тест. Представьте, что интервал был больше, например, пять минут. Я предполагаю, что вы, вероятно, не хотите ждать пять минут при каждом запуске тестов.
Используя Sinon, мы можем решить обе эти проблемы (плюс многие другие) и устранить их сложность.
Как работает Синон?
Sinon помогает устранить сложность в тестах, позволяя легко создавать так называемые тест-двойники .
Тест-двойники, как следует из названия, заменяют фрагменты кода, используемые в ваших тестах. Оглядываясь на пример Ajax, вместо настройки сервера мы заменили бы вызов Ajax на test-double. В примере со временем мы использовали бы тест-двойники, чтобы позволить нам «двигаться вперед во времени».
Это может звучать немного странно, но основная концепция проста. Поскольку JavaScript очень динамичен, мы можем взять любую функцию и заменить ее чем-то другим. Тест-двойники просто развивают эту идею немного дальше. С помощью Sinon мы можем заменить любую функцию JavaScript на test-double, который затем можно настроить для выполнения различных задач, чтобы упростить тестирование сложных вещей.
Синон делит тест-двойники на три типа:
- Шпионы , которые предлагают информацию о вызовах функций, не влияя на их поведение
- Заглушки , которые похожи на шпионов, но полностью заменяют функцию. Это позволяет заставить функцию-заглушку делать все что угодно — генерировать исключение, возвращать определенное значение и т. Д.
- Насмешки , которые облегчают замену целых объектов, объединяя как шпионов, так и окурков
Кроме того, Синон также предоставляет некоторые другие помощники, хотя они выходят за рамки этой статьи:
- Поддельные таймеры , которые можно использовать для перемещения вперед во времени, например, для запуска
setTimeout
- Поддельный XMLHttpRequest и сервер , который можно использовать для подделки запросов и ответов Ajax
С помощью этих функций Sinon позволяет вам решать все сложные проблемы, которые вызывают внешние зависимости в ваших тестах. Если вы освоите приемы эффективного использования Sinon, вам не понадобятся другие инструменты.
Установка Синона
Прежде всего нам нужно установить Sinon.
Для тестирования Node.js:
- Установите Sinon через npm, используя
npm install sinon
- Require Sinon в вашем тесте с
var sinon = require('sinon');
Для тестирования на основе браузера:
- Вы можете установить Sinon через npm с помощью npm
npm install sinon
, использовать CDN или загрузить его с веб-сайтаnpm install sinon
- Включите
sinon.js
на страницу вашего бегуна.
Начиная
Sinon обладает множеством функциональных возможностей, но большая часть его строится поверх самого себя. Вы узнаете об одной части, и вы уже знаете о следующей. Это делает Sinon простым в использовании, когда вы изучите основы и узнаете, что делает каждая отдельная часть.
Обычно нам нужен Sinon, когда наш код вызывает функцию, которая доставляет нам проблемы.
С Ajax это может быть $.get
или XMLHttpRequest
. Со временем функция может быть setTimeout
. С базами данных это может быть mongodb.findOne
.
Чтобы было проще говорить об этой функции, я буду называть ее зависимостью . Функция, которую мы тестируем, зависит от результата другой функции.
Можно сказать, что базовый шаблон использования с Sinon состоит в замене проблемной зависимости на test-double .
- При тестировании Ajax мы заменяем
XMLHttpRequest
на test-double, который делает вид, что делает запрос Ajax - При тестировании времени мы заменяем
setTimeout
таймером - При тестировании доступа к базе данных мы могли бы заменить
mongodb.findOne
на test-double, который немедленно возвращает некоторые поддельные данные
Давайте посмотрим, как это работает на практике.
Шпионы
Шпионы — самая простая часть Sinon, и над ними строится другая функциональность.
Основное использование шпионов — сбор информации о вызовах функций. Вы также можете использовать их, чтобы помочь проверить вещи, например, была ли вызвана функция или нет.
var spy = sinon.spy(); //We can call a spy like a function spy('Hello', 'World'); //Now we can get information about the call console.log(spy.firstCall.args); //output: ['Hello', 'World']
Функция sinon.spy
возвращает объект Spy
, который может быть вызван как функция, но также содержит свойства с информацией о любых вызовах, сделанных к нему. В приведенном выше firstCall
свойство firstCall
содержит информацию о первом вызове, например firstCall.args
который представляет собой список переданных аргументов.
Хотя вы можете создавать анонимных шпионов, как описано выше, вызывая sinon.spy
без параметров, более распространенным sinon.spy
является замена другой функции шпионом.
var user = { ... setName: function(name){ this.name = name; } } //Create a spy for the setName function var setNameSpy = sinon.spy(user, 'setName'); //Now, any time we call the function, the spy logs information about it user.setName('Darth Vader'); //Which we can see by looking at the spy object console.log(setNameSpy.callCount); //output: 1 //Important final step - remove the spy setNameSpy.restore();
Замена другой функции шпионом работает аналогично предыдущему примеру, но с одним важным отличием: когда вы закончили использовать шпион, важно не забыть восстановить исходную функцию, как в последней строке примера выше. Без этого ваши тесты могут плохо себя вести.
У шпионов много разных свойств, которые предоставляют разную информацию о том, как они использовались. Шпионская документация Синона содержит полный список всех доступных опций.
На практике вы не можете использовать шпионов очень часто. Скорее всего, вам понадобится заглушка, но шпионам может быть удобно, например, проверить, был ли вызван обратный вызов:
function myFunction(condition, callback){ if(condition){ callback(); } } describe('myFunction', function() { it('should call the callback function', function() { var callback = sinon.spy(); myFunction(true, callback); assert(callback.calledOnce); }); });
В этом примере я использую Mocha в качестве основы тестирования и Chai в качестве библиотеки утверждений. Если вы хотите узнать больше об одном из них, пожалуйста, обратитесь к моей предыдущей статье: модульное тестирование вашего JavaScript с помощью Mocha и Chai .
Утверждения Синона
Прежде чем мы продолжим и поговорим о заглушках, давайте сделаем небольшой обход и посмотрим на утверждения Синона .
В большинстве случаев тестирования со шпионами (и заглушками) вам нужен какой-то способ проверки результата теста.
Мы можем использовать любое утверждение для проверки результатов. В предыдущем примере с обратным вызовом мы использовали функцию assert
Чая, которая обеспечивает достоверность значения.
assert(callback.calledOnce);
Проблема в том, что сообщение об ошибке в случае сбоя неясно. Вам просто скажут: «Ложь не была правдой» или что-то в этом роде. Как вы, вероятно, можете себе представить, не очень полезно выяснять, что пошло не так, и вам нужно взглянуть на исходный код теста, чтобы выяснить это. Не смешно.
Чтобы решить эту проблему, мы можем включить в утверждение пользовательское сообщение об ошибке.
assert(callback.calledOnce, 'Callback was not called once');
Но зачем беспокоиться, когда мы можем использовать собственные утверждения Синона ?
describe('myFunction', function() { it('should call the callback function', function() { var callback = sinon.spy(); myFunction(true, callback); sinon.assert.calledOnce(callback); }); });
Использование подобных утверждений Синона дает нам гораздо лучшее сообщение об ошибке из коробки. Это становится очень полезным, когда вам нужно проверить более сложные условия, такие как параметры функции.
Вот несколько примеров других полезных утверждений, представленных Синоном:
-
sinon.assert.calledWith
может использоваться для проверки того, что функция была вызвана с определенными параметрами (это, вероятно, тот, который я использую чаще всего) -
sinon.assert.callOrder
может проверить, что функции были вызваны в определенном порядке.
Как и в случае со шпионами, в документации по утверждению Синона есть все доступные опции. Если вам нравится использовать Chai, есть также плагин sinon-chai , который позволяет вам использовать утверждения Sinon через интерфейс expect
или выбора Chai.
Столбики
Окурки — двойник теста из-за их гибкости и удобства. У них есть все функции шпионов, но вместо того, чтобы просто следить за тем, что делает функция, заглушка полностью заменяет ее. Другими словами, при использовании шпиона исходная функция все еще выполняется, но при использовании заглушки — нет.
Это делает заглушки идеальными для ряда задач, таких как:
- Замена Ajax или других внешних вызовов, которые делают тесты медленными и трудными для записи
- Запуск разных путей кода в зависимости от вывода функции
- Проверка необычных условий, например, что происходит, когда выдается исключение?
Мы можем создавать окурки так же, как шпионы …
var stub = sinon.stub(); stub('hello'); console.log(stub.firstCall.args); //output: ['hello']
Мы можем создавать анонимные заглушки, как со шпионами, но заглушки становятся действительно полезными, когда вы используете их для замены существующих функций.
Например, если у нас есть некоторый код, который использует функциональные возможности jQuery Ajax, протестировать его будет сложно. Код отправляет запрос на любой сервер, который мы настроили, поэтому нам нужно, чтобы он был доступен, или добавить специальный код в код, чтобы не делать этого в тестовой среде — что является большим нет-нет. В вашем коде почти никогда не должно быть специфичных для теста случаев.
Вместо того, чтобы прибегать к плохим практикам, мы можем использовать Sinon и заменить функциональность Ajax заглушкой. Это делает тестирование тривиальным.
Вот пример функции, которую мы протестируем. Он принимает объект в качестве своего параметра и отправляет его через Ajax на предварительно определенный URL-адрес.
function saveUser(user, callback) { $.post('/users', { first: user.firstname, last: user.lastname }, callback); }
Обычно это сложно проверить из-за вызова Ajax и предварительно определенного URL, но если мы используем заглушку, это становится легко.
Допустим, мы хотим убедиться, что функция обратного вызова, переданная saveUser
вызывается правильно после завершения запроса.
describe('saveUser', function() { it('should call callback after saving', function() { //We'll stub $.post so a request is not sent var post = sinon.stub($, 'post'); post.yields(); //We can use a spy as the callback so it's easy to verify var callback = sinon.spy(); saveUser({ firstname: 'Han', lastname: 'Solo' }, callback); post.restore(); sinon.assert.calledOnce(callback); }); });
Здесь мы заменим функцию Ajax заглушкой. Это означает, что запрос никогда не отправляется, и нам не нужен сервер или что-либо еще — мы имеем полный контроль над тем, что происходит в нашем тестовом коде!
Поскольку мы хотим убедиться, что обратный вызов, который мы передаем в saveUser
будет вызван, мы дадим команду заглушке уступить . Это означает, что заглушка автоматически вызывает первую функцию, переданную ей в качестве параметра. Это имитирует поведение $.post
, который будет вызывать обратный вызов после завершения запроса.
В дополнение к заглушке мы создаем шпиона в этом тесте. Мы могли бы использовать нормальную функцию в качестве обратного вызова, но использование шпиона позволяет легко проверить результат теста с sinon.assert.calledOnce
утверждения sinon.assert.calledOnce
‘s sinon.assert.calledOnce
.
В большинстве случаев, когда вам нужна заглушка, вы можете следовать той же базовой схеме:
- Найдите проблемную функцию, такую как
$.post
- Посмотрите, как это работает, чтобы вы могли подражать в тесте
- Создать заглушку
- Установите заглушку так, чтобы в вашем тесте было то поведение, которое вы хотите
Заглушка не должна имитировать каждое поведение. Необходимо только то поведение, которое вам нужно для теста, и все остальное можно не учитывать.
Другое распространенное использование заглушек — проверка того, что функция была вызвана с определенным набором аргументов.
Например, для нашей функциональности Ajax мы хотим убедиться, что отправляются правильные значения. Следовательно, мы можем получить что-то вроде:
describe('saveUser', function() { it('should send correct parameters to the expected URL', function() { //We'll stub $.post same as before var post = sinon.stub($, 'post'); //We'll set up some variables to contain the expected results var expectedUrl = '/users'; var expectedParams = { first: 'Expected first name', last: 'Expected last name' }; //We can also set up the user we'll save based on the expected data var user = { firstname: expectedParams.first, lastname: expectedParams.last } saveUser(user, function(){} ); post.restore(); sinon.assert.calledWith(post, expectedUrl, expectedParams); }); });
Опять же, мы создаем заглушку для $.post()
, но на этот раз мы не устанавливаем ее для yield. Этот тест не заботится о обратном вызове, поэтому иметь его выход не нужно.
Мы настроили некоторые переменные, содержащие ожидаемые данные — URL и параметры. Хорошей практикой является установка таких переменных, поскольку это позволяет легко увидеть, каковы требования для теста. Это также помогает нам настроить user
переменную без повторения значений.
На этот раз мы использовали утверждение sinon.assert.calledWith()
. Мы передаем заглушку в качестве первого параметра, потому что на этот раз мы хотим убедиться, что заглушка была вызвана с правильными параметрами.
Есть также другой способ тестирования Ajax-запросов в Sinon. Это с помощью поддельной функциональности Sinon XMLHttpRequest. Здесь мы не будем вдаваться в подробности, но если вы хотите узнать, как это работает, посмотрите мою статью о Ajax-тестировании с поддельным XMLHttpRequest от Sinon .
Mocks
Насмешки — это другой подход к заглушкам. Если вы слышали термин «фиктивный объект», то это то же самое — макеты Синона можно использовать для замены целых объектов и изменения их поведения, аналогично функциям заглушки.
Они в первую очередь полезны, если вам нужно заглушить более одной функции из одного объекта. Если вам нужно заменить только одну функцию, заглушку проще использовать.
Вы должны быть осторожны при использовании издевательств! Благодаря их возможностям легко сделать ваши тесты чрезмерно специфичными — тестировать слишком много и слишком специфических вещей — что может сделать ваши тесты непреднамеренно хрупкими.
В отличие от шпионов и окурков, у насмешек есть встроенные утверждения. Вы заранее определяете ожидаемые результаты, сообщая фиктивному объекту, что должно произойти, а затем вызывая функцию проверки в конце теста.
Допустим, мы используем store.js для сохранения вещей в localStorage, и мы хотим протестировать функцию, связанную с этим. Мы можем использовать макет, чтобы помочь протестировать его следующим образом:
describe('incrementStoredData', function() { it('should increment stored value by one', function() { var storeMock = sinon.mock(store); storeMock.expects('get').withArgs('data').returns(0); storeMock.expects('set').once().withArgs('data', 1); incrementStoredData(); storeMock.restore(); storeMock.verify(); }); });
При использовании насмешек мы определяем ожидаемые вызовы и их результаты, используя свободный стиль вызовов, как показано выше. Это то же самое, что использование утверждений для проверки результатов теста, за исключением того, что мы определяем их storeMock.verify()
, и для их проверки мы вызываем storeMock.verify()
в конце теста.
В терминологии фиктивных объектов Синона, вызов mock.expects('something')
создает ожидание . mock.something()
метод mock.something()
должен быть вызван. Каждое ожидание, в дополнение к специальным функциям, поддерживает те же функции, что и шпионы и заглушки.
Вы можете обнаружить, что часто намного проще использовать заглушку, чем макет — и это прекрасно. Насмешки следует использовать с осторожностью.
Для полного списка макет-специфических функций, проверьте макет документации Синона .
Важная рекомендация: используйте sinon.test ()
Есть одна важная лучшая практика с Sinon, которую следует помнить при использовании шпионов, пней или издевательств.
Если вы замените существующую функцию на test-double, используйте sinon.test()
.
В предыдущем примере мы использовали stub.restore()
или mock.restore()
для очистки после их использования. Это необходимо, поскольку в противном случае тест-двойник остается на месте и может отрицательно повлиять на другие тесты или вызвать ошибки.
Но использование функции restore()
напрямую проблематично. Возможно, что тестируемая функция вызывает ошибку и завершает тестовую функцию перед restore()
!
У нас есть два способа решить эту проблему: мы можем обернуть все это в блок try catch
. Это позволяет нам поместить вызов restore()
в блок finally
, гарантируя, что он будет запущен независимо от того, что.
Или, лучше, мы можем обернуть тестовую функцию с помощью sinon.test()
it('should do something with stubs', sinon.test(function() { var stub = this.stub($, 'post'); doSomething(); sinon.assert.calledOnce(stub); });
Обратите внимание, что в приведенном выше примере второй параметр it()
заключен в sinon.test()
. Второе замечание — мы используем this.stub()
вместо sinon.stub()
.
Обтекание теста с помощью sinon.test()
позволяет нам использовать функцию песочницы Sinon , позволяющую нам создавать шпионов, заглушек и издевательства с помощью this.spy()
, this.stub()
и this.mock()
. Любые двойники, которые вы создаете с помощью песочницы, очищаются автоматически .
Обратите внимание, что в приведенном выше примере кода нет функции stub.restore()
— он не нужен благодаря тестовой среде.
Если вы используете sinon.test()
там, где это возможно, вы можете избежать проблем, когда тесты начинают sinon.test()
сбой случайно, потому что более ранний тест не sinon.test()
из-за ошибки.
Синон не волшебство
Синон делает много вещей, и иногда может показаться трудным понять, как это работает. Давайте посмотрим на некоторые простые примеры JavaScript, как работает Sinon, чтобы мы могли лучше понять, что он делает под капотом. Это поможет вам использовать его более эффективно в разных ситуациях.
Мы можем создавать шпионов, окурков и издевательства тоже вручную. Причина, по которой мы используем Sinon, заключается в том, что она делает задачу тривиальной — создание их вручную может быть довольно сложным, но давайте посмотрим, как это работает, чтобы понять, что делает Sinon.
Во-первых, шпион по сути является оберткой функции:
//A simple spy helper function createSpy(targetFunc) { var spy = function() { spy.args = arguments; spy.returnValue = targetFunc.apply(this, arguments); return spy.returnValue; }; return spy; } //Let's spy on a simple function: function sum(a, b) { return a + b; } var spiedSum = createSpy(sum); spiedSum(10, 5); console.log(spiedSum.args); //Output: [10, 5] console.log(spiedSum.returnValue); //Output: 15
Мы можем довольно легко получить шпионскую функциональность с помощью такой функции. Но обратите внимание, что шпионы Синона предоставляют гораздо более широкий набор функций, включая поддержку утверждений. Это делает Синона намного удобнее.
Что насчет заглушки тогда?
Чтобы сделать действительно простую заглушку, вы можете просто заменить функцию новой:
var stub = function() { }; var original = thing.otherFunction; thing.otherFunction = stub; //Now any calls to thing.otherFunction will call our stub instead
Но опять же, есть несколько преимуществ окурков Синона:
- У них есть полная функциональность шпиона в них
- Вы можете легко восстановить исходное поведение с помощью
stub.restore()
- Вы можете утверждать против окурков Sinon
В издевательствах просто совмещается поведение шпионов и окурков, что позволяет использовать их функции по-разному.
Даже несмотря на то, что иногда Sinon может показаться «волшебным», это можно сделать довольно легко с помощью вашего собственного кода, по большей части. Sinon просто намного удобнее в использовании, чем необходимость писать собственную библиотеку для этой цели.
Вывод
Тестирование реального кода иногда может показаться слишком сложным, и от него легко отказаться. Но с помощью Sinon тестирование практически любого вида кода становится легким делом.
Просто запомните основной принцип: если функция затрудняет написание вашего теста, попробуйте заменить его на test-double. Этот принцип применяется независимо от того, что делает функция.
Хотите узнать больше о том, как применить Sinon с вашим собственным кодом? Зайдите на мой сайт, и я пришлю вам бесплатный Sinon в реальном руководстве , которое включает лучшие практики Sinon и три реальных примера того, как применять его в различных типах тестирования!