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