从Grunt测试Grunt插件
編寫針對(duì)grunt插件的測(cè)試結(jié)果比預(yù)期的要簡(jiǎn)單。 我需要運(yùn)行多個(gè)任務(wù)配置,并想通過(guò)在主目錄中鍵入grunt test來(lái)調(diào)用它們。
通常,第一個(gè)任務(wù)失敗后會(huì)發(fā)出咕聲。 這使得不可能在主項(xiàng)目gruntfile中存儲(chǔ)多個(gè)失敗方案。 從那里運(yùn)行它們將需要--force選項(xiàng),但是grunt會(huì)忽略所有不是最佳的警告。
較干凈的解決方案是在單獨(dú)的目錄中有一堆gruntfile,然后從主項(xiàng)目gruntfile調(diào)用它們。 這篇文章解釋了如何做到這一點(diǎn)。
示范項(xiàng)目
演示項(xiàng)目是帶有一個(gè)grunt任務(wù)的小型grunt插件。 根據(jù)action選項(xiàng)屬性的值,任務(wù)要么失敗并顯示警告,要么將成功消息打印到控制臺(tái)中。
任務(wù):
grunt.registerMultiTask('plugin_tester', 'Demo grunt task.', function() {//merge supplied options with default optionsvar options = this.options({ action: 'pass', message: 'unknown error'});//pass or fail - depending on configured optionsif (options.action==='pass') {grunt.log.writeln('Plugin worked correctly passed.');} else {grunt.warn('Plugin failed: ' + options.message);} });有三種不同的方法編寫grunt插件單元測(cè)試。 每個(gè)解決方案在test目錄中都有其自己的nodeunit文件,并在本文中進(jìn)行說(shuō)明:
- plugin_exec_test.js –最實(shí)用的解決方案 ,
- plugin_fork_test.js – 解決了先前解決方案失敗的罕見(jiàn)情況,
- plugin_spawn_test.js – 可能 ,但最不實(shí)用。
所有這三個(gè)演示測(cè)試都包含三種不同的任務(wù)配置:
// Success scenario options: { action: 'pass' } // Fail with "complete failure" message options: { action: 'fail', message: 'complete failure' } //Fail with "partial failure" message options: { action: 'fail', message: 'partial failure' }每個(gè)配置都存儲(chǔ)在test目錄內(nèi)的單獨(dú)gruntfile中。 例如,存儲(chǔ)在gruntfile-pass.js文件中的成功方案如下所示:
grunt.initConfig({// prove that npm plugin works toojshint: { all: [ 'gruntfile-pass.js' ] },// Configuration to be run (and then tested).plugin_tester: { pass: { options: { action: 'pass' } } } });// Load this plugin's task(s). grunt.loadTasks('./../tasks'); // next line does not work - grunt requires locally installed plugins grunt.loadNpmTasks('grunt-contrib-jshint');grunt.registerTask('default', ['plugin_tester', 'jshint']);這三個(gè)測(cè)試gruntfiles看起來(lái)幾乎相同,只有plugin_tester目標(biāo)的options對(duì)象改變了。
從子目錄運(yùn)行Gruntfile
我們的測(cè)試gruntfiles存儲(chǔ)在test子目錄中,而grunt不能很好地處理這種情況。 本章介紹了問(wèn)題所在,并介紹了兩種解決方法。
問(wèn)題
要查看問(wèn)題,請(qǐng)轉(zhuǎn)到演示項(xiàng)目目錄并運(yùn)行以下命令:
grunt --gruntfile test/gruntfile-problem.jsGrunt響應(yīng)以下錯(cuò)誤:
Local Npm module "grunt-contrib-jshint" not found. Is it installed? Warning: Task "jshint" not found. Use --force to continue.Aborted due to warnings.說(shuō)明
Grunt假定grunfile和node_modules存儲(chǔ)庫(kù)存儲(chǔ)在同一目錄中。 雖然node.js require函數(shù)會(huì)在所有父目錄中搜索所需模塊,但loadNpmTasks不會(huì)。
該問(wèn)題有兩種可能的解決方案,一種簡(jiǎn)單而有趣:
- 在測(cè)試目錄( 簡(jiǎn)單 )中創(chuàng)建本地npm存儲(chǔ)庫(kù),
- 從父目錄中執(zhí)行繁重的加載任務(wù)( fancy )。
盡管第一個(gè)“簡(jiǎn)單”解決方案比較干凈,但演示項(xiàng)目使用了第二個(gè)“精美”解決方案。
解決方案1:復(fù)制Npm存儲(chǔ)庫(kù)
主要思想很簡(jiǎn)單,只需在tests目錄內(nèi)創(chuàng)建另一個(gè)本地npm存儲(chǔ)庫(kù):
- 將package.json文件復(fù)制到tests目錄。
- 向其中添加僅測(cè)試依賴項(xiàng)。
- 每次運(yùn)行測(cè)試時(shí),請(qǐng)運(yùn)行npm install命令。
這是更清潔的解決方案。 它只有兩個(gè)缺點(diǎn):
- 測(cè)試依賴項(xiàng)必須單獨(dú)維護(hù),
- 所有插件依賴項(xiàng)都必須安裝在兩個(gè)位置。
解決方案2:從父目錄加載Grunt任務(wù)
另一個(gè)解決方案是強(qiáng)制grunt從存儲(chǔ)在另一個(gè)目錄中的npm存儲(chǔ)庫(kù)加載任務(wù)。
Grunt插件加載
Grunt有兩種方法可以加載插件:
- loadTasks('directory-name') –將所有任務(wù)加載到目錄中,
- loadNpmTasks('plugin-name') –加載插件定義的所有任務(wù)。
loadNpmTasks函數(shù)采用grunt插件和模塊存儲(chǔ)庫(kù)的固定目錄結(jié)構(gòu)。 它猜測(cè)應(yīng)該存儲(chǔ)任務(wù)的目錄名稱,然后調(diào)用loadTasks('directory-name')函數(shù)。
本地npm存儲(chǔ)庫(kù)為每個(gè)npm軟件包都有單獨(dú)的子目錄。 所有g(shù)runt插件都應(yīng)該具有tasks子目錄,并且其中的.js文件都包含任務(wù)。 例如, loadNpmTasks('grunt-contrib-jshint')調(diào)用從node_mudules/grunt-contrib-jshint/tasks目錄加載任務(wù),等效于:
grunt.loadTasks('node_modules/grunt-contrib-jshint/tasks')因此,如果要從父目錄加載grunt-contrib-jshint插件的所有任務(wù),可以執(zhí)行以下操作:
grunt.loadTasks('../node_modules/grunt-contrib-jshint/tasks')循環(huán)父目錄
更為靈活的解決方案是遍歷所有父目錄,直到找到最近的node_modules存儲(chǔ)庫(kù)或到達(dá)根目錄為止。 這是在grunt-hacks.js模塊內(nèi)部實(shí)現(xiàn)的。
loadParentNpmTasks函數(shù)循環(huán)父目錄:
module.exports = new function() {this.loadParentNpmTasks = function(grunt, pluginName) {var oldDirectory='', climb='', directory, content;// search for the right directorydirectory = climb+'node_modules/'+ pluginName;while (continueClimbing(grunt, oldDirectory, directory)) {climb += '../';oldDirectory = directory;directory = climb+'node_modules/'+ pluginName;}// load tasks or return an errorif (grunt.file.exists(directory)) {grunt.loadTasks(directory+'/tasks');} else {grunt.fail.warn('Tasks plugin ' + pluginName + ' was not found.');}}function continueClimbing(grunt, oldDirectory, directory) {return !grunt.file.exists(directory) &&!grunt.file.arePathsEquivalent(oldDirectory, directory);}}();修改后的Gruntfile
最后,我們需要通過(guò)以下步驟替換grunt.loadNpmTasks('grunt-contrib-jshint')的常規(guī)grunt.loadNpmTasks('grunt-contrib-jshint')調(diào)用:
var loader = require("./grunt-hacks.js"); loader.loadParentNpmTasks(grunt, 'grunt-contrib-jshint');縮短的gruntfile:
module.exports = function(grunt) {var loader = require("./grunt-hacks.js");grunt.initConfig({jshint: { /* ... */ },plugin_tester: { /* ... */ }});grunt.loadTasks('./../tasks');loader.loadParentNpmTasks(grunt, 'grunt-contrib-jshint'); };缺點(diǎn)
該解決方案有兩個(gè)缺點(diǎn):
- 它不處理集合插件。
- 如果grunt曾經(jīng)改變grunt插件的預(yù)期結(jié)構(gòu),則必須修改解決方案。
如果您還需要集合插件,請(qǐng)查看grunts task.js以了解如何支持它們。
從Java腳本調(diào)用Gruntfile
我們需要做的第二件事是從javascript調(diào)用gruntfile。 唯一的麻煩是,咕unt聲會(huì)在任務(wù)失敗時(shí)退出整個(gè)過(guò)程。 因此,我們需要從子進(jìn)程中調(diào)用它。
節(jié)點(diǎn)模塊子進(jìn)程具有三個(gè)不同的功能,可以在子進(jìn)程中運(yùn)行命令:
- exec –在命令行執(zhí)行命令,
- spawn –在命令行上執(zhí)行命令的方式不同,
- fork –在子進(jìn)程中運(yùn)行節(jié)點(diǎn)模塊。
第一個(gè)是exec ,最容易使用,并且在第一章中進(jìn)行了說(shuō)明。 第二章介紹了如何使用fork以及為什么它不如exec最佳。 第三章是關(guān)于生成。
執(zhí)行力
Exec在子進(jìn)程中運(yùn)行命令行命令。 您可以指定要在哪個(gè)目錄中運(yùn)行它,設(shè)置環(huán)境變量,設(shè)置超時(shí),然后在該超時(shí)后將命令終止。 當(dāng)命令完成運(yùn)行時(shí),exec調(diào)用回調(diào)并將其stdout流,stderr流和錯(cuò)誤傳遞給命令(如果命令崩潰)。
除非另有配置,否則命令將在當(dāng)前目錄中運(yùn)行。 我們希望它在tests子目錄中運(yùn)行,所以我們必須指定options對(duì)象的cwd屬性: {cwd: 'tests/'} 。
stdout和stderr流內(nèi)容都存儲(chǔ)在緩沖區(qū)中。 每個(gè)緩沖區(qū)的最大大小設(shè)置為204800,如果命令產(chǎn)生更多輸出,則exec調(diào)用將崩潰。 這筆錢足以應(yīng)付我們的小任務(wù)。 如果需要更多,則必須設(shè)置maxBuffer options屬性。
致電執(zhí)行
以下代碼段顯示了如何從exec運(yùn)行g(shù)runtfile。 該函數(shù)是異步的,并在完成之后調(diào)用whenDoneCallback :
var cp = require("child_process");function callGruntfile(filename, whenDoneCallback) {var command, options;command = "grunt --gruntfile "+filename+" --no-color";options = {cwd: 'test/'};cp.exec(command, options, whenDoneCallback); }注意:如果將npm安裝到測(cè)試目錄中( 簡(jiǎn)單的解決方案 ),則需要使用callNpmInstallAndGruntfile函數(shù)而不是callGruntfile :
function callNpmInstallAndGruntfile(filename, whenDoneCallback) {var command, options;command = "npm install";options = {cwd: 'test/'};cp.exec(command, {}, function(error, stdout, stderr) {callGruntfile(filename, whenDoneCallback);}); }單元測(cè)試
第一節(jié)點(diǎn)單元測(cè)試運(yùn)行成功方案,然后檢查流程是否成功完成而沒(méi)有失敗,標(biāo)準(zhǔn)輸出是否包含預(yù)期消息以及標(biāo)準(zhǔn)錯(cuò)誤是否為空。
成功場(chǎng)景單元測(cè)試:
pass: function(test) {test.expect(3);callGruntfile('gruntfile-pass.js', function (error, stdout, stderr) {test.equal(error, null, "Command should not fail.");test.equal(stderr, '', "Standard error stream should be empty.");var stdoutOk = contains(stdout, 'Plugin worked correctly.');test.ok(stdoutOk, "Missing stdout message.");test.done();}); },第二節(jié)點(diǎn)單元測(cè)試運(yùn)行“完全失敗”方案,然后檢查進(jìn)程是否按預(yù)期失敗。 請(qǐng)注意,標(biāo)準(zhǔn)錯(cuò)誤流為空,警告被打印到標(biāo)準(zhǔn)輸出中。
失敗的場(chǎng)景單元測(cè)試:
fail_1: function(test) {test.expect(3);var gFile = 'gruntfile-fail-complete.js';callGruntfile(gFile, function (error, stdout, stderr) {test.equal(error, null, "Command should have failed.");test.equal(error.message, 'Command failed: ', "Wrong error message.");test.equal(stderr, '', "Non empty stderr.");var stdoutOk = containsWarning(stdout, 'complete failure');test.ok(stdoutOk, "Missing stdout message.");test.done();}); }第三次“部分故障”節(jié)點(diǎn)單元測(cè)試與之前的測(cè)試幾乎相同。 整個(gè)測(cè)試文件可在github上找到 。
缺點(diǎn)
壞處:
- 必須預(yù)先設(shè)置最大緩沖區(qū)大小。
叉子
Fork在子進(jìn)程中運(yùn)行node.js模塊,等效于在命令行上調(diào)用node <module-name> 。 Fork使用回調(diào)將標(biāo)準(zhǔn)輸出和標(biāo)準(zhǔn)錯(cuò)誤發(fā)送給調(diào)用方。 兩個(gè)回調(diào)都可以被多次調(diào)用,并且調(diào)用方會(huì)分段獲取子進(jìn)程的輸出。
僅當(dāng)需要處理任意大小的stdout和stderr或需要自定義grunt功能時(shí),使用fork才有意義。 如果您不這樣做,則exec更易于使用。
本章分為四個(gè)子章節(jié):
- 從javascript 呼叫g(shù)runt ,
- 讀取節(jié)點(diǎn)模塊中的命令行參數(shù),
- 在子進(jìn)程中啟動(dòng)節(jié)點(diǎn)模塊,
- 編寫單元測(cè)試。
呼喚咕unt聲
Grunt并非以編程方式被調(diào)用。 它沒(méi)有公開(kāi)“公共” API,也沒(méi)有對(duì)其進(jìn)行記錄。
我們的解決方案模仿了grunt-cli的功能,因此相對(duì)來(lái)說(shuō)是將來(lái)安全的。 Grunt-cli與grunt核心是分開(kāi)分發(fā)的,因此更改的可能性較小。 但是,如果確實(shí)更改,則此解決方案也必須更改。
從javascript運(yùn)行咕unt聲需要我們執(zhí)行以下操作:
- 將gruntfile名稱與其路徑分開(kāi),
- 更改活動(dòng)目錄,
- 調(diào)用grunts tasks功能。
從javascript呼叫g(shù)runt:
this.runGruntfile = function(filename) {var grunt = require('grunt'), path = require('path'), directory, filename;// split filename into directory and filedirectory = path.dirname(filename);filename = path.basename(filename);//change directoryprocess.chdir(directory);//call gruntgrunt.tasks(['default'], {gruntfile:filename, color:false}, function() {console.log('done');}); };模塊參數(shù)
該模塊將從命令行調(diào)用。 節(jié)點(diǎn)將命令行參數(shù)保留在內(nèi)部
process.argv數(shù)組:
呼叫叉
Fork具有三個(gè)參數(shù):模塊的路徑,帶有命令行參數(shù)的數(shù)組和options對(duì)象。 使用tests/Gruntfile-1.js參數(shù)調(diào)用module.js :
child = cp.fork('./module.js', ['tests/Gruntfile-1.js'], {silent: true})silent: true選項(xiàng)使返回的child進(jìn)程的stdout和stderr在父級(jí)內(nèi)部可用。 如果將其設(shè)置為true,則返回的對(duì)象將提供對(duì)調(diào)用者的stdout和stderr流的訪問(wèn)。
在每個(gè)流上調(diào)用on('data', callback) 。 每次子進(jìn)程向流發(fā)送某些內(nèi)容時(shí),都會(huì)調(diào)用傳遞的回調(diào):
child.stdout.on('data', function (data) {console.log('stdout: ' + data); // handle piece of stdout }); child.stderr.on('data', function (data) {console.log('stderr: ' + data); // handle piece of stderr });子進(jìn)程可能崩潰或正常結(jié)束其工作:
child.on('error', function(error){// handle child crashconsole.log('error: ' + error); }); child.on('exit', function (code, signal) {// this is called after child process endedconsole.log('child process exited with code ' + code); });演示項(xiàng)目使用以下函數(shù)來(lái)調(diào)用fork和綁定回調(diào):
/*** callbacks: onProcessError(error), onProcessExit(code, signal), onStdout(data), onStderr(data)*/ function callGruntfile(filename, callbacks) {var comArg, options, child;callbacks = callbacks || {};child = cp.fork('./test/call-grunt.js', [filename], {silent: true});if (callbacks.onProcessError) {child.on("error", callbacks.onProcessError);}if (callbacks.onProcessExit) {child.on("exit", callbacks.onProcessExit);}if (callbacks.onStdout) {child.stdout.on('data', callbacks.onStdout);}if (callbacks.onStderr) {child.stderr.on('data', callbacks.onStderr);} }編寫測(cè)試
每個(gè)單元測(cè)試都調(diào)用callGruntfile函數(shù)。 回調(diào)會(huì)在標(biāo)準(zhǔn)輸出流中搜索所需的內(nèi)容,檢查退出代碼是否正確,如果錯(cuò)誤流中出現(xiàn)某些錯(cuò)誤,則失敗,或者如果fork調(diào)用返回錯(cuò)誤,則失敗。
成功場(chǎng)景單元測(cè)試:
pass: function(test) {var wasPassMessage = false, callbacks;test.expect(2);callbacks = {onProcessError: function(error) {test.ok(false, "Unexpected error: " + error);test.done();},onProcessExit: function(code, signal) {test.equal(code, 0, "Exit code should have been 0");test.ok(wasPassMessage, "Pass message was never sent ");test.done();},onStdout: function(data) {if (contains(data, 'Plugin worked correctly.')) {wasPassMessage = true;}},onStderr: function(data) {test.ok(false, "Stderr should have been empty: " + data);}};callGruntfile('test/gruntfile-pass.js', callbacks); }對(duì)應(yīng)于失敗場(chǎng)景的測(cè)試幾乎相同,可以在github上找到。
缺點(diǎn)
缺點(diǎn):
- 使用的grunt函數(shù)不屬于官方API。
- 子進(jìn)程輸出流以塊而不是一個(gè)大塊的形式提供。
產(chǎn)生
Spawn是fork和exec之間的交叉。 與exec類似,spawn能夠運(yùn)行可執(zhí)行文件并向其傳遞命令行參數(shù)。 子進(jìn)程輸出流的處理方式與fork中的處理方式相同。 它們通過(guò)回調(diào)分段發(fā)送給父級(jí)。 因此,與fork一樣,只有在需要任意大小的stdout或stderr時(shí),使用spawn才有意義。
問(wèn)題
產(chǎn)卵的主要問(wèn)題發(fā)生在Windows上。 必須準(zhǔn)確指定要運(yùn)行的命令的名稱。 如果使用參數(shù)grunt調(diào)用spawn,則spawn需要不帶后綴的可執(zhí)行文件名。 grunt.cmd真正的grunt可執(zhí)行文件grunt.cmd 。 否則, spawn 忽略Windows環(huán)境變量PATHEXT 。
循環(huán)后綴
如果要從spawn調(diào)用grunt ,則需要執(zhí)行以下操作之一:
- 針對(duì)Windows和Linux使用不同的代碼,或者
- 從環(huán)境中讀取PATHEXT并循環(huán)遍歷,直到找到正確的后綴。
以下函數(shù)循環(huán)遍歷PATHEXT并將正確的文件名傳遞給回調(diào):
function findGruntFilename(callback) {var command = "grunt", options, extensionsStr, extensions, i, child, onErrorFnc, hasRightExtension = false;onErrorFnc = function(data) {if (data.message!=="spawn ENOENT"){grunt.warn("Unexpected error on spawn " +extensions[i]+ " error: " + data);}};function tryExtension(extension) {var child = cp.spawn(command + extension, ['--version']);child.on("error", onErrorFnc);child.on("exit", function(code, signal) {hasRightExtension = true;callback(command + extension);});}extensionsStr = process.env.PATHEXT || '';extensions = [''].concat(extensionsStr.split(';'));for (i=0; !hasRightExtension && i<extensions.length;i++) {tryExtension(extensions[i]);} }編寫測(cè)試
一旦有了grunt命令名,就可以調(diào)用spawn 。 Spawn會(huì)觸發(fā)與fork完全相同的事件,因此
callGruntfile接受完全相同的回調(diào)對(duì)象,并將其屬性綁定到子進(jìn)程事件:
測(cè)試也幾乎與上一章中的測(cè)試相同。 唯一的區(qū)別是,在執(zhí)行其他所有操作之前,您必須先找到grunt可執(zhí)行文件名。 成功場(chǎng)景測(cè)試如下所示:
pass: function(test) {var wasPassMessage = false;test.expect(2);findGruntFilename(function(gruntCommand){var callbacks = {/* ... callbacks look exactly the same way as in fork ... */};callGruntfile(gruntCommand, 'gruntfile-pass.js', callbacks);}); }完整的成功方案測(cè)試以及兩個(gè)失敗方案測(cè)試都可以在github上獲得 。
缺點(diǎn)
缺點(diǎn):
- Spawn會(huì)忽略PATHEXT后綴,需要使用自定義代碼來(lái)處理它。
- 子進(jìn)程輸出流以塊而不是一個(gè)大塊的形式提供。
結(jié)論
有三種方法可以從gruntfile內(nèi)部測(cè)試grunt插件。 除非您有充分的理由不這樣做,否則請(qǐng)使用exec 。
翻譯自: https://www.javacodegeeks.com/2015/02/testing-grunt-plugin-from-grunt.html
總結(jié)
以上是生活随笔為你收集整理的从Grunt测试Grunt插件的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 适应证是什么意思 适应证的解释
- 下一篇: 使用rx-java的异步抽象