Вступление
В первой части мы обрисовали в общих чертах, каковы были цели серии, а также установили наши инструменты. Во второй части мы собираемся начать говорить о модульном тестировании JavaScript в ES6 с использованием Jest и о том, где сходство начинается и заканчивается тем, с чем вы, возможно, уже знакомы в Visual Studio, и о тестировании классических языков ООП, таких как C #.
В этой части мы рассмотрим:
- Общедоступные и частные в JavaScript
- Зависимости и упрощение тестирования на себе
- Моки JavaScript с Jest и их создание
- Функции тестирования и побочные эффекты
Общедоступные и частные в JavaScript
Одна из первых вещей, о которой мы хотим знать, когда впервые переходим к JavaScript с языка .Net, - это инкапсуляция. С точки зрения модульного тестирования, делая что-то общедоступным, а что-то еще частным, вы заключаете с классом договор о том, что «это то, что он может делать», и, в более широком смысле, общедоступные элементы - это ваш путь к тестированию тестируемой системы. Частные вещи - это детали реализации, вещи, которые известны только классу, вам и следующему разработчику, чтобы увидеть, как реализованы общедоступные вещи.
Не говоря уже об очевидном, в JavaScript нет публичных и частных ключевых слов. То, что у нас есть с JavaScript, - это область действия, а также то, что доступно и видно в других областях.
Чтобы продемонстрировать это, давайте посмотрим, как это было достигнуто до ES6.
Здесь мы создали то, что эквивалентно классу. Мы создали частную переменную и функцию, а также общедоступное свойство и функцию. Мы создали новый объект Foo и дважды обратились к общедоступному методу, один раз предоставив ему строку из общедоступного свойства, а другой заставив его использовать частную строковую переменную.
Теперь давайте посмотрим, как добиться того же поведения с помощью ES6.
Вышеупомянутый класс можно использовать так же, как и версию ES5. Но посмотрите, что случилось с нашей частной переменной и функцией, они больше не объявлены в теле класса. Разумеется, это сделает частную переменную и функцию доступными для всех?
Ну нет, код ES6 зависит от того, что вы импортируете и экспортируете из файла JavaScript. Чтобы использовать класс Foo, вам понадобится другой файл JavaScript, который выглядел бы примерно так:
Освоение ключевого слова export и понимание того, что есть и как использовать то, что экспортируется из файла JavaScript, является важной концепцией при тестировании и разработке для JavaScript. В ES5 мы занимались только областью действия, хотя это работает, это может стать громоздким при разработке больших приложений. В ES6 все более модульное, у нас все еще есть область видимости, а слова const
и let
позволяют нам создавать переменные, которые работают так же, как мы ожидали бы в .Net, но экспорт немного отличается.
Вы можете рассматривать файл JavaScript как класс, вы можете создавать в нем частные переменные и функции и экспортировать только тот класс, который вам нужен, как показано выше. Экспорт является гибким и позволит вам экспортировать что угодно. Если вы хотите разработать функциональный стиль, вы можете просто рассматривать файл JavaScript как библиотеку функций без каких-либо классов и просто экспортировать функции. Вы также можете просто экспортировать переменные, позволяющие создавать общие значения констант.
По сути, то, что вы экспортируете из файла JavaScript, является общедоступным. Однако у пользователя файла есть выбор: импортировать все или выборочно выбрать то, что он хочет, что отличается от использования класса или интерфейса в .Net. См. Https://developer.mozilla.org/en-US/docs/web/javascript/reference/statements/export
Зависимости и упрощение тестирования.
Итак, теперь, когда мы установили, что по крайней мере эквивалентно публичному и частному в мире .Net, давайте поговорим о зависимостях.
Мы установили, что с помощью public (или export
) мы говорим, что это то, что будет делать мой класс, это поведение и доступные данные, пока что все это хорошо.
К сожалению, с точки зрения тестирования это не очень хорошо. Причина, по которой это не очень хорошо, заключается в том, что поскольку import
, который вы сейчас используете для своего собственного класса или функции, эквивалентен using
в C #, где вы можете просто создать новый класс из пространства имен. Возможно, вы слышали выражение «где новое, там и клей». То есть вы только что соединили и жестко связали свой код с тем, что вы только что импортировали.
В целом принцип инверсии зависимостей гласит:
- Модули высокого уровня не должны зависеть от модулей низкого уровня. Оба должны зависеть от абстракций.
- Абстракции не должны зависеть от деталей. Детали (конкретные реализации) должны зависеть от абстракций.
Эти два предложения, по сути, можно подытожить, сказав: «Вы не должны зависеть от новых и новых классов, которые содержат детали реализации, вместо этого вы должны полагаться и использовать интерфейс или абстрактный класс, который реализуют ваши классы и могут быть заменены использованием полиморфизма. ».
Независимо от абстракций, ваш код становится связанным. Представьте на мгновение, что вы пишете API, и как часть этого API у вас есть контроллер, который имеет класс обслуживания, который по сути является оболочкой для компонентов внутри системы, таких как постоянство и сохранение данных в базе данных. Ваши классы и зависимости будут выглядеть так:
Если вы решили протестировать либо контроллер, либо классы обслуживания, у вас не будет другого выбора, кроме как называть это интеграционным тестом. Невозможно независимо протестировать отдельный компонент без компонентов более низкого уровня. Вместо этого принципы DI учат нас и то, что мы обычно делаем в .Net, - это иметь что-то похожее на это:
Здесь мы перевернули поток управления. Конечно, у нас есть еще кое-что, но при тестировании контроллера я могу протестировать его независимо от класса обслуживания, создав заглушку или макет IService
. Я также получаю больше контроля над ожидаемыми результатами, учитывая фальшивый объект, который я создаю и использую при тестировании контроллера.
Это отлично подходит для того, к чему мы привыкли, но сейчас мы находимся на земле JavaScript, а у JavaScript нет интерфейсов. Означает ли это, что мы застряли в зависимости от низкоуровневых деталей?
Нет, совсем нет. В Jest есть встроенные способы получения тех же функций, что и при использовании Moq в .Net, но он просто делает это так, как вы сначала не ожидали, о чем мы поговорим дальше. Но прежде чем мы это сделаем, я надеюсь, что я понял это, импортировав и напрямую используя что-то, что было экспортировано, вы привязываете себя к этому, не используя некоторые уловки из Jest.
Как уже упоминалось, у JavaScript нет интерфейсов. Что у него есть, так это утиная типизация, что означает, что если у вас есть один объект, на котором есть метод с именем doStuff
, и у вас есть другой объект, который также имеет метод с именем doStuff
, эти объекты и методы взаимозаменяемы, и JavaScript не будет жаловаться на то, что они другого класса. Например:
В приведенном выше фрагменте оба регистратора взаимозаменяемы, и MyImportantClass
не важно, какой класс он получит, просто он будет использовать что-то, на котором есть метод журнала. Это внедрение зависимости для JavaScript.
Гораздо лучше внедрить зависимость в класс, чем импортировать и использовать компонент в вашем файле JavaScript. Эти зависимости, конечно, нужно куда-то импортировать, и они каким-то образом должны попасть в класс, который будет их использовать. Если вы используете React, вы уже мыслите модульно и должны думать о разделении и разбиении компонентов на компоненты. С этой целью вам следует подумать о невизуальных компонентах контейнера и визуальных компонентах.
Компоненты вашего контейнера - это, по сути, ваш составной корень. Это компоненты, которые импортируют все зависимости и внедряют их в ваши визуальные компоненты. Я настоятельно рекомендую вам прочитать https://medium.com/@dan_abramov/smart-and-dumb-components-7ca2f9a7c7d0. Если вы используете React и Redux вместе, вы можете использовать такие методы, как mapStateToProps
и mapDispatchToProps
, которые позволяют вставлять зависимости в свойства компонента.
Важно понимать, что чем больше вы импортируете и используете, тем больше насмешек с Jest вам нужно будет делать позже. В конце концов, это не обязательно плохо, в конце концов, Jest может легко внедрить эти зависимости за вас, но, на мой взгляд, проще использовать утиную печать и вручную создавать собственные макеты, внедряя их в качестве зависимостей, чем использовать только Jest. Вы должны оставаться прагматичными, просто хорошо осознавать эту связь, возникающую при импорте.
Моки JavaScript с Jest и их создание
Прежде чем мы перейдем к мокам с Jest, давайте кратко рассмотрим набор тестов и модульный тест.
Выше у нас есть один набор тестов с одним тестом, это минимум, который вам нужен для настройки теста с Jest. Вы получаете те же методы настройки и удаления, которые можно было бы ожидать от чего-то вроде NUnit, так beforeEach
, afterEach
и т. Д. (См. Https://jestjs.io/docs/en/setup-teardown.html также для утверждений с ожиданием https://jestjs.io/docs/en/expect )
Теперь для примера использования моков рассмотрим практический пример.
Выше приведен пример редуктора redux, используемого для изменения локали в приложении Redux, но это не важно. Важно то, что у нас есть две зависимости в нашем файле: одна для defaultLocalProvider
, а другая для localActionTypes
. Меня не слишком беспокоит localeActionTypes
, но меня больше беспокоит defaultLocaleProvider
. У него есть детали реализации, которые зависят от получения языкового стандарта браузера и его синтаксического анализа в формате, который позже может быть использован для определения правильного файла локализации. Здесь нет никакого отношения к нашему редуктору.
Нам нужна фиктивная реализация нашего defaultLocaleProvider
, чтобы вернуть то, что мы можем протестировать, что-то надежное, чего мы можем ожидать в тесте.
Что нам нужно сделать, так это сказать jest, что нужно искать имитацию реализации defaultLocaleProvider
при запуске теста:
Выше мы импортировали localeReducer
, это наша тестируемая система. Но под ним у нас есть новая строка jest.mock
. Это указывает Jest искать фиктивную реализацию defaultLocaleProvider
(которую localeReducer
импортирует в свой собственный файл), когда defaultLocaleProvider
является зависимостью от нашей тестируемой системы.
Макет для defaultLocaleProvider
выглядит так:
Как видите, это простая функция, которая просто возвращает строку «fakeLocale». Когда наш редуктор запускается в первый раз в нашем тесте, он получит свое начальное состояние (локаль) от этой функции, установив в качестве локали значение «fakeLocale», и мы сможем проверить это в нашем тесте.
Однако для того, чтобы это работало, нам нужна новая папка, в которой наш defaultLocaleProvider
находится в нашем решении под названием __mocks__
, а внутри у нас должен быть другой файл, названный так же, как файл, который мы издеваемся, вот так:
Эта папка макетов - это специальная папка, в которую Jest заглядывает, когда ищет фиктивные реализации. Как правило, вы выполняете этот тип имитации при имитации модуля.
jest.mock
имеет преимущество, так как он автоматически устанавливает все экспорты модуля в фиктивную функцию, что позволяет позже перезаписать их.
Давайте посмотрим на альтернативу jest.mock
в jest.SpyOn
.
Мы собираемся попробовать и протестировать этот простой фрагмент кода:
Здесь мы видим это там, где нет зависимостей как таковых, а просто функция с объектом навигатора. Затем он пытается получить язык из навигатора, иначе, если ничего не найдено, он вернет «en».
При использовании jest.spyOn
нам не нужна фиктивная реализация в том же каталоге. Вместо этого мы можем сделать что-то вроде этого:
Здесь мы импортировали тестируемую систему navigatorLocale
. Мы пользуемся преимуществом beforeAll
, который настраивает langaugesMock
как имитацию, а затем beforeEach
сбрасывает возвращаемое значение на null.
В первом тесте у нас нет моков, и мы тестируем, что возвращается значение по умолчанию («en»). Во втором мы устанавливаем возвращаемое значение как массив, тогда, как мы знаем из кода, должен быть возвращен первый элемент из массива languages, который в нашем случае - «ja».
Если бы существовало эмпирическое правило для jest.mock
vs jest.spyOn
, я бы, вероятно, предложил использовать jest.mock
, когда требуемый макет требовался для нескольких файлов (имитирующие модули), и jest.spyOn
, когда он был нужен в одном случае.
Функции тестирования и побочные эффекты
Возможно, указанные выше jest.mock
и jest.spyOn
слишком тяжелы для того, что вам нужно, или они могут не точно соответствовать тому, что вы хотели бы проверить на наличие побочных эффектов, вещей, которые функция использовала или видоизменила, к которым у вас может или не может быть доступа.
Представьте на мгновение, что вы пишете компонент React, который инкапсулирует кнопку HTML следующим образом:
В этом простом примере вы хотите убедиться, что функция onClick
вызывается при нажатии кнопки. Это побочный эффект происходящего события.
Вы можете сделать это как с jest.spyOn
, так и с Jest.fn
. jest.spyOn
имеет другое применение, заключающееся в том, что вам не нужно заменять детали реализации вызываемой функции, а просто попросить Jest посмотреть функцию, чтобы вы могли позже проверить, была ли она вызвана.
jest.fn
не позволяет восстанавливать исходные детали имитируемой функции, но это полезно знать, поскольку jest.spyOn
- это синтаксический сахар, который использует jest.fn
.
Возвращаясь к нашему примеру выше, мы можем протестировать этот компонент следующим образом:
Здесь мы используем методы, специфичные для тестирования компонентов React, но самое важное, что нужно отметить, это то, что мы внедрили фиктивную функцию с именем mockOnClick
. Затем мы смоделировали щелчок по компоненту. Наконец, мы ожидали, что будет вызвана функция mockOnClick
. Я также мог проверить, сколько раз вызывалась эта функция.
Чтобы добавить фиктивную реализацию в jest.spyOn
, вы должны использовать свойство mockImplementation
следующим образом:
Добавление вашей функции в качестве параметра к функции, но с jest.fn
при создании функции вы можете добавить ее в качестве первого параметра при создании:
Что использовать, зависит от вас и от обстоятельств теста.
Заключение
Во второй части мы рассмотрели инкапсуляцию с помощью JavaScript и ее важность, мы также поговорили о принципе инверсии зависимостей и о том, как мы обычно реализуем его в .Net, а также о трудностях, связанных с тем, чтобы сделать то же самое с JavaScript и ES6, чтобы узнать о наших зависимостях.
Наконец, мы поговорили о создании трех типов моков Jest, а также о том, когда вы будете использовать каждый тип и как их протестировать.
В следующей части мы поговорим о тестировании реагирующих компонентов.