編程不僅僅是給計(jì)算機(jī)下達(dá)如何完成一項(xiàng)任務(wù)的指令,它還包括以一種精確的方式與他人交流思想,甚至是與未來(lái)的自己。這樣的交流可以有多個(gè)目標(biāo),也許是為了共享信息,或者只是為了更容易地修改—如果你不理解或不記得很久以前做過(guò)什么,那么就很難修改。
當(dāng)我們編寫(xiě)軟件時(shí),我們還需要確保代碼具有預(yù)期的功能。雖然有定義語(yǔ)義的正式方法,但是最簡(jiǎn)單、最快速(但不那么嚴(yán)格)的方法是將該功能投入使用,并查看它是否產(chǎn)生預(yù)期的結(jié)果。
大多數(shù)開(kāi)發(fā)人員都熟悉這些實(shí)踐:代碼文檔作為注釋來(lái)明確代碼塊的目標(biāo),以及一系列測(cè)試來(lái)確保函數(shù)給出所需的輸出。
但是通常文檔和測(cè)試是在不同的步驟中完成的。通過(guò)統(tǒng)一這些實(shí)踐,我們可以為參與項(xiàng)目開(kāi)發(fā)的任何人提供更好的體驗(yàn)。本文探討了一個(gè)簡(jiǎn)單的程序?qū)崿F(xiàn),該程序可以運(yùn)行既適用于文檔編寫(xiě)又適用于測(cè)試的JavaScript規(guī)范。
我們將構(gòu)建一個(gè)命令行界面,該界面將查找目錄中的所有規(guī)范文件,提取每個(gè)規(guī)范中找到的所有斷言,并計(jì)算它們的結(jié)果,最后顯示哪些斷言失敗了,哪些斷言通過(guò)了。
規(guī)范的格式
每個(gè)規(guī)范文件將從模板文本導(dǎo)出一個(gè)字符串。第一行可以作為規(guī)范的標(biāo)題。模板文字將允許我們?cè)谧址g嵌入JS表達(dá)式,每個(gè)表達(dá)式將表示一個(gè)斷言。要識(shí)別每個(gè)斷言,我們可以用一個(gè)獨(dú)特的字符開(kāi)始行。
在本例中,我們可以使用bar字符(|)和破折號(hào)(-)的組合,破折號(hào)類(lèi)似于旋轉(zhuǎn)門(mén)符號(hào),有時(shí)可以將其作為邏輯斷言的符號(hào)表示。
下面是一個(gè)例子,對(duì)它的用法做了一些解釋:
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. `
現(xiàn)在讓我們繼續(xù)我們程序的高層結(jié)構(gòu)。
我們程序的結(jié)構(gòu)
我們的程序的整個(gè)結(jié)構(gòu)可以在幾行代碼中定義,除了使用兩個(gè)Node.js庫(kù)來(lái)處理文件系統(tǒng)(fs)和目錄路徑(path)之外,沒(méi)有任何依賴(lài)關(guān)系。在本節(jié)中,我們只定義程序的結(jié)構(gòu),函數(shù)定義將在下一節(jié)中給出。
#!/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)
因?yàn)檫@也是我們的CLI(命令行接口)的入口點(diǎn),所以我們需要添加第一行shebang,它表示這個(gè)文件應(yīng)該由節(jié)點(diǎn)程序執(zhí)行。不需要添加特定的庫(kù)來(lái)處理命令選項(xiàng),因?yàn)槲覀冎粚?duì)單個(gè)參數(shù)感興趣。但是,如果您計(jì)劃以相當(dāng)大的方式擴(kuò)展此程序,則可以考慮其他選項(xiàng)。
要獲得目標(biāo)測(cè)試文件或目錄,我們必須將執(zhí)行命令的路徑(使用process.cwd())與用戶(hù)提供的參數(shù)作為執(zhí)行命令時(shí)的第一個(gè)參數(shù)(使用process.argv[2])連接起來(lái)。
您可以在process對(duì)象的Node.js文檔中找到對(duì)這些值的引用。通過(guò)這種方法,我們獲得了目標(biāo)目錄/文件的絕對(duì)路徑。
現(xiàn)在,我們要做的第一件事是找到所有的JavaScript規(guī)范文件。如第12行所示,我們可以使用條件運(yùn)算符來(lái)提供更大的靈活性:如果用戶(hù)提供了一個(gè)規(guī)范文件作為目標(biāo)然后我們就直接使用,文件路徑。
否則,如果用戶(hù)提供了一個(gè)目錄路徑然后我們必須找到相匹配的所有文件模式specRegExp定義的常數(shù),我們使用findSpecifications函數(shù)以后,我們將定義。這個(gè)函數(shù)將返回目標(biāo)目錄中每個(gè)規(guī)范文件的路徑數(shù)組。
在第18行中,我們通過(guò)組合兩個(gè)函數(shù)getspecification()和getassertion()來(lái)定義assertionGroups常量。首先獲取每個(gè)規(guī)范文件的內(nèi)容,然后從中提取斷言。
我們稍后將定義這兩個(gè)函數(shù),現(xiàn)在只需要注意,我們使用第一個(gè)函數(shù)的輸出作為第二個(gè)函數(shù)的參數(shù),從而簡(jiǎn)化了過(guò)程,并在這兩個(gè)函數(shù)之間建立了直接的聯(lián)系。
雖然我們可以只有一個(gè)函數(shù),通過(guò)拆分它們,我們可以更好地了解什么是實(shí)際的過(guò)程,但請(qǐng)記住,程序應(yīng)該清晰易懂;僅僅做到這一點(diǎn)是不夠的。
assertionsGroup常量的結(jié)構(gòu)如下:
assertionGroup[specification][assertion]
接下來(lái),我們將所有這些斷言記錄到用戶(hù)日志中,以便使用logassertion()函數(shù)報(bào)告結(jié)果。每個(gè)斷言將包含結(jié)果(true或false)和一個(gè)小描述,我們可以使用該信息為每種類(lèi)型的結(jié)果賦予特殊的顏色。
最后,我們根據(jù)斷言的結(jié)果定義退出代碼。這將向流程提供關(guān)于程序如何結(jié)束的信息:流程是成功的還是失敗了?退出碼為0表示進(jìn)程成功退出,如果失敗則為1,或者在我們的示例中,當(dāng)至少一個(gè)斷言失敗時(shí)為1。
查找所有規(guī)范文件
要找到所有的JavaScript規(guī)范文件,我們可以使用一個(gè)遞歸函數(shù),該函數(shù)遍歷用戶(hù)作為CLI參數(shù)指定的目錄。在搜索時(shí),應(yīng)該使用程序開(kāi)始時(shí)定義的正則表達(dá)式(/.spec.js$/)檢查每個(gè)文件,該表達(dá)式將匹配以.spec.js結(jié)尾的所有文件路徑。
function findSpecifications (dir, matchPattern) { return fs.readdirSync(dir) .map(filePath => path.join(dir, filePath)) .filter(filePath => matchPattern.test(filePath) && fs.statSync(filePath).isFile()) }
我們的findspecification函數(shù)接受一個(gè)目標(biāo)目錄(dir)和一個(gè)正則表達(dá)式,該正則表達(dá)式標(biāo)識(shí)規(guī)范文件(matchPattern)。
獲取每個(gè)規(guī)范的內(nèi)容
由于我們導(dǎo)出的是模板文本,因此獲取內(nèi)容和計(jì)算后的斷言非常簡(jiǎn)單,因此我們必須導(dǎo)入每個(gè)文件,當(dāng)它被導(dǎo)入時(shí),所有的斷言都將自動(dòng)進(jìn)行計(jì)算。
function getSpecifications (paths) { return paths.map(path => require(path)) }
使用map()函數(shù),我們使用節(jié)點(diǎn)的require函數(shù)將數(shù)組的路徑替換為文件的內(nèi)容。
從文本中提取斷言
此時(shí),我們有一個(gè)數(shù)組,其中包含每個(gè)規(guī)范文件的內(nèi)容,并且已經(jīng)計(jì)算了它們的斷言。我們使用旋轉(zhuǎn)門(mén)指示器(|-)來(lái)查找所有這些斷言并提取它們。
function getAssertions (specifications) { return specifications.map(specification => ({ title: specification.split('nn', 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 /, '') } }) })) }
這個(gè)函數(shù)將返回一個(gè)類(lèi)似的數(shù)組,但是用一個(gè)如下結(jié)構(gòu)的對(duì)象替換每個(gè)規(guī)范的內(nèi)容:
title: <String: Name of this particular specification>, assertions: [ { value: <Boolean: The result of the assertion>, description: <String: The short description for the assertion> } ] }
標(biāo)題是用規(guī)范字符串的第一行設(shè)置的。然后,每個(gè)斷言都作為數(shù)組存儲(chǔ)在斷言鍵中。該值將斷言的結(jié)果表示為布爾值。我們將使用這個(gè)值來(lái)知道斷言是否成功。
此外,描述將顯示給用戶(hù),作為識(shí)別哪些斷言成功和哪些斷言失敗的方法。我們?cè)诿糠N情況下都使用正則表達(dá)式。
記錄結(jié)果
我們沿著程序構(gòu)建的數(shù)組現(xiàn)在有一系列JavaScript規(guī)范文件,其中包含一列找到的斷言及其結(jié)果和描述,因此除了向用戶(hù)報(bào)告結(jié)果之外,沒(méi)有什么可做的。
{ function logAssertions(assertionGroups) { // Methods to log text with colors const ansiColor = { blue: text => console.log(`x1b[1mx1b[34m${text}x1b[39mx1b[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') }
我們可以根據(jù)結(jié)果使用顏色來(lái)格式化輸入。為了在終端上顯示顏色,我們需要添加ANSI轉(zhuǎn)義碼。為了在下一個(gè)塊中簡(jiǎn)化它們的用法,我們將每種顏色保存為ansiColor對(duì)象的方法。
首先,我們要顯示規(guī)范的標(biāo)題,請(qǐng)記住,我們?yōu)槊總€(gè)規(guī)范使用數(shù)組的第一個(gè)維度,并將其命名為一組(斷言)。然后,我們使用它們各自的顏色根據(jù)它們的值記錄所有斷言:綠色表示計(jì)算為true的斷言,紅色表示具有其他值的斷言。
注意比較,我們檢查true是否為字符串,因?yàn)槲覀儚拿總€(gè)文件接收字符串。
檢查結(jié)果
最后,最后一步是檢查所有測(cè)試是否成功。
function checkAssertions (assertionGroups) { return assertionGroups.some( group => group.assertions.some(assertion => assertion.value === 'false') ) ? 1 : 0 }
我們使用數(shù)組的some()方法檢查每個(gè)斷言組(規(guī)范),看看是否至少有一個(gè)值是' ' ' false ' ' '。我們嵌套了其中的兩個(gè)因?yàn)槲覀冇幸粋€(gè)二維數(shù)組。
運(yùn)行我們的程序
此時(shí),我們的CLI應(yīng)準(zhǔn)備好運(yùn)行一些JavaScript規(guī)范,并查看是否拾取并評(píng)估了斷言。在test目錄中,您可以從本文開(kāi)頭復(fù)制規(guī)范示例,并將以下命令粘貼到您的文件中:package.json
"scripts": { "test": "node index.js test" }
其中test是包含示例規(guī)范文件的目錄的名稱(chēng)。
當(dāng)運(yùn)行npm test命令時(shí),您應(yīng)該看到使用它們各自顏色的結(jié)果。
相關(guān)免費(fèi)學(xué)習(xí)推薦:js視頻教程