Функциональное программирование и тестирование. Может быть, вы дали им попробовать в одиночку, но почему-то вы никогда не стали частью вашей обычной практики. Они могут показаться невинными сами по себе, но совместное тестирование и функциональное программирование могут создать непреодолимое искушение, почти вынуждая вас писать более чистый, надежный и более понятный код.
Что ж, хорошая новость заключается в том, что совместная работа с обоими методами может дать некоторые реальные преимущества. На самом деле, как только вы попробуете, насколько сладкой может быть эта комбинация, вы можете почувствовать себя настолько же зависимым от нее, как и я, и я был бы готов поспорить, что вы вернетесь еще.
В этой статье я познакомлю вас с принципами тестирования функционального JavaScript. Я покажу вам, как начать работу с фреймворком Jasmine и создать чистую функцию, используя подход, основанный на тестировании.
Зачем тестировать?
Тестирование заключается в том, чтобы убедиться, что код в вашем приложении выполняет то, что вы ожидаете, и продолжает делать то, что вы ожидаете, когда вы вносите изменения, чтобы у вас был работающий продукт, когда вы закончите. Вы пишете тест, который определяет ожидаемую функциональность при определенных обстоятельствах, запускаете этот тест для кода и, если результат не соответствует тесту, вы получаете предупреждение. И вы продолжаете получать это предупреждение, пока не исправите свой код.
Тогда вы получите награду.
И да, это заставит вас чувствовать себя хорошо.
Тестирование имеет много разновидностей, и есть место для здоровой дискуссии о том, где нарисованы границы, но в двух словах:
- Модульные тесты проверяют функциональность изолированного кода
- Интеграционные тесты проверяют поток данных и взаимодействие компонентов
- Функциональные тесты смотрят на поведение всего приложения
Примечание: не отвлекайтесь на тот факт, что существует тип тестирования, называемый функциональным тестированием. Это не то, на чем мы остановимся в этой статье о тестировании функционального JavaScript. Фактически, подход, который вы будете использовать для функционального тестирования общего поведения приложения, вероятно, не сильно изменится, используете ли вы методы функционального программирования в своем JavaScript. Функциональное программирование действительно помогает, когда вы создаете свои модульные тесты.
Вы можете написать тест в любой точке процесса кодирования, но я всегда обнаруживал, что наиболее эффективно написать модульный тест перед написанием функции, которую вы планируете тестировать. Эта практика, известная как разработка через тестирование (TDD) , побуждает вас разбить функциональность вашего приложения до того, как вы начнете писать, и определите, какие результаты вы хотите получить для каждого раздела кода, сначала написав тест, а затем кодируя для получения этого результата. ,
Дополнительным преимуществом является то, что TDD часто вынуждает вас подробно беседовать с людьми, которые платят вам за написание ваших программ, чтобы убедиться, что то, что вы пишете, действительно то, что они ищут. В конце концов, легко сделать один тестовый проход. Трудно определить, что делать со всеми вероятными входами, с которыми вы столкнетесь, и правильно их обрабатывать, не ломая ничего.
Почему функциональный?
Как вы можете себе представить, то, как вы пишете свой код, во многом зависит от того, насколько просто его тестировать. Существуют некоторые шаблоны кода, такие как тесная связь поведения одной функции с другой или сильная зависимость от глобальных переменных, которые могут значительно затруднить выполнение кода модульным тестом. Иногда вам может понадобиться использовать неудобные методы, такие как «насмешка» поведения внешней базы данных или моделирование сложной среды выполнения, чтобы установить тестируемые параметры и результаты. Таких ситуаций не всегда можно избежать, но обычно можно выделить места в коде, где они требуются, чтобы остальную часть кода можно было легко протестировать.
Функциональное программирование позволяет вам независимо работать с данными и поведением в вашем приложении. Вы создаете свое приложение, создавая набор независимых функций, каждая из которых работает изолированно и не зависит от внешнего состояния. В результате ваш код становится почти самодокументированным, связывая вместе небольшие четко определенные функции, которые ведут себя согласованным и понятным образом.
Функциональное программирование часто противопоставляется императивному программированию и объектно-ориентированному программированию. JavaScript может поддерживать все эти методы и даже смешивать и сочетать их. Функциональное программирование может быть достойной альтернативой созданию последовательностей императивного кода, которые отслеживают состояние приложения на нескольких этапах, пока не будет возвращен результат. Или построение вашего приложения из взаимодействий между сложными объектами, которые инкапсулируют все методы, которые применяются к определенной структуре данных.
Как работают чистые функции
Функциональное программирование побуждает вас создавать приложение из крошечных, многократно используемых, компонуемых функций, которые выполняют только одну конкретную вещь и возвращают одно и то же значение для одного и того же ввода каждый раз. Такая функция называется чистой функцией . Чистые функции являются основой функционального программирования, и все они имеют следующие три качества:
- Не полагайтесь на внешнее состояние или переменные
- Не вызывать побочные эффекты и не изменять внешние переменные.
- Всегда возвращать один и тот же результат для одного и того же ввода
Еще одним преимуществом написания функционального кода является то, что он значительно упрощает модульное тестирование. Чем больше кода вы сможете тестировать модулем, тем удобнее вы можете рассчитывать на свою способность рефакторинга кода в будущем без нарушения основных функций.
Что делает функциональный код простым для тестирования?
Если вы думаете о концепциях, которые мы только что обсудили, вы, вероятно, уже понимаете, почему функциональный код легче тестировать. Написание тестов для чистой функции тривиально, потому что каждый вход имеет согласованный вывод. Все, что вам нужно сделать, это установить ожидания и запустить их против кода. Нет никакого контекста, который нужно устанавливать, нет никаких функциональных зависимостей, которые нужно отслеживать, нет изменяющегося состояния вне функции, которую нужно моделировать, и нет переменных внешних источников данных, которые нужно смоделировать.
Существует множество вариантов тестирования — от полноценных сред до библиотек утилит и простых способов тестирования. К ним относятся жасмин , мокко , энзим , джест и множество других. Каждый из них имеет свои преимущества и недостатки, лучшие варианты использования и лояльные последователи . Jasmine — это надежный фреймворк, который можно использовать в самых разных обстоятельствах, поэтому здесь кратко демонстрируется, как можно использовать Jasmine и TDD для разработки чистой функции в браузере.
Вы можете создать HTML-документ, который извлекает из библиотеки тестирования Jasmine либо локально, либо из CDN. Пример страницы, включающей библиотеку Jasmine и тестовый прогон, может выглядеть примерно так:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <title>Jasmine Test</title> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/jasmine/2.6.1/jasmine.min.css"> </head> <body> <script src="https://cdnjs.cloudflare.com/ajax/libs/jasmine/2.6.1/jasmine.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/jasmine/2.6.1/jasmine-html.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/jasmine/2.6.1/boot.min.js"></script> </body> </html>
Это включает библиотеку Jasmine, а также загрузочный скрипт Jasmine HTML и стиль. В этом случае тело документа пустое, ожидая проверки вашего JavaScript и ваших тестов Jasmine.
Тестирование функционального JavaScript — наш первый тест
Для начала давайте напишем наш первый тест. Мы можем сделать это в отдельном документе или включить его в элемент <script>
на странице. Мы собираемся использовать функцию description, определенную библиотекой Jasmine, чтобы описать желаемое поведение для новой функции, которую мы еще не написали.
Новая функция, которую мы собираемся написать, будет называться isPalindrome
и она будет возвращать true
если переданная строка является одинаковой вперед и назад, и возвращать false
противном случае. Тест будет выглядеть так:
describe("isPalindrome", () => { it("returns true if the string is a palindrome", () => { expect(isPalindrome("abba")).toEqual(true); }); });
Когда мы добавляем это в скрипт на нашей странице и загружаем его в браузер, мы получаем рабочую страницу отчета Jasmine, показывающую ошибку. Что мы и хотим на данный момент. Мы хотим знать, что тест выполняется и что он не проходит. Таким образом наш мозг, жаждущий одобрения, знает, что нам есть что исправить.
Итак, давайте напишем простую функцию на JavaScript с достаточной логикой, чтобы пройти наш тест. В этом случае это будет просто функция, которая выполнит один тест, вернув ожидаемое значение.
const isPalindrome = (str) => true;
Да, действительно. Я знаю, это выглядит нелепо, но держись там со мной.
Когда тестовый бегун снова, он проходит. Конечно. Но очевидно, что этот простой код не делает то, что мы можем ожидать от тестера палиндрома. Мы написали минимальный объем кода, который проходит тест. Но мы знаем, что наш код не сможет эффективно оценить палиндромы. На данный момент нам нужны дополнительные ожидания. Итак, давайте добавим еще одно утверждение к нашей функции описания:
describe("isPalindrome", () => { it("returns true if the string is a palindrome", () => { expect(isPalindrome("abba")).toEqual(true); }); it("returns false if the string isn't a palindrome", () => { expect(isPalindrome("Bubba")).toEqual(false); }); });
При перезагрузке нашей страницы результаты теста становятся красными и не работают. Мы получаем сообщения о том, в чем проблема, и результат теста становится красным.
Красный!
Наш мозг чувствует, что есть проблема.
Конечно, есть. Теперь было isPalindrome
что наша простая функция isPalindrome
которая просто возвращает true, не работает эффективно против этого нового теста. Итак, давайте обновим isPalindrome
добавив возможность сравнивать строку, переданную вперед и назад.
const isPalindrome = (str) => { return str .split("") .reverse() .join("") === str; };
Тестирование вызывает привыкание
Зеленый снова. Теперь это удовлетворяет. Вы получили этот небольшой прилив допамина, когда вы перезагрузили страницу?
С этими изменениями наш тест снова проходит. Наш новый код эффективно сравнивает прямую и обратную строки и возвращает true
если строка одинакова вперед и назад, и false
противном случае.
Этот код является чистой функцией, потому что он просто делает одну вещь, и делает это последовательно с постоянным входным значением, не создавая никаких побочных эффектов, не внося никаких изменений в переменные вне себя или полагаясь на состояние приложения. Каждый раз, когда вы передаете этой функции строку, она сравнивает прямую и обратную строки и возвращает результат независимо от того, когда и как она вызывается.
Вы можете видеть, насколько легко такая согласованность делает эту функцию для модульного тестирования. На самом деле, написание кода, управляемого тестами, может побудить вас писать чистые функции, потому что их гораздо проще тестировать и изменять.
И вы хотите удовлетворение от прохождения теста. Вы знаете, что делаете.
Рефакторинг чистой функции
На этом этапе добавление дополнительных функций, таких как обработка нестрокового ввода, игнорирование различий между заглавными и строчными буквами и т. Д., Тривиально. Просто спросите владельца продукта, как он хочет, чтобы программа работала. Поскольку у нас уже есть тесты для проверки того, что строки будут обрабатываться согласованно, теперь мы можем добавить проверку ошибок или приведение строк или любое другое поведение, которое нам нравится, для нестроковых значений.
Например, давайте посмотрим, что произойдет, если мы добавим тест для числа типа 1001, который может быть интерпретирован как палиндром, если бы он был строкой:
describe("isPalindrome", () => { it("returns true if the string is a palindrome", () => { expect(isPalindrome("abba")).toEqual(true); }); it("returns false if the string isn't a palindrome", () => { expect(isPalindrome("Bubba")).toEqual(false); }); it("returns true if a number is a palindrome", () => { expect(isPalindrome(1001)).toEqual(true); }); });
Это дает нам красный экран и снова провальный тест, потому что наша текущая функция isPalindrome
не знает, как обращаться с isPalindrome
входами.
Наступает паника. Мы видим красный. Тест не пройден.
Но теперь мы можем безопасно обновить его, чтобы обрабатывать не строковые входы, приводя их к строкам и проверяя их таким образом. Мы могли бы придумать функцию, которая выглядит примерно так:
const isPalindrome = (str) => { return str .toString() .split("") .reverse() .join("") === str.toString(); };
И теперь все наши тесты пройдены, мы видим зеленый, и этот сладкий, сладкий дофамин заливает наш мозг, управляемый тестами.
Добавив toString()
в цепочку оценки, мы можем разместить нестандартные входные данные и преобразовать их в строки перед тестированием. И что самое приятное, поскольку другие наши тесты все еще выполняются каждый раз, мы можем быть уверены, что мы не нарушили функциональность, которую мы получили ранее, добавив эту новую возможность в нашу чистую функцию. Вот что мы получаем в итоге:
Поиграйте с этим тестом и начните писать свои собственные, используя Jasmine или любую другую подходящую вам библиотеку.
Как только вы включите тестирование в свой рабочий процесс разработки кода и начнете писать чистые функции для модульного тестирования, вам может быть трудно вернуться к своей старой жизни. Но ты никогда не захочешь.
Эта статья была рецензирована Vildan Softic . Спасибо всем рецензентам SitePoint за то, что сделали контент SitePoint как можно лучше!