Home  >  Article  >  Web Front-end  >  A detailed explanation of how to build a runnable JavaScript specification

A detailed explanation of how to build a runnable JavaScript specification

青灯夜游
青灯夜游forward
2020-10-23 17:57:181684browse

A detailed explanation of how to build a runnable JavaScript specification

Programming is more than just giving instructions to a computer on how to complete a task. It is also about communicating ideas in a precise way to other people, even your future self. Such communication can have multiple goals, perhaps to share information, or just to make revision easier—which is difficult to do if you don't understand or remember what was done a long time ago.

When we write software, we also need to ensure that the code has the intended functionality. While there are formal ways of defining semantics, the simplest and fastest (but less rigorous) approach is to put the feature to use and see if it produces the expected results.

Most developers are familiar with these practices: code documentation as comments to clarify the goal of a block of code, and a series of tests to ensure that the function gives the desired output.

But usually documentation and testing are done in different steps. By unifying these practices, we can provide a better experience for anyone involved in project development. This article explores the implementation of a simple program that can run a JavaScript specification suitable for both documentation and testing.

We will build a command line interface that will look for all specification files in a directory, extract all assertions found in each specification, calculate their results, and finally display which assertions failed and which ones failed Passed.

Format of the specification

Each specification file will export a string from the template text. The first line can serve as the title of the specification. Template literals will allow us to embed JS expressions between strings, and each expression will represent an assertion. To identify each assertion, we can start the line with a unique character.

In this example, we can use a combination of the bar character (|) and a dash (-). The dash is similar to the revolving door symbol, which can sometimes be represented as a symbol for logical assertions.

Here's an example with some explanation of its usage:

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.
`

Now let's move on to the high-level structure of our program.

Structure of our program

The entire structure of our program can be defined in a few lines of code, in addition to using two Node.js libraries to handle the file system (fs ) and directory path (path), there are no dependencies. In this section we only define the structure of the program, the function definitions will be given in the next section.

#!/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)

Because this is also the entry point of our CLI (Command Line Interface), we need to add the first line of the shebang, which indicates that this file should be executed by the node program. There is no need to add a specific library to handle command options since we are only interested in a single parameter. However, if you plan to expand this program in a significant way, you may consider other options.

To obtain the target test file or directory, we must combine the path to the executed command (using process.cwd()) with the parameters provided by the user as the first parameter when executing the command (using process.argv[2 ])connect them.

You can find references to these values ​​in the Node.js documentation for the process object. By this method we get the absolute path of the target directory/file.

Now, the first thing we need to do is find all the JavaScript specification files. As shown in line 12, we can use conditional operators to provide more flexibility: if the user provides a canonical file as the target then we just use the file path directly.

Otherwise, if the user provides a directory path then we must find all files that match the pattern defined by specRegExp, which we will define later using the findSpecifications function. This function will return an array of paths to each specification file in the target directory.

In line 18, we define the assertionGroups constant by combining the two functions getspecification() and getassertion(). First get the contents of each specification file and then extract assertions from it.

We will define these two functions later, for now just note that we use the output of the first function as the parameter of the second function, thus simplifying the process and between the two functions A direct connection was established.

While we can have just one function, and by splitting them we can better understand what the actual process is, remember that the program should be clear and understandable; just being that is not enough.

The assertionsGroup constant is structured as follows:

assertionGroup[specification][assertion]

Next, we log all these assertions to the user log so that the results can be reported using the logassertion() function. Each assertion will contain the result (true or false) and a small description, and we can use this information to give each type of result a special color.

Finally, we define the exit code based on the result of the assertion. This will provide the process with information about how the program ended: did the process succeed or fail? An exit code of 0 means the process exited successfully, 1 if it failed, or in our example, 1 when at least one assertion failed .

Find all specification files

要找到所有的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())
}

我们的findspecification函数接受一个目标目录(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>
    }
  ]
}

标题是用规范字符串的第一行设置的。然后,每个断言都作为数组存储在断言键中。该值将断言的结果表示为布尔值。我们将使用这个值来知道断言是否成功。

此外,描述将显示给用户,作为识别哪些断言成功和哪些断言失败的方法。我们在每种情况下都使用正则表达式。

记录结果

我们沿着程序构建的数组现在有一系列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 === &#39;true&#39;
        ? ansiColor.green(assertion.description)
        : ansiColor.red(assertion.description)
    })
  })
 
  console.log(&#39;\n&#39;)
}

我们可以根据结果使用颜色来格式化输入。为了在终端上显示颜色,我们需要添加ANSI转义码。为了在下一个块中简化它们的用法,我们将每种颜色保存为ansiColor对象的方法。

首先,我们要显示规范的标题,请记住,我们为每个规范使用数组的第一个维度,并将其命名为一组(断言)。然后,我们使用它们各自的颜色根据它们的值记录所有断言:绿色表示计算为true的断言,红色表示具有其他值的断言。

注意比较,我们检查true是否为字符串,因为我们从每个文件接收字符串。

检查结果

最后,最后一步是检查所有测试是否成功。

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

我们使用数组的some()方法检查每个断言组(规范),看看是否至少有一个值是' ' ' false ' ' '。我们嵌套了其中的两个因为我们有一个二维数组。

运行我们的程序

此时,我们的CLI应准备好运行一些JavaScript规范,并查看是否拾取并评估了断言。在test目录中,您可以从本文开头复制规范示例,并将以下命令粘贴到您的文件中:package.json

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

其中test是包含示例规范文件的目录的名称。

当运行npm test命令时,您应该看到使用它们各自颜色的结果。

相关免费学习推荐:js视频教程

更多编程相关知识,请访问:编程入门!!

The above is the detailed content of A detailed explanation of how to build a runnable JavaScript specification. For more information, please follow other related articles on the PHP Chinese website!

Statement:
This article is reproduced at:sitepoint.com. If there is any infringement, please contact admin@php.cn delete