Статьи

Развертывание инструмента тестирования JavaScript: Sinon.js против testdouble.js

Два греческих воина со щитами боевых жуков

При модульном тестировании реального кода существует множество ситуаций, затрудняющих написание тестов. Как проверить, была ли вызвана функция? Как вы тестируете Ajax-вызов? Или код с использованием setTimeout ? Вот когда вы используете удвоение теста — заменяющий код, который затрудняет тестирование.

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

Недавно новая библиотека, метко названная testdouble.js , делает волны. Он имеет такой же набор функций, как Sinon.js, с некоторыми отличиями здесь и там.

В этой статье мы рассмотрим, что предлагают и Sinon.js, и testdouble.js, и сравним их достоинства и недостатки. Будет ли Sinon.js оставаться лучшим выбором, или претендент получит приз?

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

Терминология, используемая в этой статье

Чтобы было легко понять, о чем идет речь, вот краткий обзор используемой терминологии. Это определения для Sinon.js, и они могут немного отличаться в других местах.

  • Двойной тест — это замена функции, используемой во время теста. Это может относиться к любому из трех типов, упомянутых ниже.
  • Шпион — это двойной тест, который позволяет проверять эффекты, не влияя на поведение целевой функции.
  • Заглушка — это тестовый двойник, который заменяет поведение целевой функции чем-то другим, например, возвращает значение.
  • Насмешка — это другой подход к заглушкам. Моксы содержат встроенную проверку и могут использоваться вместо отдельного утверждения.

Следует отметить, что одной из целей testdouble.js является уменьшение путаницы между терминами этого типа.

Краткий обзор Sinon.js и testdouble.js

Давайте начнем с того, как сравнить Sinon.js и testdouble.js в базовом использовании.

У Синона есть три отдельных понятия для тестовых двойников: шпионы, заглушки и издевательства. Идея заключается в том, что каждый представляет свой сценарий использования. Это делает библиотеку более знакомой тем, кто приезжает из других языков или кто читал книги, используя ту же терминологию, такую ​​как xUnit Test Patterns . Но другая сторона заключается в том, что эти три понятия также могут затруднить понимание Синона при первом его использовании.

Вот основной пример использования Sinon:

 //Here's how we can see a function call's parameters: var spy = sinon.spy(Math, 'abs'); Math.abs(-10); console.log(spy.firstCall.args); //output: [ -10 ] spy.restore(); //Here's how we can control what a function does: var stub = sinon.stub(document, 'createElement'); stub.returns('not an html element'); var x = document.createElement('div'); console.log(x); //output: 'not an html element' stub.restore(); 

Напротив, testdouble.js выбирает API, который является более простым. Вместо использования таких понятий, как шпионы или заглушки, он использует язык, гораздо более знакомый разработчикам JavaScript, такой как td.function , td.object и td.replace . Это делает testdouble потенциально легче подобрать и лучше подходит для определенных задач. Но с другой стороны, некоторые более продвинутые варианты использования могут быть вообще невозможны (что иногда является преднамеренным).

Вот как выглядит testdouble.js:

 //Here's how we can see a function call's parameters: var abs = td.replace(Math, 'abs'); Math.abs(-10); var explanation = td.explain(abs); console.log(explanation.calls[0].args); //output: [ -10 ] //Here's how we can control what a function does: var createElement = td.replace(document, 'createElement'); td.when(createElement(td.matchers.anything())).thenReturn('not an html element'); var x = document.createElement('div'); console.log(x); //output: 'not an html element' //testdouble resets all testdoubles with one call, no need for separate cleanup td.reset(); 

Язык, используемый testdouble, более прост. Мы «заменяем» функцию, а не «заглушаем» ее. Мы просим testdouble «объяснить» функцию, чтобы получить от нее информацию. Кроме этого, пока он довольно похож на Синона.

Это также распространяется на создание «анонимных» двойников теста:

 var x = sinon.stub(); 

против

 var x = td.function(); 

Шпионы и окурки Синона имеют свойства, которые предлагают больше информации о них. Например, Sinon предоставляет такие свойства, как stub.callCount и stub.args . В случае testdouble мы получаем эту информацию из td.explain :

 //we can give a name to our test doubles as well var x = td.function('hello'); x('foo', 'bar'); td.explain(x); console.log(x); /* Output: { name: 'hello', callCount: 1, calls: [ { args: ['foo', 'bar'], context: undefined } ], description: 'This test double `hello` has 0 stubbings and 1 invocations.\n\nInvocations:\n - called with `("foo", "bar")`.', isTestDouble: true } */ 

Одно из самых больших различий связано с тем, как вы настраиваете свои заглушки и проверки. С Sinon вы цепляете команды после заглушки и используете утверждение для проверки результата. testdouble.js просто показывает, как вы хотите, чтобы функция вызывалась, или как «репетировать» вызов функции.

 var x = sinon.stub(); x.withArgs('hello', 'world').returns(true); var y = sinon.stub(); sinon.assert.calledWith(y, 'foo', 'bar'); 

против

 var x = td.function(); td.when(x('hello', 'world')).thenReturn(true); var y = td.function(); td.verify(y('foo', 'bar')); 

Это может облегчить понимание API testdouble, так как вам не нужно знать, какие операции вы можете связать и когда.

Сравнение общих задач тестирования более подробно

На высоком уровне обе библиотеки достаточно похожи. Но как насчет типичных задач тестирования, которые вам могут понадобиться в реальном проекте? Давайте рассмотрим несколько случаев, когда различия начинают проявляться.

testdouble.js не имеет шпионов

Первое, что следует отметить, это testdouble.js, в котором нет понятия «шпион». В то время как Sinon.js позволяет нам заменять вызов функции, чтобы мы получали от нее информацию, сохраняя поведение функции по умолчанию, это вообще невозможно с testdouble.js. Когда вы заменяете функцию на testdouble, она всегда теряет свое поведение по умолчанию.

Это не обязательно проблема, однако. Самым распространенным способом использования шпионов было бы их использование для проверки того, что были вызваны обратные вызовы, что легко сделать с помощью функции td.function :

 var spy = sinon.spy(); myAsyncFunction(spy); sinon.assert.calledOnce(spy); 

против

 var spy = td.function(); myAsyncFunction(spy); td.verify(spy()); 

Хотя это и не большая проблема, все же хорошо знать, что между этими двумя понятиями есть разница, так как в противном случае вы можете быть удивлены, если ожидаете, что сможете использовать шпионов более специфичным образом с testdouble.js.

testdouble.js требует более точного ввода

Второе отличие, с которым вы столкнетесь, состоит в том, что testdouble более строго относится к входам.

И заглушки, и утверждения Синона позволяют вам быть неточными относительно заданных параметров. Это проще всего проиллюстрировать на примере:

 var stub = sinon.stub(); stub.withArgs('hello').returns('foo'); console.log(stub('hello', 'world')); //output: 'foo' sinon.assert.calledWith(stub, 'hello'); //no error 

против

 var stub = td.function(); td.when(stub('hello')).thenReturn('foo'); console.log(stub('hello', 'world')); //output: undefined td.verify(stub('hello')); //throws error! 

По умолчанию Sinon не заботится о том, сколько дополнительных параметров дано функции. Хотя он предоставляет такие функции, как sinon.assert.calledWithExactly , они не предлагаются в документации по умолчанию. Такие функции, как stub.withArgs также не имеют «точного» варианта.

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

Можно разрешить указание произвольных параметров в testdouble.js, это просто не по умолчанию:

 //tell td to ignore extra arguments entirely td.when(stub('hello'), { ignoreExtraArgs: true }).thenReturn('foo'); 

С ignoreExtraArgs: true поведение аналогично Sinon.js

testdouble.js имеет встроенную поддержку Promise

Хотя использование обещаний с Sinon.js несложно, в testdouble.js есть встроенные методы для возврата и отклонения обещаний.

 var stub = sinon.stub(); stub.returns(Promise.resolve('foo')); //or stub.returns(Promise.reject('foo')); 

против

 var stub = td.function(); td.when(stub()).thenResolve('foo'); //or td.when(stub()).thenReject('foo'); 

Примечание : в Sinon 1.x можно включить аналогичные вспомогательные функции с помощью функции sinon-as-обещано . Sinon 2.0 и новее включают поддержку обещаний в виде stub.resolves и stub.rejects

Поддержка обратного вызова testdouble.js более надежна

И Sinon, и testdouble обеспечивают простой способ вызвать функцию-заглушку с обратным вызовом. Тем не менее, они имеют некоторые различия в том, как они работают.

stub.yields использует stub.yields чтобы заглушка stub.yields первую функцию, которую она получает в качестве параметра.

 var x = sinon.stub(); x.yields('a', 'b'); //callback1 is called with 'a' and 'b' x(callback1, callback2); 

По умолчанию в testdouble.js используется шаблон в стиле узла, где обратный вызов считается последним параметром. Вам также не нужно указывать это при репетиции вызова:

 var x = td.function(); td.when(x(td.matchers.anything())).thenCallback('a', 'b'); //callback2 is called with 'a' and 'b' x(callback1, callback2); 

Что делает поддержку обратного вызова testdouble более надежной, так это то, что вы можете легко определить поведение для сценариев с несколькими обратными вызовами или в тех случаях, когда обратные вызовы находятся в другом порядке.

Предположим, что мы хотим вместо этого вызвать callback1

 var x = td.function(); td.when(x(td.callback, td.matchers.anything())).thenCallback('a', 'b'); //callback1 is called with 'a' and 'b' x(callback1, callback2); 

Обратите внимание, что мы передали td.callback как первый параметр функции в td.when . Это сообщает testdouble, какой параметр является обратным вызовом, который мы хотим использовать.

С Sinon возможно изменить и поведение:

 var x = sinon.stub(); x.callsArgWith(1, 'a', 'b'); //callback1 is called with 'a' and 'b' x(callback1, callback2); 

В этом случае мы используем callsArgWith вместо yields . Мы должны предоставить конкретный индекс вызова, чтобы он работал, что может быть довольно сложным, особенно для функций со многими параметрами.

Что если мы хотим вызвать оба обратных вызова с некоторыми значениями?

 var x = td.function(); td.when(x(td.callback('a', 'b'), td.callback('foo', 'bar'))).thenReturn(); //callback1 is called with 'a' and 'b' //callback2 is called with 'foo' and 'bar' x(callback1, callback2); 

С Синоном это невозможно вообще. Вы можете callsArgWith несколько вызовов к callsArgWith , но он будет вызывать только один из них.

testdouble.js имеет встроенную замену модуля

В дополнение к возможности замены функций с помощью td.replace , testdouble позволяет заменять целые модули.

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

 module.exports = function() { //do something }; 

Если мы хотим заменить это на testdouble, мы можем использовать td.replace('path/to/file') , например …

 var td = require('testdouble'); //assuming the above function is in ../src/myFunc.js var myFunc = td.replace('../src/myFunc'); myFunc(); td.verify(myFunc()); 

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

 var sinon = require('sinon'); var proxyquire = require('proxyquire'); var myFunc = proxyquire('../src/myFunc', sinon.stub()); 

Еще одна вещь, которую стоит отметить при замене модуля — testdouble.js автоматически заменяет весь модуль. Если это экспорт функции, как в примере здесь, он заменяет функцию. Если это объект, содержащий несколько функций, он заменяет все из них. Функции конструктора и классы ES6 также поддерживаются. И proxyquire, и rewire требуют от вас индивидуального указания, что заменить и как.

testdouble.js не хватает некоторых помощников Синона

Если вы используете фальшивые таймеры Sinon , фальшивый XMLHttpRequest или фальшивый сервер , вы заметите, что они отсутствуют в testdouble.

Поддельные таймеры доступны как плагин , но функции XMLHttpRequests и Ajax должны обрабатываться по-другому.

Одним из простых решений является замена используемой функции Ajax, например, $.post :

 //replace $.post so when it gets called with 'some/url', //it will call its callback with variable `someData` td.replace($, 'post'); td.when($.post('some/url')).thenCallback(someData); 

Очистить после тестов легче с testdouble.js

Распространенным камнем преткновения для начинающих с Sinon.js, как правило, является очистка шпионов и пней. Тот факт, что Синон предлагает три различных способа сделать это, не помогает.

 it('should test something...', function() { var stub = sinon.stub(console, 'log'); stub.restore(); }); 

или:

 describe('something', function() { var sandbox; beforeEach(function() { sandbox = sinon.sandbox.create(); }); afterEach(function() { sandbox.restore(); }); it('should test something...', function() { var stub = sandbox.stub(console, 'log'); }); }); 

или:

 it('should test something...', sinon.test(function() { this.stub(console, 'log'); //with sinon.test, the stub cleans up automatically })); 

Обычно методы sandbox и sinon.test рекомендуются на практике, так как в противном случае очень легко случайно оставить заглушки или шпионов на месте, что может вызвать проблемы в других тестах. Это может привести к трудно отслеживаемым сбоям каскадирования.

testdouble.js предоставляет только один способ очистки ваших тестовых пар: td.reset() . Рекомендованный способ — вызвать его в afterEach

 describe('something', function() { afterEach(function() { td.reset(); }); it('should test something...', function() { td.replace(console, 'log'); //the replaced log function gets cleaned up in afterEach }); }); 

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

Плюсы и минусы

Мы посмотрели на функциональность в обеих библиотеках сейчас. Они оба предлагают схожий набор функций, но имеют несколько отличную философию дизайна друг от друга. Можем ли мы разбить это на плюсы и минусы?

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

Недостатком этого является сложность. Хотя его гибкость позволяет экспертам делать больше вещей, это также означает, что некоторые задачи более сложны, чем в testdouble.js. Для тех, кто не знаком с концепцией двойных тестов, у него также может быть более крутая кривая обучения. На самом деле, даже такой, как я, который очень хорошо знаком с ним, может испытывать затруднения при разработке некоторых различий между sinon.stub и sinon.mock !

Вместо этого testdouble.js выбирает несколько более простой интерфейс. Большинство из них достаточно просты в использовании и более интуитивно понятны для JavaScript, в то время как Sinon.js иногда может чувствовать, что он был разработан с учетом какого-то другого языка. Благодаря этому и некоторым из его принципов проектирования, его может быть легче подобрать для начинающих, и даже опытные тестировщики найдут многие задачи проще для выполнения. Например, testdouble использует один и тот же API-интерфейс как для настройки дубликатов теста, так и для проверки результатов. Он также может быть менее подвержен ошибкам из-за более простого механизма очистки.

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

Функция за функцией сравнения

Ниже приведено сравнение функций:

Особенность Sinon.js testdouble.js
Шпионы да нет
Столбики да да
Задержка результатов заглушки нет да
Mocks да Да 1
Обещание поддержки Да (в 2.0+) да
Помощники по времени да Да (через плагин)
Помощники Ajax да Нет (замените функцию вместо)
Замена модуля нет да
Встроенные утверждения да да
Matchers да да
Индивидуальные соответствия да да
Аргументы захватчиков 2 да
Прокси тест удваивается нет да
  1. У testdouble.js технически нет насмешек, как у Sinon.js. Однако, поскольку макеты в Sinon по сути являются объектами, которые содержат заглушки и проверки, аналогичного эффекта можно добиться с помощью td.replace(someObject)
  2. Некоторые эффекты, аналогичные захватчикам аргументов, могут быть достигнуты с помощью stub.yield (не путать с stub.yields )

Резюме и заключение

И Sinon.js, и testdouble.js предоставляют довольно схожий набор функций. Ни один из них явно не превосходит в этом смысле.

Самые большие различия между ними в их API. Sinon.js, пожалуй, немного более многословен, и в то же время предоставляет множество вариантов действий. Это может быть как его благословение, так и проклятие. testdouble.js имеет более упорядоченный API, который может облегчить его изучение и использование, но из-за его более продуманного дизайна некоторые могут найти его проблематичным.

Так какой из них подходит мне?

Согласны ли вы с принципами дизайна testdouble? Если да, то нет причин не использовать его. Я использовал Sinon.js во многих проектах, и я могу с уверенностью сказать, что testdouble.js выполняет как минимум 95% всего, что я делал с Sinon.js, а оставшиеся 5%, вероятно, выполнимы с помощью некоторого простого обходного пути.

Если вы обнаружили, что Sinon.js сложен в использовании или ищете более «JavaScripty» способ выполнения тестовых двойников, тогда testdouble.js также подойдет вам. Даже будучи тем, кто потратил много времени на изучение использования Sinon, я склонен рекомендовать попробовать testdouble.js и посмотреть, нравится ли вам это.

Однако некоторые аспекты testdouble.js могут вызвать головную боль у тех, кто знает Sinon.js или иным образом является опытным тестером. Например, полное отсутствие шпионов может нарушить условия сделки. Для экспертов и тех, кто хочет максимальной гибкости, Sinon.js по-прежнему отличный выбор.

Если вы хотите узнать больше о том, как использовать тестовые дубли на практике, посмотрите мой бесплатный Sinon.js в руководстве по реальному миру . Несмотря на то, что он использует Sinon.js, вы можете применять те же методы и лучшие практики с testdouble.js.

Вопросов? Комментарии? Вы уже используете testdouble.js? Не могли бы вы попробовать после прочтения этой статьи? Позвольте мне знать в комментариях ниже.

Эта статья была рецензирована Джеймсом Райтом , Джоан Инь , Кристианом Йохансеном и Джастином Сирлсом . Спасибо всем рецензентам SitePoint за то, что сделали контент SitePoint как можно лучше!