Вступление

В первой части мы обрисовали в общих чертах, каковы были цели серии, а также установили наши инструменты. Во второй части мы собираемся начать говорить о модульном тестировании 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, а также о том, когда вы будете использовать каждый тип и как их протестировать.

В следующей части мы поговорим о тестировании реагирующих компонентов.