Простой JavaScript разбираемся с mocking, stubbing и интерфейсами


Оглавление (нажмите, чтобы открыть):

Как вы тестируете статические методы?

Как вы тестируете статические методы?

Никак. Если у вас есть что-то внутри статического метода что стоит покрыть тестами — значит что-то пошло не так. Обычно там крайне примитивная логика, на уровне вызвать конструктор с нужными параметрами и т.д.

В вашем же вопросе вы интересуетесь как мокать статические методы. И тут ответ тоже никак:

Please note that final, private and static methods cannot be stubbed or mocked. They are ignored by PHPUnit’s test double functionality and retain their original behavior.

11 полезных приёмов в JavaScript

В этой статье мы расскажем об 11 весьма полезных приёмах в языке JavaScript. Они помогут вам сократить объём кода и оптимизировать его. Итак, начнём.

Конвертация в Boolean при помощи оператора !!

Порой нам нужно проверить, существует ли значение переменной и валидно ли оно, чтобы в этих случаях считать их верными, то есть равными true . Для этого можно применить к переменной оператор двойного отрицания !! , который автоматически меняет любой тип данных на логический (boolean) и возвращает false только в тех случаях, когда переменная принимает одно из следующих значений: 0 , null , «» , undefined или NaN . Вот простой пример:

Если значение account.cash больше нуля, account.hasMoney будет равна true .

Конвертация в Number при помощи оператора +

Удивительно простой приём! Работает он только со строками, а во всех остальных случаях возвращает NaN (Not a Number). Взглянем на пример:

Эта магия также работает с типом Date, возвращая timestamp:

Раз уж речь зашла о магии, советуем прочитать нашу статью, которая расскажет о настоящей магии JavaScript.

Упрощённая запись условных выражений

Допустим, у нас есть подобный код:

Его можно сократить, поместим оператор && между переменной (значение которой проверяется) и функцией. Вот так можно записать наш код:

Аналогично можно проверить, есть ли у объекта те или иные атрибуты и методы:

Это связано с так называемой короткой логикой: если левый операнд && равен false , то правый операнд даже не будет вычислен, потому что и так ясно, что все выражение ложно. И если правым операндом является функция, то она не будет вызвана.

Значения по умолчанию при помощи оператора ||

В ES6 есть возможность задать значение аргумента по умолчанию. Для того, чтобы воспроизвести эту фичу в старых браузерах, можно использовать оператор || , добавив в качестве второго параметра значение по умолчанию. Если первый параметр вернёт false , будет использоваться второй. Вот пример:

Подробнее о стандарте ES6 можно почитать в наших статьях:

Распознавание свойств объекта

Приём очень полезен, когда нужно проверить, существует ли тот или иной атрибут или метод. Также он может пригодиться и при кроссбраузерной вёрстке. Представим, что вам нужно написать код, совместимый с древним Internet Explorer 6, и вы хотите использовать document.querySelector() для получения элементов по идентификаторам. Но в IE6 такой функции не существует, поэтому нужно добавить проверку:

В этом примере мы можем использовать document.getElementById() как запасной вариант, если в объекте-документе нет функции querySelector .

Получение последнего элемента массива

Наверняка вы знакомы с методом Array.prototype.slice(begin, end) : с его помощью можно вырезать из массива кусок, задавая его границы begin и end . Если вы не зададите значение end , то функция автоматически задаст максимально возможное значение. Но немногие знают, что этот метод может принимать и отрицательные значения — если begin равен отрицательному числу, вы получите последние элементы массива:

Укорачивание массива

Этот способ позволяет зафиксировать размер массива и удалить все лишние элементы. Например, у вас есть массив из 10 элементов, а вам нужны только первые 5. В таком случае достаточно одной простой команды: array.length = 5 . Вот наглядный пример:

Замена всех подстрок

Метод String.replace() позволяет заменять подстроки при помощи регулярных выражений, но по умолчанию заменяется лишь первое вхождение подстроки. Тем не менее, вы можете исправить это, добавив /g в конец «регулярки»:

Слияние массивов

Если вам нужно объединить два массива, вы можете использовать метод Array.concat() :

Однако он не подходит для слияния больших массивов, поскольку потребляет большое количество памяти, создавая новый массив. В этом случае лучше использовать Array.push.apply(arr1, arr2) , что поместит второй массив в первый, сократив потребление памяти:

Конвертация NodeList в массив

Если вы вызовете document.querySelectorAll(«p») , то наверняка получите массив элементов DOM, объект класса NodeList. Проблема в том, что у него нет таких полезных методов, как sort() , reduce() , map() и filter() . Чтобы активировать эти и другие полезные методы, вам нужно конвертировать NodeList в массив. Для этого нужно просто выполнить [].slice.call(elements) :

Перемешивание элементов массива

Чтобы перемешать элементы массива, не используя сторонние библиотеки вроде Lodash, можно выполнить этот приём:

Тестирование для “чайников”.

Что такое тестирование

Тестирование — это процесс в программировании, позволяющий проверить на корректность отдельные модули исходного кода программы.

Когда нужно и не нужно тестировать

Всегда ли нужно тестировать ваше приложение? Рекомендуем ответить вам самим на этот вопрос после следующего утверждения: “Любой долгосрочный проект без надлежащего покрытия тестами обречен рано или поздно быть переписанным с нуля”.

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

  • Вы всегда пишете код для себя, создавая pet проект, не имея планов на его дальнейшее коммерческое использование или распространение в рамках сообщества.
  • Вы создаете рекламные одностраничники, продающие страницы, простые флеш-играми или баннеры — сложная верстка/анимация или большой объем статики. Никакой логики нет, только представление.
  • Создание статического сайта-визитки, т.е. 1–4 html-страницы с одной или несколькими формами для отправки данных. Тут закономерно нет никакой особенной логики, быстрее просто все проверить самостоятельно, так сказать «руками».
  • Вы делаете рекламный проект для выставки. Срок работы — от нескольких недель до месяца. В начале проекта не до конца известно, что именно должно получиться в конце. Задача проекта — отработать несколько дней на выставке в качестве презентации.

В представленных вариантах по объективным причинам (сжатые сроки, бюджеты, размытые цели или очень простые требования) вы не получите выигрыша от написания тестов.

Основные термины (пирамида, TDD, BDD, stubs, mock)

Существует несколько подходов к написанию тестов. Первая модель — классика: сначала разработка, а затем тестирование “code first”. Это означает, что сначала происходит написание кода, затем мы тестируем продукт и отправляем его или на доработку, или переходим к следующей стадии разработки.

Другой подход можно назвать “test first” режимом. Это означает, что мы можем начать тестирование еще до написания самой функции — например, мы можем создать единичный тест или автоматически выполняемый набор тестов до того, как функция или какой-то кусок кода будет разработан и внедрен в приложение. Одним из наиболее популярных примеров здесь является Test-Driven Development.

Тем не менее, стоит упомянуть, что техника “ test first” не так популярна, как “ code first”. Это связано с тем, что в большинстве проектов все еще сложно автоматизировать что то, что еще не было разработано. Обобщая оба упомянутых выше подхода, можно сделать вывод, что нет особой разницы и что автоматизацию тестов мы можем использовать в любом из вариантов. Ни один из этих подходов не может считаться хорошим или плохим и выбор в первую очередь зависит от проекта т.е. каждый конкретный случай следует рассматривать отдельно.

Также наряду с термином ТDD вы можете услышать и о BDD подходе.

  1. Сначала разработчик пишет несколько тестов.
  2. Затем разработчик запускает эти тесты и (очевидно) они терпят неудачу, потому что ни одна из этих функций еще не реализована.
  3. Далее разработчик действительно реализует эти тесты в коде.
  4. Если разработчик хорошо пишет свой код, то на следующем этапе он увидит, что тесты проходят.
  5. Разработчик может затем реорганизовать свой код, добавить комментарии так как он уверен, что если новый код что-то сломает, тогда тесты предупредят об этом.


Behavior-driven development (BDD) — подход создан для того, чтобы исправить проблемы, которые могут возникнуть при использовании ТDD, а именно, обеспечить лучшее взаимопонимание внутри команды, т.е. не только для разработчиков, облегчить поддержку кода через наглядное представление о его функциональности, тесты и их результаты выглядят более “человечно”, облегчается процесс миграции при переходе на другой язык программирования.

В варианте с BDD — в начале мы описываем поведение и спецификации, которые затем управляют нашей разработкой программного обеспечения. Поведение и спецификации могут показаться ужасно похожими на тесты, но разница очень тонкая и важная.

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

Еще одной важной концепцией тестирования является тестовая пирамида. Пирамида тестирования используется для распределения тестов по уровням приложения.

Каждое приложение можно разделить на несколько слоев. Рассмотрим типичное расслоение с уровнем компонентов, сервисами и пользовательским интерфейсом. Нижняя часть пирамиды покрыта модульными (unit) тестами. Они написаны в основном разработчиками и охватывают атомарные компоненты, такие как классы, методы и функции. Запускаются очень часто, работают быстро и их количество в рамках приложения велико.

Б. Страустрап в своей книге о C++ предлагает следующий подход к разделению кода на отдельные блоки: если «это» действие — сделайте метод. Если несколько действий объединены общим смыслом и/или процессом — объявите класс. Если придерживаться этого правила, то автоматически класс будет модулем вашего приложения.

Следующий уровень — интеграционные тесты. Т.е. когда идет проверка, не ломает ли новый функционал код, который уже написан ранее в рамках системы. Также тут мы можем иметь сценарии, которые охватывают более сложные функции, такие как тесты API. Запускаем реже, как правило, при мердже веток или объединении больших участков кода.

В верхней части находятся тесты пользовательского интерфейса (end to end) Они действуют так же, как конечный пользователь работает с приложением. Запускаем очень редко — несколько раз за проект. Работают очень медленно.

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

Еще один набор терминов, с которыми придется столкнуться в процессе написания тестов — это стабы (stubs) и моки (mock).

Очень часто наш код (функция, модуль) имеют внешние зависимости. Внешняя зависимость — это все, что делает ваши тесты не правдивыми и сложно-поддерживаемыми. Файловая система — зависимость: структура каталогов может быть другой на другой машине. База данных — зависимость, ее может не быть на другой машине. Веб-сервис — зависимость: может не быть интернета или может присутствовать фаервол и.т.д

Если на вопрос: «будет ли этот компонент вести себя так же на другой машине?» вы отвечаете нет, то его необходимо “подменить” и тут вам на помощь как раз придут стабы и моки. Но есть и другая сторона медали, когда разработчик начинает увлекаться и приходит к тому, что подменяет вообще все. Соответственно тесты перестают проверять само приложение и начинают тестировать стабы, моки. Это в корне не верно. Если «живых» реализаций в тесте нет, то этот тест не тестирует ничего.

Иногда эти термины stubs и mock путают: разница в том, что стаб ничего не проверяет, а лишь имитирует заданное состояние. А мок — это объект, у которого есть ожидания. Например, что данный метод класса должен быть вызван определенное число раз. Иными словами, ваш тест никогда не сломается из-за «стаба», а вот из-за мока может.

С технической точки зрения это значит, что, используя стабы, мы проверяем состояние тестируемого класса или результат выполненного метода. При использовании мока мы проверяем, соответствуют ли ожидания мока поведению тестируемого класса. Также лучше использовать не более одного мока на тест. Иначе с высокой вероятностью вы нарушите принцип «тестировать только одну вещь». При этом, в одном тесте может быть сколько угодно стабов или же мок и стабы.

Есть уже готовые фреймворки, которые предоставляют такой функционал: Sinon, Jasmine, enzyme, Jest, testdouble

Пример использования стаба (sinonjs.org)

Пишем тесты правильно (требования, оценка результата)

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

Есть несколько рекомендаций, придерживаясь которых, вы можете избежать данной проблемы.

1) Тесты в пределах проекта должны быть расположены в соответствии с общей логикой и должны быть частью системы контроля версий. Например, если приложение монолитное, положите все тесты в папку test; если у вас много разных компонентов, храните тесты в папке каждого компонента.

2) Особое внимание уделите именованию тестов. Одна из лучших практик: добавьте к каждому проекту его собственный тестовый проект.

Если у вас, например, есть части системы your-project.models, your-project.controllers, то тесты для этих частей могут именоваться следующим образом: your-project.models.tests, your-project.controllers.tests

3)Такие же “логичные” походы используйте для именования тестовых классов или методов.

Например, один из вариантов именования для тестирования метода — _ _ .

4)Каждый тестирующий класс или метод должен тестировать только одну сущность. Если процесс слишком сложен (например, покупка в интернет-магазине), разделите его на несколько частей и протестируйте их отдельно.

5)Не делайте ненужных утверждений (assertion). Какое конкретное поведение вы тестируете? Если это не основное поведение, то оно и не нуждается в тестировании! Помните, что модульные тесты — это спецификация дизайна того, как должно срабатывать определенное поведение, а не список наблюдений за всем кодом.

6) Вы определенно ошиблись, если вам нужно запускать тесты в определенном порядке, или если они работают только при активной базе данных или сетевом соединении.

7)Не стоит писать велосипеды. Пользуйтесь готовыми тестовыми фреймворками. Подберите тот, который подходит вам в данном, конкретном случае.

8)Боритесь с зависимостями. Тесты не должны зависеть от окружения, в котором они выполняются. Например, не проверяйте настройки конфигурации устройства. По необходимости используйте стабы и моки, а также готовые фреймворки для их написания.

9)Не относитесь к своим тестам как к второсортному коду. Все принципы, применяемые в разработке продакшн-кода могут и должны применяться при написании тестов. (DRY, KISS)

10)Тест должен легко поддерживаться. Есть всего три причины, почему тест перестал проходить:

  • Ошибка в продакшн-коде: это баг, его нужно завести в баг-трекере и починить.
  • Баг в тесте: видимо, продакшн-код изменился, а тест написан с ошибкой (например, тестирует слишком много или не то, что было нужно).
  • Смена требований. Если требования изменились слишком сильно — тест должен упасть. Это правильно и нормально. Вам нужно разобраться с новыми требованиями и исправить тест. Или удалить, если он больше не актуален.
Мастер Йода рекомендует:  Унифицированный процесс Rational (RUP)

11)Тесты должны запускаться регулярно в автоматическом режиме.

Определить успешность вашей системы тестирования на проекте можно двумя способами:

  1. Количество багов в новых релизах (в т.ч. и регрессии).
  2. Покрытие кода.

Первый показывает, есть ли у наших действий результат, или мы впустую расходуем время. Второй — как много нам еще предстоит сделать.

Тестовое покрытие — полезный инструмент для поиска непроверенных частей кодовой базы. Тестовый охват мало полезен в качестве числового заявления о том, насколько хороши ваши тесты. “Нормальным” считается покрытие в пределах 80%.

Наиболее популярные инструменты для измерения покрытия кода, написанного на JavaScript istanbul.js blanket.js JSCover

Выбор фреймворка для создания тестов

На сегодняшний день доступна целая масса фреймворков для тестирования JavaScript-кода (overview).

Учитывая подобное разнообразие, выбор того или иного фреймворка, как правило, напрямую зависит от непосредственной задачи, которую мы ставим перед собой в процессе написания тестов. Идеально, когда функционал фреймворка покрывает несколько или все поставленные задачи (единая среда).

Например, поддерживаемая структура тестов. Если мы говорим о поддержке BDD, то следует выбирать среди Mocha, Jasmine, Jest, Сucumber.

Также доступны на выбор несколько assertion библиотек: Chai, Jasmine, Jest, Unexpected.

Если для вас важную роль играет представление и отображение результатов ваших проверок, то наибольший функционал в данной области предоставляют Mocha, Jasmine, Jest, Karma.

Для использования snap-shots в вашем тестировании следует обратить внимание на Jest или Ava.

Для борьбы с зависимостями и использования mocks и stubs следует обратить внимание на специализированные фреймворки типа Sinon.js, Jasmine, enzyme, Jest, testdouble.js.

Как уже упоминалось выше для измерения охвата и покрытия кода тестами возможно использование Istanbul, Jest.

Для функциональных тестов, для создания пользовательских сценариев поведения, необходимо использование браузерной среды или браузерной среды с программируемым API, что доступно в рамках Protractor, Nightwatch, Phantom.js, Сasper.

Описание некоторых фреймворков

JSDOM является реализацией JavaScript-стандартов WHATWG DOM и HTML. Другими словами, JSDom имитирует среду браузера, не запуская ничего, кроме простого JS. В этой моделируемой среде браузера тесты могут выполняться очень быстро. Недостатком JSDom является то, что не все может быть смоделировано вне реального браузера (например, вы не можете сделать снимок экрана), поэтому его использование ограничивает доступность ваших тестов.


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

Phantom.js — реализует «headless» браузер Webkit, который находится между реальным браузером и JSDom в скорости и стабильности. Достаточно популярен.

Karma — позволяет запускать тесты в браузерах, включая настоящие браузеры, Phantom, JSdom и даже устаревшие браузеры. Karma размещает тестовый сервер со специальной веб-страницей для запуска тестов в среде страницы. Эта страница может быть запущена во многих браузерах. Это также означает, что тесты можно запускать удаленно с помощью таких служб, как BrowserStack.

Chai — самая популярная assertion библиотека.

Unexpected — это также assertion библиотека с немного отличающимся синтаксисом от Chai.

Sinon.js — это набор очень мощных тестовых шпионов, заглушек и макетов (mocks) для модульного тестирования.

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

Jasmine — представляет собой платформу тестирования, обеспечивающую все, что вам требуется для ваших тестов: работающая среда, структура, отчетность, assertion и mocks инструменты.

Mocha — в настоящее время является наиболее часто используемой библиотекой. В отличие от Jasmine, она используется со сторонними библиотеками mocks и assertions (обычно Enzyme и Chai). Это означает, что Mocha немного сложнее настроить, но она более гибкая и открыта для расширений.

Jest — это платформа тестирования, рекомендованная Facebook. Он использует функционал Jasmine и добавляет функции поверх него, поэтому все упоминания о Jasmine относится и к нему.

Ava — минималистическая библиотека, которая имеет возможность запускать тесты параллельно.

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

Protractor — это библиотека, которая использует Selenium, но добавляет улучшенный синтаксис и специально встроенные хуки для Angular.

Nightwatch — имеет собственную реализацию selenium WebDriver. И обеспечивает собственную среду тестирования, тестовый сервер, assertion и другие инструменты.

Сasper — написан поверх Phantom и Slimer (так же, как Phantom, но в Gecko FireFox), чтобы при помощи специальных утилиты более просто создавать Phantom и Slimer скрипты. Каспер предоставляет нам более быстрый, но менее стабильный способ запуска функциональных тестов в браузерах с интерфейсом UI.

Сucumber — еще один замечательный фреймворк для функционального тестирования. Включает в себя весь основной функционал, ранее перечисленный в похожих фреймворках.

Javascript Mocks And Stubs

Objectives

  1. Define a stub
  2. Define a mock
  3. Explain when to use mocks and stubs
  4. Explain when you might want to avoid mocks and stubs

What are stubs?

While spies wrap existing functionality, stubs allow us to replace the functionality completely. The original function won’t run anymore, but rather our new stub that simply returns a result. This is especially handy if we want to test async stuff that makes use of things like jQuery’s AJAX methods.

For example, let’s take a look at this code:

This seems easy enough, but async code like this gets really hard to test. What we’ll do instead is stub jQuery’s .get() method — this way it doesn’t make any external requests for the purposes of our test. This makes our test a lot faster and super stable.

Using stubs, it’s also really easy to test our code for edge cases. What happens when the request fails, or sends back unexpected data? Stubbing the method like this gives us absolute control over the data that is returned through the $.get() method, making testing for this stuff trivial.

What are mocks?

Mocks are the end-all of fake methods. They’re kind of like spies, but they also have stubbed behavior (much like stubs), and contain ‘pre-programmed expectations’. That just means that we tell the mock what to expect up front.

Let’s assume that we have some kind of storage mechanism that saves a user’s preferences:

Now, we have some other code that makes use of this function to save the options in the UI to the storage:

Let’s test the sendPrefsToStorage() method. We’ll mock our storage since we’re interested in verifying our function, and not the logic of storage itself:

As you can see, it’s easy to mock both methods on the storage object using Sinon’s mock. However, it is important that we only mock what we actually need. Mocking too much stuff will lead to brittle tests.

Avoiding stubs and mocks

The best kind of code is modular with most of its functions being pure (remember, a pure function is a function that determines its return value only by its input values, without any side-effects). As such, it’s easier to test our actual implementations piece by piece rather than relying on stubs and mocks.

Stubs and mocks are still useful for testing the annoying async parts or mocking out methods in a library, but they should be used very sparingly.

Практические советы по улучшению кода на JavaScript

В этой статье я приведу несколько практических советов из своего опыта как улучшить свой код на JavaScript.

Используйте TypeScript

Первое, что вы можете сделать, чтобы улучшить свой код на JS, это не писать на JS. Используйте TypeScript. Для непосвященных TypeScript (TS) – это отдельный язык программирования расширяющий стандартный JS (все, что выполняется в JS, запускается в TS). Долгое время поддержка TS во всей экосистеме была достаточно непоследовательной, ее было сложно использовать и мне было неудобно рекомендовать ее. К счастью, эти дни давно позади, и большинство фреймворков поддерживают TS из коробки. Давайте поговорим о том, почему вам нужно использовать его.

TypeScript обеспечивает «безопасность типов».

Безопасность типов это такой процесс, в котором компилятор проверяет, что все типы используемые «легальным» способом в пределах компилируемого фрагмента кода. Другими словами, если вы создаете функцию foo, которая принимает число:

Эта функция foo должна вызываться только с числом:

Помимо накладных расходов на добавление типов в ваш код, нет никаких минусов в обеспечении безопасности типов. С другой стороны, выгода слишком велика, чтобы ее игнорировать. Безопасность типов обеспечивает дополнительный уровень защиты от распространенных ошибок / багов, что является благословением для таких языков, как JS.

Типы Typescript, облегчают рефакторинг крупных приложений.

Рефакторинг большого JS-приложения может стать настоящим кошмаром. Основная трудность рефакторинга JS связана с тем, что обычный рефакторинг не поддерживает контроль над сигнатурами функций. Например, если у меня может есть функция myAPI, которая используется в 1000 различных мест:

и вдруг мне нужно немного поменять сигнатуру вызова:

Я должен быть на 100% уверен, что в каждом месте, где используется эта функция (1000 мест), я корректно обновляю использование. Если я пропущу даже одно место, мои учетные данные используемые внутри функции могут пропасть. Вот тот же сценарий с TS:

Как видите, функция myAPITS претерпела те же изменения, что и аналог в JavaScript. В JavaScript этот код работает без ошибки в TypeScript у вас сразу будет ошибка из-за неправильного типа. И из-за «безопасности типов», которую мы обсуждали ранее, эти 1000 случаев будут блокировать компиляцию, и ваши учетные данные не будут потеряны (а это всегда хорошо).

TypeScript облегчает общение в командной архитектуре.


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

Мне уже приходилось писать код, но теперь я могу поделиться своим прогрессом и получить обратную связь, не тратя больше времени. Я твердо верю, что принуждение разработчиков сначала определять интерфейсы и API приводит к лучшему коду.

В целом, TS превратилась в зрелую и более предсказуемую альтернативу чистой JS. Я все еще использую в некоторых проектах ванильный JS, но в большинстве новых проектов, которые я начинаю сегодня, я подключаю TS с самого начала.

Используйте современные функции

JavaScript – один из самых популярных (если не самый) языков программирования в мире. Вы можете ожидать, что 20-летний язык, которым пользуются сотни миллионов людей, к настоящему времени будет «всем понятным и известным», но на самом деле все наоборот. В последнее время в JS было сделано много изменений и дополнений. Как человек, который начал писать JS только в последние 2 года, у меня было преимущество, что я пришел без предвзятости или ожиданий. Это привело к гораздо более прагматичному, нерелигиозному выбору того, какие особенности языка использовать, а какие избегать.

async и await

Долгое время асинхронные, управляемые событиями обратные вызовы (callback) были неизбежной частью разработки JS:

Я не собираюсь тратить время на объяснение того, почему вышеизложенное является проблематичным (я описывал это раньше). Чтобы решить проблему с обратными вызовами, в JS была добавлена новая концепция «Promises (Обещания)». Promises позволяют вам писать асинхронную логику, избегая проблем с вложениями, которые ранее преследовали код на основе обратного вызова (callback).

Самым большим преимуществом Promises по сравнению с обратными вызовами является удобочитаемость и возможность объединения в цепочки через оператор then.

Несмотря на то, что Promises великолепны, они все же оставляют желать лучшего. В конце концов, написание Promises все еще не казалось “родным” для JS. Чтобы исправить это, комитет ECMAScript решил добавить новый метод использования promises, async и await:

Единственное предупреждение: все что вы используете с await, должно быть объявлено с async:

функция makeHttpRequest из предыдущего примера

Также возможно использовать await с Promise напрямую, так как асинхронная функция – это просто необычная оболочка для Promise. Это также означает, что код async / await и код Promise функционально эквивалентны. Так что не стесняйтесь использовать async / await как можно чаще.

let и const

На протяжении большей части существования JS существовал только один способ объявления переменных var. У var есть довольно уникальные / интересные правила относительно того, как он обрабатывает область видимости. Поведение в области видимости var противоречиво и сбивает с толку, что приводит к неожиданному поведению и, следовательно, к ошибкам в течение всего жизненного цикла JS. Но в ES6 есть альтернатива var, const и let. На данный момент не нужно больше использовать var. Любая логика, которая использует var, всегда может быть преобразована в эквивалентный const и let.

Что касается использования const против let, я всегда начинаю с объявления всех переменных с const. const намного более ограничен и «неизменяем», что обычно приводит к лучшему коду. Не существует так уж много «реальных сценариев», где необходимо использовать только let, я бы сказал, что 1/20 переменных я объявляю с помощью let остальные с const.

Я сказал, что const “неизменяем”, но он не работает так же, как const в C / C ++. Что означает const в среде выполнения JavaScript, это ссылка на переменную но это не означает, что содержимое, сохраненное по этой ссылке, никогда не изменится. Для примитивных типов (number, boolean и т. д.) const неизменяемый (потому что это всего один адрес памяти). Но для всех объектов (классов, массивов, dicts) const не гарантирует неизменность.

Стрелочные функции Arrow => Functions

Функции стрелок являются кратким методом объявления анонимных функций в JS. Анонимные функции, описывают функции, которые не имеют явного имени. Обычно анонимные функции передаются как обратный вызов или перехват событий (event hook).

обычная анонимная функция

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

стрелочная анонимная функция

Помимо того, что функции стрелок намного более кратки, они также имеют гораздо более практичное поведение при определении области видимости scope. Функция стрелки наследует this от области, в которой они были определены.

В некоторых случаях функции стрелок могут быть еще более краткими:

Стрелочные функции, которые находятся в одной строке, включают в себя неявный оператор return . Нет необходимости в скобках или точках с запятой с однострочными стрелочными функциями.

Оператор Spread (распространения) .

Извлечение пар ключ / значение одного объекта и добавление их в качестве потомков другого объекта – очень распространенный сценарий. Исторически, было несколько способов сделать это, но все эти методы довольно неуклюжи:

Этот шаблон невероятно распространен, поэтому описанный выше подход быстро становится громоздким. Благодаря «оператору Spread» его больше никогда не нужно использовать:

Самое замечательное, что это также без проблем работает с массивами:

Вероятно, это не самая важная из недавних функций JS, но она одна из моих любимых.

Шаблонные строки / Шаблонные литералы

Строки являются одной из наиболее распространенных программных конструкций. Вот почему так плохо, что объявление строк все еще плохо поддерживаются во многих языках. Долгое время JS был в семье подобных языков. Но добавление шаблонных литералов ставит JS в отдельную категорию. Шаблонные литералы изначально и удобно решают две самые большие проблемы с написанием строк, добавлением динамического содержимого и написанием строк, которые соединяют несколько строк:

Мастер Йода рекомендует:  Что такое «государственный поисковик» и кто его создаст

Я думаю, что код говорит сам за себя. Какая удивительная реализация.

Деструктуризация объекта

Деструктуризация объекта – это способ извлечения значений из коллекции данных (объект, массив и т. д.) без необходимости перебирать данные или явно обращаться к их ключам:

Но подождите, это еще не все. Вы также можете определить деструктурирование в сигнатуре функции:

Это также работает с массивами:

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

Всегда предполагайте, что ваша программа будет работать в асинхронной среде

При написании параллельных приложений ваша цель – оптимизировать объем работы, которую вы можете выполнить за один раз. Если у вас есть 4 доступных ядра и ваш код может использовать только одно ядро, 75% вашего потенциала тратится впустую. Это означает, что блокирование синхронных операций – главный враг параллельных вычислений. Но, учитывая, что JS является однопоточным языком, он не работает на нескольких ядрах. Так какой в этом смысл?

JS является однопоточным, но не однофайловым. Даже если он не многопоточный, он все же параллельный (concurrent). Отправка HTTP-запроса может занять несколько секунд или даже минут. Если JS прекратит выполнение кода до тех пор, пока ответ не вернется из запроса, код станет непригодным для использования.

JavaScript решает это с помощью цикла событий. Цикл обработки событий проходит по зарегистрированным событиям и выполняет их на основе внутренней логики планирования / расстановки приоритетов. Это то, что позволяет отправлять тысячи «одновременных» HTTP-запросов или считывать несколько файлов с диска в одно и то же время. Здесь есть одна загвоздка: JavaScript может использовать эту возможность, только если вы используете правильные функции. Самый простой пример – цикл for:

Ванильный цикл – одна из наименее параллельных конструкций, которая существует в программировании. На моей последней работе я возглавлял команду, которая месяцами пыталась преобразовать традиционные циклы языка R for в автоматически параллельный код. Это в принципе невозможная проблема, которую можно решить только в ожидании улучшения глубокого обучения. Трудность распараллеливания цикла for возникает из-за нескольких проблемных паттернов. Последовательные циклы for очень редки, но только они делают невозможным гарантировать разделимость циклов for:

Этот код выдает ожидаемый результат, только если он выполняется по порядку, итерация за итерацией. Если вы попытаетесь выполнить несколько итераций одновременно, процессор может неправильно выполнить ветвление на основе неточных значений, что приведет к аннулированию результата. У нас был бы другой разговор, если бы это был код на C, так как его использование отличается и есть довольно много хитростей, которые компилятор может сделать с циклами. В JavaScript традиционный цикл for следует использовать только в случае крайней необходимости. В противном случае используйте следующие конструкции:

Я объясню, почему это улучшение по сравнению с традиционным циклом for. Вместо выполнения каждой «итерации» по порядку (последовательно), такие конструкции, как map, принимают все элементы и отправляют их в виде отдельных событий в пользовательскую функцию map. Это напрямую сообщает среде выполнения, что отдельные «итерации» не имеют никакой связи или зависимости друг от друга, что позволяет им работать одновременно. Есть много случаев, когда цикл for будет таким же быстродействующим по сравнению с map или forEach. Но я все еще утверждаю, что потеря нескольких миллисекунд сейчас стоит того, чтобы использовать четко определенное API. Таким образом, любые будущие улучшения в реализации этих шаблонов доступа к данным пойдут на пользу вашему коду. Цикл for слишком универсален, чтобы иметь смысл оптимизировать для того же шаблона.

Существуют и другие допустимые варианты асинхронизации вне map и forEach, например for-await-of.

Используйте Lint и применяйте стили кодирования

Код без единого стиля невероятно сложен для чтения и понимания. Поэтому критически важным аспектом написания кода высокого уровня на любом языке является последовательный и разумный стиль. Из-за широты экосистемы JS существует множество опций для линтеров и особенностей стиля. Многие люди спрашивают, должны ли они использовать eslint или prettier. Для меня они служат разным целям, и поэтому должны использоваться вместе. Eslint – это традиционный «линтер», в большинстве случаев он будет выявлять проблемы с вашим кодом, которые имеют меньшее отношение к стилю и больше связаны с корректностью. Например, я использую eslint с правилами AirBNB. При такой конфигурации следующий код приведет к сбою линтера:

По сути, это гарантирует, что вы следуете правилам относительно того, что является «хорошим», а что нет. В связи с этим, линтеры по своей природе самоуверенны. Но как и со всеми мнениями, учитывайте что, линтер может ошибаться в какой определенной ситуации.

Prettier – это средство форматирования кода. Оно меньше заботится о «правильности» и гораздо больше беспокоится о единообразии и последовательности. Prettier не собирается жаловаться на использование var, но он автоматически выровняет все скобки в вашем коде. В моем личном процессе разработки я всегда выполнял Prettier как последний шаг перед отправкой кода в Git. Во многих случаях даже имеет смысл запускать Prettier автоматически при каждом коммите в репозитарий. Это гарантирует, что весь код, поступающий в систему контроля версий, будет иметь согласованный стиль и структуру.

Тестируйте свой код

Написание тестов – это косвенный, но невероятно эффективный метод улучшения написанного вами кода JS. Я рекомендую освоиться с широким спектром инструментов тестирования. Ваши потребности в тестировании будут различаться, и нет единого инструмента, который справится со всем. В экосистеме JS существует множество хорошо зарекомендовавших себя инструментов тестирования, поэтому выбор инструментов в основном зависит от личного вкуса. Как всегда, выбирайте сами.

Test Driver – Ava


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

Ava – это правильный баланс выразительности и лаконичности. Параллельная и разрозненная архитектура Ava – причина моей любви к ней. Тесты, которые работают быстрее, экономят время разработчиков и деньги компаний. Ava может похвастаться множеством приятных функций, таких как встроенные утверждения, хотя при этом и остается минимальным.

Альтернативы: Jest, Mocha, Jasmine

Spies (шпионы) и Stubs (заглушки) – Sinon

Spies дают нам «функции аналитики», например, сколько раз была вызвана функция, как они были вызваны, и другие проницательные данные.

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

Альтернативы: testdouble

Mocks – Nock

HTTP-mocking – это процесс подделки некоторой части процесса http-запроса, что бы тестировщик мог внедрить пользовательскую логику для имитации поведения сервера.

Http mocking может быть настоящей болью, nock делает его менее болезненным. Nock напрямую переопределяет встроенный запрос nodejs и перехватывает исходящие http-запросы. Это, в свою очередь, дает вам полный контроль над ответом.

Альтернативы: я не знаю ни одного ��

Web Automation – Selenium

Selenium – это то, к чему я испытываю смешанные чувства по поводу рекомендаций. Поскольку это самый популярный вариант для веб-автоматизации, он имеет огромное сообщество и набор онлайн-ресурсов. К сожалению, кривая обучения довольно крутая, и для реального использования она зависит от множества внешних библиотек. Тем не менее, это единственный реальный бесплатный вариант, поэтому, если вы не занимаетесь веб-автоматизацией корпоративного уровня, Selenium будет вам очень полезен.

Чудес не бывает или я ошибаюсь?

О разном в программировании, тестировании и руководстве командами в IT

Страницы

пятница, 30 марта 2012 г.

Mock vs Stub

  • Низкая скорость выполнения тестов с реальными объектами (если это, например, работа с базой, файлами, почтовым сервером и т.п.)
  • Собственно необходимость запуска тестов независимо от окружения (например, на машине у любого разработчика)
  • Система, в которой работает код, не дает возможности (или дает, но это сложно делать) запустить код с определенным входным набором данных.
  • Нет возможности проверить, что SUT отработал правильно, например он меняет не свое состояние, а состояние внешней системы. И там эту проверку сделать сложно.

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

А вообще «используйте mocks только, когда это действительно нужно»

Mocking and Stubbing with protractor

I want to test my angular app with protractor. The app has an API Module that talks to the server During these tests I want to mock this Api Module. I don’t want to do full integration tests, but tests from the user input with expected values from the API. Not only could this make the client tests faster, it would also allow me to test for edge cases, like connection errors.

How can I do this with protractor? I just started to setup integration tests.

I used the npm protractor module, installed selenium, adjusted the default config and used the onProtractorRunner.js to verify my setup works.

What is the recommended way of mocking? I assume that the mocking has to be done within the browser and not directly in the test file. I assume that the commands in the test file are protractor specific and will be sent to the selenium runners. Therefore I can’t share javascript objects during the session and the test.

I somehow expect that I will need a spy library like sinon.js or is this already included in protractor?

Edit: I read about this issue in the protractor issue tracker, which could be a way to do it. Basically you write a Mock Module in the test, that is sent to be executed in the browser/ the applications scope.

Edit: Here are more promising Issues. The first talks about adding Mocks to the Angular App. The second talks about mocking the backend.

This looks really nice, in this case the Angular App would stay in it’s original form. However this currently only works with the deprecated ng-scenarios.

Блог Александра Кондуфорова

об информационных технологиях, программировании, путешествиях и фотографии

Thursday, September 25, 2008

Введение в mock-объекты. Классификация

Еще один длинный, но, надеюсь, полезный пост 🙂 Пригодится тем, кто хочет начать писать unit-тесты, уже пишет их и хочет увеличить свои познания, и даже тем, кто тесты не пишет, но хочет быть в курсе того, что же это за зверь такой страшный — unit-тестирование.

Как я уже писал в предыдущем посте об unit -тестировании, иногда, для того, чтобы протестировать какой-нибудь кусок кода (например, метод), нужно довольно сильно постараться. Причем, это еще не тот вид извращений, когда вы тестируете методы UI, проблемы могут начаться с тестирования бизнес-логики. Дело в том, что очень часто тестируемый метод может вызывать методы других классов, которые в данном случае тестировать не нужно. Unit -тест потому и называется модульным, что тестирует отдельные модули, а не их взаимодействие. Причем, чем меньше тестируемый модуль – тем лучше с точки зрения будущей поддержки тестов. Для тестирования взаимодействия используются интеграционные тесты, где вы уже тестируете скорее полные use cases , а не отдельную функциональность.

Однако наши классы очень часто используют другие классы в своей работе. Например, слой бизнес логики ( Business Logic layer ) часто работает с другими объектами бизнес логики или обращается к слою доступа к данным (Data Access l ayer). В трехслойной архитектуре веб-приложений это вообще постоянный процесс: Presentation l ayer обращается к Business Logic layer , тот, в свою очередь, к Data Access layer , а Data Access layer – к базе данных. Как же тестировать подобный код, если вызов одного метода влечет за собой цепочку вплоть до базы данных?

В таких случаях на помощь приходят так называемые mock-объекты, предназначенные для симуляции поведения реальных объектов во время тестирования. Вообще, понятие mock -объект достаточно широко: оно может, с одной стороны, обозначать любые тест-дублеры (Test Do ubles ) или конкретный вид этих дублеров – mock-объекты. Я постараюсь использовать этот термин исключительно во втором случае, чтобы никого не путать, и не запутаться самому 🙂

Понятие тест-дублеров введено неким Gerard Meszaros в своей книге « XUnit Test Patterns » и теперь с подачи небезызвестного Мартина Фаулера эта терминология набирает популярность. Джерард и Мартин делят все тест-дублеры на 4 группы:

  • Dummy – пустые объекты, которые передаются в вызываемые внутренние методы, но не используются. Предназначены лишь для заполнения параметров методов.
  • Fake – объекты, имеющие работающие реализации, но в таком виде, который делает их неподходящими для production -кода (например, In Memory Database ).
  • Stub – объекты, которые предоставляют заранее заготовленные ответы на вызовы во время выполнения теста и обычно не отвечающие ни на какие другие вызовы, которые не требуются в тесте. Также могут запоминать какую-то дополнительную информацию о количестве вызовов, параметрах и возвращать их потом тесту для проверки.
  • Mock – объекты, которые заменяют реальный объект в условиях теста и позволяют проверять вызовы своих членов как часть системы или unit -теста. Содержат заранее запрограммированные ожидания вызовов, которые они ожидают получить. Применяются в основном для т.н. interaction (behavioral) testing .

Поначалу эта классификация выглядит очень непонятной. Но если вдуматься, то можно разобраться, в чем заключается отличие между теми и иными типами объектов. Предположим, что вам нужно протестировать метод Foo () класса TestFoo, который делает вызов другого метода Bar() класса TestBar. Предположим, что метод Bar() принимает какой-нибудь объект класса Bla в качестве параметра и потом ничего особого с ним не делает. В таком случае имеет смысл создать пустой объект Bla , передать его в класс TestFoo (сделать это можно при помощи широко применяемого паттерна Dependency Injection или каким-либо другим приемлемым способом), а затем уже Foo() при тестировании сам вызовет метод TestBar .Bar() с переданным пустым объектом. Это и есть иллюстрация использования dummy-объекта в unit-тестировании.


К сожалению, редко можно обойтись простыми dummy-объектами. Иногда метод Bar () выполняет какие-то действия с ним (допустим, Bar () сохраняет данные в базу или вызывает веб-сервис, а мы этого не хотим). В таких случаях наш объект класса TestBar должен быть уже не таким глупым. Мы должны научить его в ответ на запрос сохранения данных просто выполнить какой-то простой код (допустим, сохранение во внутреннюю коллекцию). В таких случаях можно выделить интерфейс I TestBar , который будет реализовывать класс TestBar и наш дополнительный класс FakeB ar . При unit-тестировании мы просто будем создавать объект класса FakeB ar и передавать его в класс с методом Foo() через интерфейс. Естественно, при этом класс B ar будет по-прежнему создаваться в реальном приложении, а FakeB ar будет использован лишь в тестировании. Это иллюстрация fake-объекта.

Со stub- и mock-объектами все немного сложнее, хотя и здесь есть от чего отталкиваться. Stub -объекты (стабы) – это типичные заглушки. Они ничего полезного не делают и умеют лишь возвращать определенные данные в ответ на вызовы своих методов. В нашем примере стаб бы подменял класс TestBar и в ответ на вызов Bar() просто бы возвращал какие-то левые данные. При этом внутренняя реализация реального метода Bar() бы просто не вызывалась. Реализуется этот подход через интерфейс и создание дополнительного класса StubBar, либо просто через создание StubBar, который является унаследованным от TestBar. В принципе, реализация очень похожа на fake-объект с тем лишь исключением, что стаб ничего полезного, кроме постоянного возвращения каких-то константных данных не требует. Типичная заглушка. Стабам позволяется лишь сохранять у себя внутри какие-нибудь данные, удостоверяющие, что вызовы были произведены или содержащие копии переданных параметров, которые затем может проверить тест.

Mock -объект (мок) , в свою очередь, является, грубо говоря, более умной реализацией заглушки, которая уже не просто возвращает предустановленные данные, но еще и записывает все вызовы, которые проходят через нее, чтобы вы могли дальше в unit-тесте проверить, что именно эти методы вот этих вот классов были вызваны тестируемым методом и именно в такой последовательности (хотя учет последовательности и строгость проверки, в принципе, настраиваемая вещь). То есть мы можем сделать мок MockFoo , который будет каким-то образом вызывать реальный метод Foo() класса TestFoo и затем смотреть, какие вызовы тот сделал. Или сделать мок MockBar и затем проверить, что при вызове метода Foo() реально произошел вызов метода Bar() с нужными нам параметрами. Не совсем понятно? 🙂 Вам нужно знать еще кое-что о unit-тестировании, чтобы понять разницу.

Мастер Йода рекомендует:  Редиректы с использованием HTTPS

Unit -тестирование условно делится на два подхода:

  • state — based testing , в котором мы тестируем состояние объекта после прохождения unit-теста
  • interaction (behavioral) testing , в котором мы тестируем взаимодействие между объектами, поведение тестируемого метода, последовательность вызовов методов и их параметры и т.д.

То есть в state-based testing нас интересует в основном, в какое состояние перешел объект после вызова тестируемого метода, или, что более часто встречается, что в реальности вернул наш метод и правилен ли этот результат. Подобные проверки проводятся при помощи вызова методов класса Assert различных unit-тест фреймворков: Assert.AreEqual(), Assert.That(), Assert.IsNull() и т.д.

В interaction testing нас интересует прежде всего не статическое состояние объекта, а те динамические вызовы методов, которые происходят у него внутри. То есть для нашего примера с классами TestFoo и TestBar мы будем проверять, что тестируемый метод Foo() действительно вызвал метод Bar() класса TestBar, а не то, что он при этом вернул и в какое состояние перешел. Как правило, в случае подобного тестирования программисты используют специальные mock-фреймворки ( TypeMock . Net , EasyMock . Net , MoQ , Rhino Mocks , NMock 2 ), которые содержат определенные конструкции для записи ожиданий и их последующей проверки через методы Verify(), VerifyAll(), VerifyAllExpectations() или других (в зависимости от конкретного фреймворка).

То есть во многом это отличие можно назвать аналогичным отличию state machine diagram и activity diagram в UML : описывают они, в принципе, одно и то же, но разными способами. Иногда удобнее один, иногда второй.

Фаулер вот называет эти два подхода классическим ( classical ) и мокистским ( mockist , ну и слово выдумал) unit-тестированием и делит программистов на тех, кто предпочитают первый и кто предпочитает второй подходы. Я бы этого не делал. Мне кажется, что иногда просто удобнее проверить состояние объекта, а иногда – его взаимодействие с другими объектами. Поэтому эти два подхода прекрасно уживаются вместе, когда вы понимаете, о чем идет речь, и что именно вы хотите сейчас проверить. Так же, как уживаются в одном тесте моки и стабы.

Вот такие пирожки. Теперь несколько примеров, которые иллюстрируют различия между этими двумя подходами и использования стабов и моков. Я также покажу, зачем в реальности может понадобиться interaction -тестирование, чтобы вам было проще выбирать тот или иной вид в будущем. Примеры используют NUnit и Rhino Mocks , хотя на их месте с небольшим изменением синтаксиса может оказаться почти любая другая пара фреймворков.

Допустим, у нас есть несколько простых классов:

Вот такой набор классов. Класс Order обращается к классу Warehouse, а тот обращается к базе данных. Предположим, что мы тестируем метод Fill() класса Order. Вот пример тестирования с использованием стаба для state — based тестирования:

Пара пояснений по коду . Сначала мы создаем объект типа Order, затем – стаб для класса Warehouse . После этого мы при помощи mock-фреймворка говорим, что при вызове метода HasInventory с определенными параметрами этот метод должен нам вернуть true. Аналогичным образом переопределяем поведение метода Remove (а то еще вызовет реальный и будет бяка). Далее идет вызов метода Fill() с переданным стабом, после чего проверяется, что свойство IsFilled установлено в true. Как видите, ничего сложного. Однако данный тест обладает некоторыми недостатками. Во-первых, непонятно, что делать, если в тестируемом объекте нет свойства, аналогичного IsFilled. Как проверять правильность выполнения кода? Во-вторых, непонятно, что случится, если программист удалит или закомментирует вызов следующей строчки в коде метода Fill():

IsFilled устанавливается в true , тест проходит, но код-то уже не работает!

Обе эти проблемы легко разрешаются, если мы воспользуемся interaction тестированием с использованием мока. Для этого напишем другой тест:

Начало теста аналогичное, затем идет создание мока Warehouse, после чего идет несколько вызовов метода Expect с теми же параметрами, что и в предыдущем тесте. При помощи этого метода мы говорим моку, что мы ожидаем вызова этих методов с такими параметрами и нам в ответ на их вызовы нужно вернуть такие-то значения. Затем идет вызов метода Replay(), который переводит мок из режима записи ожиданий в режим их проверки, то есть запуска тестового метода. Все моки имеют несколько режимов работы ( Record , Replay , Verify ), это распространенный подход. Далее непосредственно запуск, проверка IsFilled и вызов нового для нас метода VerifyAllExpectations(). Последний как раз и делает всю работу по проверке вызовов методов, параметров и т.д. Теперь, если метод Remove оказался закомментированным, тест не пройдет. Кроме того, нам уже не так важна проверка состояния объекта Order. Если бы свойства IsFilled не было, ничего бы не изменилось, а так мы лишь проверяем, что оно было установлено в соответствии с алгоритмом. Теперь немного поэкспериментируем с кодом. Что, если мы уберем второй Expect или поменяем их местами? Есть несколько режимов строгости проверки, которые задаются через конструктор класса Mock, который также можно использовать для создания мока. В Rhino Mocks есть три уровня строгости: Loose, Strict и Default (Loose). В Loose-режиме мок проверяет лишь то, что все ожидаемые методы были вызваны из тестируемого метода, в то время как в Strict-режиме проверяется также, что не было любых других вызовов и что порядок вызову соответствует порядку ожиданий. В других фреймворках иногда есть и другие режимы. Таким образом, в нашем случае при изменении порядка тест бы прошел, но в Strict-режиме – уже нет. Еще один момент, который показывает отличие методов Expect от методов Stub (в моке они также доступны): методы, зарегистрированные в моке при помощи метода Stub невидимы для метода VerifyAllExpectations. То есть, если нужна проверка вызовов – используйте Expect. Также стоит отметить, что при помощи дополнительных методов типа Return вы можете не только указывать возвращаемые значения, но еще генерировать exception’ы ( Throw ), вызывать настоящий метод ( CallOriginalMethod ), задавать ограничения на параметры ( Constraints ), вызывать дополнительные методы ( Callback , Do ), работать со свойствами и событиями. В общем, список потрясающий.

Как видите, иногда удобно проверить состояние, иногда – вызовы и взаимодействие. Так что это не вопрос «или-или», это вопрос – «когда» 🙂 Удачного вам unit-тестирования!

Simply JavaScript: a straightforward intro to Mocking, Stubbing, and Interfaces

Sep 4 ・8 min read

Learning JavaScript with Justin Fuller (4 Part Series)

I like to think that I’m a simple guy, I like simple things. So whenever I sense complexity, my first reaction is to wonder if I can make things easier.

Before I transitioned to software development, I spent time as a sound engineer. I was recording bands and mixing live shows. I was even recording and mixing live shows for broadcast. During that time I talked with too many people who would always attempt to solve problems by purchasing some expensive, more complex equipment. Sadly the return on investment never seemed to be all it promised.

Instead of buying into the “more expensive, more complex is better” philosophy, I spent every evening learning the basics. I focused on fundamental skills. I learned how to use an equalizer to make a voice sound natural. I learned how to use a compressor to soften quick and loud sounds or to beef up thin sounds. It turned out that the return on investment for those hours was more than I ever hoped for!

I ended up favoring the simplest tools and I was very happy with the work I produced.

I believe the same principle can be applied to almost every aspect of life—finances, parenting, even software engineering.

As people, we naturally tend to look for flashy, popular solutions that promise to solve all of our problems (or at least to perfectly solve a single problem). We are misguided about these complex solutions. We’ve created complicated problems by not properly understanding the fundamentals of whatever we’re struggling with.

JavaScript Basics

We’ll be looking at basic programming concepts and how they can be applied to JavaScript. The goal here is to have code that is simpler, more flexible, easier to understand, and easier to test.

First, before introducing interfaces, I’d like to talk about a problem.

Mocking, stubbing, and mutating

Recently I was writing code that read files from the file system. The code worked great. To test it, I had to use a library that would stop my code from reading from the file system. My tests would have been too slow if I had let it do that. Plus I needed to simulate scenarios that would have been complicated to implement with the actual file system.

Historically I would have used a library like Proxyquire or Sinon. Proxyquire allows you to override the imports of a file. Sinon allows you to mutate methods on an object. You can use either or both of these to make your code easier to test. Although it would be better to use just one.

As an example, let’s pretend you have a module called “a”. Let’s also say that module “a” imports module “b”. Proxyquire works by importing module “a” and overwriting the exports of module “b”. It won’t affect other imports of module “b” elsewhere. Sinon works by mutating the exports of module “b”. It will affect every place that imports module “b”, so you must remember to restore it when you are done.

Why are stubs bad?

Neither of these options is great because they involve mutation. In software development, we want to avoid mutation when possible. because mutation leads to a decrease in predictability across an application.

One small mutation never seems like a big deal. But when there are many small mutations it becomes difficult to track which function is changing what value and when each mutation is being done.

There’s also the nuisance of lock-in. Both sinon and proxyquire will require you to update your tests if you change your file system library from fs to fs-extra-promise. In both cases, you’ll still be using the function readFileAsync. However, sinon and proxyquire will keep on trying to override fs.readFile.

What are the alternatives?

To solve this problem I followed a principle called Dependency Inversion. Instead of my module creating its dependencies, it will expect to be given its dependencies. This produces modules that are both easier to test and more flexible. They can also be made to work with many implementations of the same dependencies.

Not only have precious lines been saved in our code, but there is also no more worrisome mutation happening! The module will now accept readFileAsync rather than creating that function itself. The module is better because it’s more focused and has fewer responsibilities.

Where does the dependency go?

The dependencies have to be imported somewhere. In an application that follows dependency inversion, you should move the dependencies as far “out” as you can. Preferably you’d import them one time at the entry point of the application.

In the example, you saw that the dependencies were moved to the entry point of the application. Everything except index.js accepted an interface. This causes the application to be flexible, easy to change, and easy to test.

What else can Dependency Inversion do?

Now that you’ve fallen in love with dependency inversion I’d like to introduce you to some more of its power.

When your module accepts an interface, you can use that module with multiple implementations of that interface. This is a scenario where the libraries TypeScript and Flow can be useful. They’ll check that you’ve provided the correct interface.

An interface is simply a collection of methods and properties. So by saying that a module accepts an interface, I am saying that a module accepts an object that implements a set of methods and properties. The expectation is that the interfaces similarly implement different functionality.

A common interface you might know is the React component interface. In TypeScript it might look like this:

Please don’t despair if you didn’t understand everything in that interface. The point is that a React Component has a predictable set of methods and properties that can be used to make many different components.

We are now beginning to venture into the territory of the Open-Closed Principle. It states that our software should be open for extension but closed for modification. This may sound very familiar to you if you’ve been building software with frameworks like Angular, or React. They provide a common interface that you extend to build your software.

Now, instead of relying on third-party interfaces for everything, you can begin to rely on your internal interfaces to create your software.

If you are writing a CRUD (create, read, update, delete) application, you can create an interface that provides the building blocks for your actions. Your modules can extend that interface to implement the business logic and use-cases.

If you are writing an application that performs tasks, you can build a task interface that provides the building blocks for different tasks. Each task can accept that task interface and extend it.

Dependency inversion and the Open-Closed principle allow you to write more reusable, testable, and predictable software. You’ll no longer have a jumbled mess of spaghetti code. Instead, you’ll have a uniform group of modules that follow the same pattern.

Many Implementations

There’s one more benefit to accepting an interface. You can implement that interface in many different ways.

Here’s my favorite example of this. Imagine that you have an interface for a CRUD application. You could have one interface that implements the database storage. This is great, but what if the database reads or writes become slow? You could also write a faster implementation that uses Redis or Memcached to speed up the response times. The only change you’ll have to make is writing a new interface. There will be no need to update business logic or anything else.

You could consider React and React-Native to be popular examples of this. They both use the same React component and React DOM interfaces, but they implement them differently. Even inside React Native, there is an implementation for both IOS and Android. Multiple implementations allow you to write your logic once and execute it in multiple ways.

Now what?

Now that you’ve learned about dependency inversion and the open-closed principle, it’s time for you to go and apply it in your code. Don’t write any imports in the next module you write. Instead, allow it to accept an interface. In your tests, you’ll be able to avoid third-party libraries that mutate your dependencies! Then try to start identifying where common interfaces can be used. You’ll slowly but surely create a better application!

This is a repost, the post originally appeared on www.justindfuller.com.

Hi, I’m Justin Fuller. I’m so glad you read my post! I need to let you know that everything I’ve written here is my own opinion and is not intended to represent my employer in any way. All code samples are my own and are completely unrelated to my employer’s code.

I’d also love to hear from you, please feel free to connect with me on Github or Twitter. Thanks again for reading!

Learning JavaScript with Justin Fuller (4 Part Series)

Добавить комментарий