grunt 插件_从Grunt测试Grunt插件
grunt 插件
編寫針對grunt插件的測試結(jié)果比預(yù)期的要簡單。 我需要運行多個任務(wù)配置,并想通過在主目錄中鍵入grunt test來調(diào)用它們。
在第一個任務(wù)失敗后,咕聲通常會退出。 這使得不可能在主項目gruntfile中存儲多個失敗方案。 從那里運行它們將需要--force選項,但是grunt會忽略所有不是最佳的警告。
較干凈的解決方案是在單獨的目錄中有一堆gruntfile,然后從主項目gruntfile調(diào)用它們。 這篇文章解釋了如何做到這一點。
示范項目
演示項目是帶有一個grunt任務(wù)的小型grunt插件。 根據(jù)action options屬性的值,任務(wù)要么失敗并顯示警告,要么將成功消息打印到控制臺中。
任務(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插件單元測試。 每個解決方案在test目錄中都有自己的nodeunit文件,并在這篇文章中進(jìn)行說明:
- plugin_exec_test.js –最實用的解決方案 ,
- plugin_fork_test.js – 解決了先前解決方案失敗的罕見情況,
- plugin_spawn_test.js – 可能 ,但最不實用。
所有這三個演示測試都包含三種不同的任務(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' }每個配置都存儲在test目錄內(nèi)的單獨gruntfile中。 例如,存儲在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']);這三個測試gruntfiles看起來幾乎相同,只有plugin_tester目標(biāo)的options對象改變了。
從子目錄運行Gruntfile
我們的測試gruntfiles存儲在test子目錄中,而grunt不能很好地處理這種情況。 本章介紹了問題所在,并介紹了兩種解決方法。
問題
要查看問題,請轉(zhuǎn)到演示項目目錄并運行以下命令:
grunt --gruntfile test/gruntfile-problem.jsGrunt響應(yīng)以下錯誤:
Local Npm module "grunt-contrib-jshint" not found. Is it installed? Warning: Task "jshint" not found. Use --force to continue.Aborted due to warnings.說明
Grunt假定grunfile和node_modules存儲庫存儲在同一目錄中。 雖然node.js require函數(shù)會在所有父目錄中搜索所需模塊,但loadNpmTasks不會。
這個問題有兩種可能的解決方案,一種簡單而有趣:
- 在測試目錄( 簡單 )中創(chuàng)建本地npm存儲庫,
- 從父目錄中執(zhí)行繁重的加載任務(wù)( fancy )。
盡管第一個“簡單”解決方案比較干凈,但演示項目使用了第二個“精美”解決方案。
解決方案1:復(fù)制Npm存儲庫
主要思想很簡單,只需在tests目錄內(nèi)創(chuàng)建另一個本地npm存儲庫:
- 將package.json文件復(fù)制到tests目錄。
- 向其中添加僅測試依賴項。
- 每次運行測試時,請運行npm install命令。
這是更清潔的解決方案。 它只有兩個缺點:
- 測試依賴項必須單獨維護(hù),
- 所有插件依賴項都必須安裝在兩個位置。
解決方案2:從父目錄加載Grunt任務(wù)
另一個解決方案是強(qiáng)制grunt從存儲在另一個目錄中的npm存儲庫加載任務(wù)。
Grunt插件加載
Grunt有兩種方法可以加載插件:
- loadTasks('directory-name') –將所有任務(wù)加載到目錄中,
- loadNpmTasks('plugin-name') –加載插件定義的所有任務(wù)。
loadNpmTasks函數(shù)采用grunt插件和模塊存儲庫的固定目錄結(jié)構(gòu)。 它猜測應(yīng)該存儲任務(wù)的目錄名稱,然后調(diào)用loadTasks('directory-name')函數(shù)。
本地npm存儲庫為每個npm軟件包都有單獨的子目錄。 所有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存儲庫或到達(dá)根目錄為止。 這是在grunt-hacks.js模塊內(nèi)部實現(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
最后,我們需要通過以下步驟替換grunt.loadNpmTasks('grunt-contrib-jshint')中通常的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'); };缺點
該解決方案有兩個缺點:
- 它不處理集合插件。
- 如果grunt曾經(jīng)改變grunt插件的預(yù)期結(jié)構(gòu),則必須修改解決方案。
如果您還需要集合插件,請查看grunts task.js以了解如何支持它們。
從Java腳本調(diào)用Gruntfile
我們需要做的第二件事是從javascript調(diào)用gruntfile。 唯一的麻煩是,咕unt聲會在任務(wù)失敗時退出整個過程。 因此,我們需要從子進(jìn)程中調(diào)用它。
節(jié)點模塊子進(jìn)程具有三種不同的功能,能夠在子進(jìn)程內(nèi)部運行命令:
- exec –在命令行執(zhí)行命令,
- spawn –在命令行上執(zhí)行命令的方式不同,
- fork –在子進(jìn)程中運行節(jié)點模塊。
第一個是exec ,最易于使用,并在第一章中進(jìn)行了說明。 第二章介紹了如何使用fork以及為什么它不如exec最佳。 第三章是關(guān)于生成。
執(zhí)行力
Exec在子進(jìn)程中運行命令行命令。 您可以指定要在哪個目錄中運行它,設(shè)置環(huán)境變量,設(shè)置超時,然后在該超時后將命令終止。 當(dāng)命令完成運行時,exec調(diào)用回調(diào)并將其傳遞給stdout流,stderr流和命令崩潰時的錯誤。
除非另有配置,否則命令將在當(dāng)前目錄中運行。 我們希望它在tests子目錄中運行,所以我們必須指定options對象的cwd屬性: {cwd: 'tests/'} 。
stdout和stderr流內(nèi)容都存儲在緩沖區(qū)中。 每個緩沖區(qū)的最大大小設(shè)置為204800,如果命令產(chǎn)生更多輸出,則exec調(diào)用將崩潰。 這筆錢足以應(yīng)付我們的小任務(wù)。 如果需要更多,則必須設(shè)置maxBuffer options屬性。
致電執(zhí)行
以下代碼段顯示了如何從exec運行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安裝到測試目錄( 簡單解決方案 ),則需要使用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);}); }單元測試
第一節(jié)點單元測試運行成功方案,然后檢查流程是否成功完成而沒有失敗,標(biāo)準(zhǔn)輸出是否包含預(yù)期的消息以及標(biāo)準(zhǔn)錯誤是否為空。
成功場景單元測試:
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é)點單元測試運行“完全失敗”方案,然后檢查進(jìn)程是否按預(yù)期失敗。 請注意,標(biāo)準(zhǔn)錯誤流為空,警告被打印到標(biāo)準(zhǔn)輸出中。
失敗的場景單元測試:
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é)點單元測試與之前的測試幾乎相同。 整個測試文件可在github上找到 。
缺點
壞處:
- 必須預(yù)先設(shè)置最大緩沖區(qū)大小。
叉子
Fork在子進(jìn)程中運行node.js模塊,等效于在命令行上調(diào)用node <module-name> 。 Fork使用回調(diào)將標(biāo)準(zhǔn)輸出和標(biāo)準(zhǔn)錯誤發(fā)送給調(diào)用方。 兩個回調(diào)都可以被多次調(diào)用,并且調(diào)用方可以分段獲取子進(jìn)程的輸出。
僅在需要處理任意大小的stdout和stderr或需要自定義grunt功能時,使用fork才有意義。 如果您不這樣做,則exec更易于使用。
本章分為四個子章節(jié):
- 從javascript 呼叫g(shù)runt ,
- 讀取節(jié)點模塊中的命令行參數(shù),
- 在子進(jìn)程中啟動節(jié)點模塊,
- 編寫單元測試。
呼喚咕unt聲
Grunt并非以編程方式被調(diào)用。 它沒有公開“公共” API,也沒有對其進(jìn)行記錄。
我們的解決方案模仿了grunt-cli的功能,因此相對安全。 Grunt-cli與grunt核心分開分發(fā),因此更改的可能性較小。 但是,如果確實更改,則此解決方案也必須更改。
從javascript運行咕unt聲需要我們執(zhí)行以下操作:
- 將gruntfile名稱與其路徑分開,
- 更改活動目錄,
- 調(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é)點將命令行參數(shù)保留在內(nèi)部
process.argv數(shù)組:
呼叫叉
Fork具有三個參數(shù):模塊的路徑,帶有命令行參數(shù)的數(shù)組和options對象。 使用tests/Gruntfile-1.js參數(shù)調(diào)用module.js :
child = cp.fork('./module.js', ['tests/Gruntfile-1.js'], {silent: true})silent: true選項使返回的child進(jìn)程的stdout和stderr在父級內(nèi)部可用。 如果將其設(shè)置為true,則返回的對象將提供對調(diào)用者的stdout和stderr流的訪問。
在每個流上調(diào)用on('data', callback) 。 每次子進(jìn)程向流發(fā)送某些內(nèi)容時,都會調(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); });演示項目使用以下函數(shù)來調(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);} }編寫測試
每個單元測試都調(diào)用callGruntfile函數(shù)。 回調(diào)會在標(biāo)準(zhǔn)輸出流中搜索所需的內(nèi)容,檢查退出代碼是否正確,在錯誤流中出現(xiàn)錯誤時失敗,或者在fork調(diào)用返回錯誤時失敗。
成功場景單元測試:
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); }對應(yīng)于失敗場景的測試幾乎相同,可以在github上找到。
缺點
缺點:
- 使用的grunt函數(shù)不屬于官方API。
- 子進(jìn)程輸出流以塊而不是一個大塊的形式提供。
產(chǎn)生
Spawn是fork和exec之間的交叉。 與exec類似,spawn能夠運行可執(zhí)行文件并向其傳遞命令行參數(shù)。 子進(jìn)程輸出流的處理方式與fork中的處理方式相同。 它們通過回調(diào)分段發(fā)送給父級。 因此,與使用fork一樣,僅當(dāng)需要任意大小的stdout或stderr時,使用spawn才有意義。
問題
產(chǎn)卵的主要問題發(fā)生在Windows上。 必須準(zhǔ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í)行以下操作之一:
- 針對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]);} }編寫測試
一旦有了grunt命令名,就可以調(diào)用spawn 。 Spawn會觸發(fā)與fork完全相同的事件,因此
callGruntfile接受完全相同的回調(diào)對象,并將其屬性綁定到子進(jìn)程事件:
測試也幾乎與上一章中的測試相同。 唯一的區(qū)別是,在執(zhí)行其他所有操作之前,您必須先找到grunt可執(zhí)行文件名。 成功場景測試如下所示:
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);}); }完整的成功方案測試以及兩個失敗方案測試都可以在github上獲得 。
缺點
缺點:
- Spawn會忽略PATHEXT后綴,需要使用自定義代碼來處理它。
- 子進(jìn)程輸出流以塊而不是一個大塊的形式提供。
結(jié)論
有三種方法可以從gruntfile內(nèi)部測試grunt插件。 除非您有非常強(qiáng)烈的理由不這樣做,否則請使用exec 。
翻譯自: https://www.javacodegeeks.com/2015/02/testing-grunt-plugin-from-grunt.html
grunt 插件
總結(jié)
以上是生活随笔為你收集整理的grunt 插件_从Grunt测试Grunt插件的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 主要矛盾和次要矛盾_次要GC,主要GC与
- 下一篇: 序列化与反序列化的单例模式_序列化代理模