Статьи

Как создать работоспособные спецификации JavaScript

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

Простой файл спецификации JavaScript

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

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

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

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

Формат спецификации

Каждый файл спецификации экспортирует одну строку из литерала шаблона . Первая строка может быть взята в качестве заголовка спецификации. Шаблонный литерал позволит нам встраивать выражения JS между строкой, и каждое выражение будет представлять утверждение. Чтобы идентифицировать каждое утверждение, мы можем начать строку с отличительного символа, в этом случае мы можем использовать комбинацию символа бара ( | ) и тире ( - ), который напоминает символ турникета, который иногда можно найти как символический представление для логических утверждений.

Ниже приведен пример с некоторыми пояснениями по его использованию:

 const dependency = require('./dependency') module.exports = ` Example of a Specification File This project allows to test JavaScript programs using specification files. Every *.spec.js file exports a single template literal that includes a general explanation of the file being specified. Each file represents a logical component of a bigger system. Each logical component is composed of several units of functionality that can be tested for certain properties. Each one of this units of functionality may have one or more assertions. Each assertion is denoted by a line as the following: |- ${dependency} The dependency has been loaded and the first assert has been evaluated. Multiple assertions can be made for each file: |- ${false} This assertion will fail. |- ${2 + 2 === 4} This assertion will succeed. The combination of | and - will form a Turnstile ligature (|-) using the appropriate font. Fira Code is recommended. A Turnstile symbol was used by Gottlob Frege at the start of sentenses being asserted as true. The intended usage is for specification-first software. Where the programmer defines the high level structure of a program in terms of a specification, then progressively builds the parts conforming that specification until all the tests are passed. A desired side-effect is having a simple way to generate up-to-date documentation outside the code for API consumers. ` 

Теперь перейдем к структуре высокого уровня нашей программы.

Структура нашей программы

Вся структура нашей программы может быть определена в нескольких строках кода и без каких-либо зависимостей, кроме двух библиотек Node.js, для работы с файловой системой ( fs ) и путями к каталогам ( path ). В этом разделе мы определим только структуру нашей программы, определения функций будут даны в следующих разделах.

 #!/usr/bin/env node const fs = require('fs') const path = require('path') const specRegExp = /\.spec\.js$/ const target = path.join(process.cwd(), process.argv[2]) // Get all the specification file paths // If a specification file is provided then just test that file // Otherwise find all the specification files in the target directory const paths = specRegExp.test(target) ? [ target ] : findSpecifications(target, specRegExp).filter(x => x) // Get the content of each specification file // Get the assertions of each specification file const assertionGroups = getAssertions(getSpecifications(paths)) // Log all the assertions logAssertions(assertionGroups) // Check for any failed assertions and return an appropriate exit code process.exitCode = checkAssertions(assertionGroups) 

Поскольку это также точка входа в наш CLI ( интерфейс командной строки ), нам нужно добавить первую строку, shebang , которая указывает, что этот файл должен выполняться программой node . Нет необходимости добавлять определенную библиотеку для обработки параметров команды, поскольку нас интересует только один параметр. Однако вы можете рассмотреть другие варианты, если планируете значительно расширить эту программу.

Чтобы получить целевой тестовый файл или каталог, мы должны объединить путь, по которому была выполнена команда (используя process.cwd() ), с аргументом, предоставленным пользователем, в качестве первого аргумента при выполнении команды (используя process.argv[2] ). Вы можете найти ссылку на эти значения в документации Node.js для объекта процесса . Таким образом мы получаем абсолютный путь к целевому каталогу / файлу.

Теперь, первое, что нам нужно сделать, это найти все файлы спецификации JavaScript. Как видно из строки 12, мы можем использовать условный оператор для обеспечения большей гибкости: если пользователь предоставляет файл спецификации в качестве цели, то мы просто используем этот путь к файлу напрямую, в противном случае, если пользователь предоставляет путь к каталогу, мы должны найти все файлы, которые соответствуют нашему шаблону, как определено константой specRegExp , мы делаем это с помощью функции findSpecifications которую мы определим позже. Эта функция будет возвращать массив путей для каждого файла спецификации в целевом каталоге.

В строке 18 мы определяем константу assertionGroups в результате объединения двух функций getSpecifications() и getAssertions() . Сначала мы получаем содержимое каждого файла спецификации, а затем извлекаем из них утверждения. Мы определим эти две функции позже, а пока просто отметим, что мы используем вывод первой функции в качестве параметра второй, что упрощает процедуру и устанавливает прямую связь между этими двумя функциями. Хотя мы могли бы иметь только одну функцию, разделив их, мы можем получить более полное представление о том, каков реальный процесс, помните, что программа должна быть ясной для понимания; просто заставить его работать недостаточно.

Структура константы assertionsGroup будет выглядеть следующим образом:

assertionGroup[specification][assertion]

Затем мы регистрируем все эти утверждения для пользователя как способ сообщить о результатах с помощью функции logAssertions() . Каждое утверждение будет содержать результат ( true или false ) и небольшое описание, мы можем использовать эту информацию, чтобы дать специальный цвет для каждого типа результата.

Наконец, мы определяем код выхода в зависимости от результатов утверждений. Это дает процессу информацию о том, как закончилась программа: процесс прошел успешно или что-то не получилось? , Код выхода 0 означает, что процесс завершился успешно, или 1 если что-то не удалось, или в нашем случае, когда по крайней мере одно утверждение не удалось.

Поиск всех файлов спецификаций

Чтобы найти все файлы спецификации JavaScript, мы можем использовать рекурсивную функцию, которая пересекает каталог, указанный пользователем в качестве параметра для CLI. Во время поиска каждый файл должен проверяться с помощью регулярного выражения, которое мы определили в начале программы ( /\.spec\.js$/ ), которое будет соответствовать всем путям файлов, заканчивающимся на .spec.js .

 function findSpecifications (dir, matchPattern) { return fs.readdirSync(dir) .map(filePath => path.join(dir, filePath)) .filter(filePath => matchPattern.test(filePath) && fs.statSync(filePath).isFile()) } 

Наша функция findSpecifications берет целевой каталог ( dir ) и регулярное выражение, которое идентифицирует файл спецификации ( matchPattern ).

Получение содержания каждой спецификации

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

 function getSpecifications (paths) { return paths.map(path => require(path)) } 

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

Извлечение утверждений из текста

На данный момент у нас есть массив с содержимым каждого файла спецификации и их утверждениями, уже оцененными. Мы используем индикатор турникета ( |- ), чтобы найти все эти утверждения и извлечь их.

 function getAssertions (specifications) { return specifications.map(specification => ({ title: specification.split('\n\n', 1)[0].trim(), assertions: specification.match(/^( |\t)*(\|-)(.|\n)*?\./gm).map(assertion => { const assertionFragments = /(?:\|-) (\w*) ((?:.|\n)*)/.exec(assertion) return { value: assertionFragments[1], description: assertionFragments[2].replace(/\n /, '') } }) })) } 

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

 { title: <String: Name of this particular specification>, assertions: [ { value: <Boolean: The result of the assertion>, description: <String: The short description for the assertion> } ] } 

title задается первой строкой строки спецификации. Затем каждое утверждение сохраняется в виде массива в ключе assertions . value представляет результат утверждения как логическое значение . Мы будем использовать это значение, чтобы узнать, было ли утверждение успешным или нет. Кроме того, описание будет показано пользователю как способ определить, какие утверждения были успешными, а какие — нет. Мы используем регулярные выражения в каждом случае.

Регистрация результатов

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

 function logAssertions(assertionGroups) { // Methods to log text with colors const ansiColor = { blue: text => console.log(`\x1b[1m\x1b[34m${text}\x1b[39m\x1b[22m`), green: text => console.log(`\x1b[32m  ${text}\x1b[39m`), red: text => console.log(`\x1b[31m  ${text}\x1b[39m`) } // Log the results assertionGroups.forEach(group => { ansiColor.blue(group.title) group.assertions.forEach(assertion => { assertion.value === 'true' ? ansiColor.green(assertion.description) : ansiColor.red(assertion.description) }) }) console.log('\n') } 

Мы можем отформатировать наш ввод с цветами в зависимости от результатов. Чтобы отобразить цвета на терминале, нам нужно добавить управляющие коды ANSI . Чтобы упростить их использование в следующем блоке, мы сохранили каждый цвет в качестве методов объекта ansiColor .

Сначала мы хотим показать заголовок спецификации, помните, что мы используем первое измерение массива для каждой спецификации, которое мы назвали его group (утверждений). Затем мы записываем все утверждения в зависимости от их значения, используя их соответствующий цвет: зеленый для утверждений, которые были оценены как true и красный для утверждений, которые имели другое значение. Обратите внимание на сравнение, мы проверяем true как строку , так как мы получаем строки из каждого файла.

Проверка результатов

Наконец, последний шаг — проверить, все ли тесты прошли успешно или нет.

 function checkAssertions (assertionGroups) { return assertionGroups.some( group => group.assertions.some(assertion => assertion.value === 'false') ) ? 1 : 0 } 

Мы проверяем каждую группу утверждений (спецификацию), чтобы '``false``' является ли хотя бы одно значение '``false``' используя метод some() Array . Мы вложили два из них, потому что у нас есть двумерный массив.

Запуск нашей программы

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

 "scripts": { "test": "node index.js test" } 

… Где test — это имя каталога, в который вы включили образец файла спецификации.

При запуске команды npm test вы должны увидеть результаты с соответствующими цветами.

Последние слова

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

  • Программное обеспечение может быть простым и полезным одновременно.
  • Мы можем создавать свои собственные инструменты, если мы хотим что-то другое, нет никаких причин для соответствия.
  • Программное обеспечение — это больше, чем «заставить его работать», но и для обмена идеями.
  • Иногда мы можем что-то улучшить, просто изменив точку зрения. В этом случае формат файлов спецификации: просто простая строка!

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

Вы можете найти исходный код, используемый в этой статье здесь .