serverless-test-plugin
Advanced tools
Comparing version 0.1.5 to 0.2.0
319
index.js
@@ -7,20 +7,16 @@ 'use strict'; | ||
module.exports = function(ServerlessPlugin, serverlessPath) { // Always pass in the ServerlessPlugin Class | ||
module.exports = function(S) { | ||
const path = require('path'), | ||
fs = require('fs'), | ||
BbPromise = require('bluebird'), | ||
const BbPromise = require('bluebird'), | ||
SCli = require(S.getServerlessPath('utils/cli')), | ||
SError = require(S.getServerlessPath('Error')), | ||
chalk = require('chalk'), | ||
SError = require(path.join(serverlessPath, 'ServerlessError')), | ||
SUtils = require(path.join(serverlessPath, 'utils')), | ||
SCli = require( path.join( serverlessPath, 'utils', 'cli' ) ), | ||
context = require( path.join( serverlessPath, 'utils', 'context' ) ), | ||
JUnitWriter = require("junitwriter"), | ||
intercept = require("intercept-stdout"); | ||
JUnitWriter = require('junitwriter'), | ||
intercept = require('intercept-stdout'); | ||
/** | ||
* ServerlessPluginBoierplate | ||
* ServerlessTestPlugin | ||
*/ | ||
class ServerlessTestPlugin extends ServerlessPlugin { | ||
class ServerlessTestPlugin extends S.classes.Plugin { | ||
@@ -32,4 +28,4 @@ /** | ||
constructor(S) { | ||
super(S); | ||
constructor() { | ||
super(); | ||
} | ||
@@ -56,3 +52,3 @@ | ||
this.S.addAction(this._runFunctionTest.bind(this), { | ||
S.addAction(this._runFunctionTest.bind(this), { | ||
handler: 'runFunctionTest', | ||
@@ -65,12 +61,20 @@ description: 'Run tests on a given function', | ||
shortcut: 'a', | ||
description: 'Test all functions' | ||
description: 'Optional - Test all functions' | ||
},{ | ||
option: 'out', | ||
shortcut: 'o', | ||
description: 'JUnit output file' | ||
description: 'Optional - JUnit output file' | ||
},{ | ||
option: 'stage', | ||
shortcut: 's', | ||
description: 'The stage used to populate your templates. Default: the first stage found in your project' | ||
},{ | ||
option: 'region', | ||
shortcut: 'r', | ||
description: 'The region used to populate your templates. Default: the first region for the first stage found.' | ||
}], | ||
parameters: [{ // Use paths when you multiple values need to be input (like an array). Input looks like this: "serverless custom run module1/function1 module1/function2 module1/function3. Serverless will automatically turn this into an array and attach it to evt.options within your plugin | ||
parameter: 'paths', | ||
description: 'One or multiple paths to your function', | ||
position: '0->' // Can be: 0, 0-2, 0-> This tells Serverless which params are which. 3-> Means that number and infinite values after it. | ||
parameters: [{ | ||
parameter: 'names', | ||
description: 'One or multiple function names', | ||
position: '0->' | ||
}] | ||
@@ -102,165 +106,166 @@ }); | ||
let _this = this; | ||
// Set an environment variable the invoked functions can check for | ||
process.env.SERVERLESS_TEST = true; | ||
return new BbPromise(function (resolve, reject) { | ||
// Prepare result object | ||
evt.data.result = { status: false }; | ||
// Set an environment variable the invoked functions can check for | ||
process.env.SERVERLESS_TEST = true; | ||
// Instantiate Classes | ||
let functions; | ||
if (evt.options.all) { | ||
// Load all functions | ||
functions = S.getProject().getAllFunctions(); | ||
} | ||
else if (S.cli && evt.options.names && evt.options.names.length === 0) { | ||
// no names or options so use cwd behavior | ||
// will return all functions if none in cwd | ||
functions = S.utils.getFunctionsByCwd(S.getProject().getAllFunctions()); | ||
} | ||
else if (evt.options.names && evt.options.names.length > 0) { | ||
// return by passed name(s) | ||
functions = evt.options.names.map(name => { | ||
const func = S.getProject().getFunction(name); | ||
if (!func) { | ||
throw new SError(`Function ${name} does not exist in your project`); | ||
} | ||
return func; | ||
}); | ||
} | ||
// Prepare result object | ||
evt.data.result = { status: false }; | ||
if (!functions || functions.length === 0) { | ||
throw new SError(`You need to specify either a function path or --all to test all functions`); | ||
} | ||
// Instantiate Classes | ||
let functions; | ||
if (evt.options.all) { | ||
// Load all functions | ||
functions = _this.S.state.getFunctions(); | ||
} | ||
else if (evt.options.paths) { | ||
// Load individual functions as specified in command line | ||
functions = _this.S.state.getFunctions({ paths: evt.options.paths }); | ||
} | ||
// Set stage and region | ||
const stages = S.getProject().stages; | ||
const stagesKeys = Object.keys(stages); | ||
if (!stagesKeys.length) { | ||
throw new SError(`We could not find a default stage for your project: it looks like your _meta folder is empty. If you cloned your project using git, try "sls project init" to recreate your _meta folder`); | ||
} | ||
if (!functions || functions.length === 0) { | ||
return BbPromise.reject(new SError( | ||
"You need to specify either a function path or --all to test all functions", | ||
SError.errorCodes.INVALID_PROJECT_SERVERLESS | ||
)); | ||
} | ||
const stage = evt.options.stage || stagesKeys[0]; | ||
const stageVariables = stages[stage]; | ||
// Iterate all functions, execute their handler and | ||
// write the results into a JUnit file... | ||
let junitWriter = new JUnitWriter(); | ||
let count = 0, succeeded = 0, failed = 0; | ||
BbPromise.each(functions, function(functionData) { | ||
let functionTestSuite = junitWriter.addTestsuite(functionData._config.sPath); | ||
count++; | ||
const region = evt.options.region || Object.keys(stageVariables.regions)[0]; | ||
if (functionData.runtime === "nodejs") { | ||
// Load function file & handler | ||
let functionFile = functionData.handler.split('/').pop().split('.')[0]; | ||
let functionHandler = functionData.handler.split('/').pop().split('.')[1]; | ||
let functionPath = path.join(_this.S.config.projectPath, functionData._config.sPath); | ||
functionFile = path.join(functionPath, (functionFile + '.js')); | ||
// Iterate all functions, execute their handler and | ||
// write the results into a JUnit file... | ||
const junitWriter = new JUnitWriter(); | ||
let count = 0, succeeded = 0, failed = 0; | ||
return BbPromise.each(functions, function(functionData) { | ||
let functionTestSuite = junitWriter.addTestsuite(functionData.name); | ||
count++; | ||
// Fire function | ||
let eventFile = (functionData.custom.test ? | ||
functionData.custom.test.event : false) || "event.json"; | ||
let functionEvent = SUtils.readAndParseJsonSync(path.join(functionPath, eventFile)); | ||
if (functionData.runtime.substring(0, 6) === 'nodejs') { | ||
// TODO Should we skip a function that's explicitly specified via command line option? | ||
if (functionData.custom.test && functionData.custom.test.skip) { | ||
SCli.log(`Skipping ${functionData._config.sPath}`); | ||
functionTestSuite.addTestcase("skipped", functionData._config.sPath); | ||
functionTestSuite.setSkipped(true); | ||
return; // skip this function | ||
} | ||
// TODO Should we skip a function that's explicitly specified via command line option? | ||
if (functionData.custom.test && functionData.custom.test.skip) { | ||
SCli.log(`Skipping ${functionData.name}`); | ||
functionTestSuite.addTestcase('skipped', functionData.name); | ||
functionTestSuite.setSkipped(true); | ||
return BbPromise.resolve(); // skip this function | ||
} | ||
return new BbPromise(function(resolve) { | ||
try { | ||
// Load the handler code | ||
functionHandler = require(functionFile)[functionHandler]; | ||
if (!functionHandler) { | ||
let msg = `Handler function ${functionData.handler} not found`; | ||
SCli.log(chalk.bold(msg)); | ||
evt.data.result.status = 'error'; | ||
evt.data.result.response = msg; | ||
return resolve(); | ||
} | ||
// Load test event data | ||
const eventFile = functionData.getRootPath((functionData.custom.test ? | ||
functionData.custom.test.event : false) || 'event.json'); | ||
const eventData = S.utils.readFileSync(eventFile); | ||
// Okay, let's go and execute the handler | ||
// We intercept all stdout from the function and dump | ||
// it into our test results instead. | ||
SCli.log(`Testing ${functionData._config.sPath}...`); | ||
let testCase = functionTestSuite.addTestcase("should succeed", functionData._config.sPath); | ||
let capturedText = ""; | ||
let unhookIntercept = intercept(function(txt) { | ||
capturedText += txt; | ||
}); | ||
let startTime = Date.now(); | ||
functionHandler(functionEvent, context(functionData.name, function (err, result) { | ||
try { | ||
// We intercept all stdout from the function and dump | ||
// it into our test results instead. | ||
SCli.log(`Testing ${functionData.name}...`); | ||
let testCase = functionTestSuite.addTestcase('should succeed', functionData.name); | ||
let capturedText = ''; | ||
let unhookIntercept = intercept(function(txt) { | ||
// Remove all ANSI color codes from output | ||
const regex = /[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g; | ||
capturedText += txt.replace(regex, ''); | ||
return ''; // don't print anything | ||
}); | ||
let duration = (Date.now() - startTime) / 1000; | ||
unhookIntercept(); // stop intercepting stdout | ||
// Finally run the Lambda function... | ||
let startTime = Date.now(); | ||
return functionData.run(stage, region, eventData) | ||
.then(function() { | ||
testCase.setSystemOut(capturedText); | ||
testCase.setTime(duration); | ||
let duration = (Date.now() - startTime) / 1000; | ||
unhookIntercept(); // stop intercepting stdout | ||
// Show error | ||
if (err) { | ||
testCase.addFailure(err.toString(), "Failed"); | ||
testCase.setSystemOut(capturedText); | ||
testCase.setTime(duration); | ||
// Done with errors. | ||
SCli.log(chalk.bgRed.white(" ERROR ") + " " + | ||
chalk.red(err.toString())); | ||
failed++; | ||
} | ||
else if (duration > functionData.timeout) { | ||
let msg = `Timeout of ${functionData.timeout} seconds exceeded`; | ||
testCase.addFailure(msg, "Timeout"); | ||
if (duration > functionData.timeout) { | ||
let msg = `Timeout of ${functionData.timeout} seconds exceeded`; | ||
testCase.addFailure(msg, "Timeout"); | ||
SCli.log(chalk.bgMagenta.white(" TIMEOUT ") + " " + | ||
chalk.magenta(msg)); | ||
failed++; | ||
} | ||
else { | ||
// Done. | ||
SCli.log(chalk.green("Success!")); | ||
succeeded++; | ||
} | ||
return resolve(); | ||
})); | ||
SCli.log(chalk.bgMagenta.white(" TIMEOUT ") + " " + | ||
chalk.magenta(msg)); | ||
failed++; | ||
} | ||
catch (err) { | ||
else { | ||
// Done. | ||
SCli.log(chalk.green("Success!")); | ||
succeeded++; | ||
} | ||
}) | ||
.catch(function(err) { | ||
unhookIntercept(); // stop intercepting stdout | ||
testCase.addFailure(err.toString(), "Failed"); | ||
SCli.log("-----------------"); | ||
SCli.log(chalk.bold("Failed to Run Handler - This Error Was Thrown:")); | ||
SCli.log(err); | ||
evt.data.result.status = 'error'; | ||
evt.data.result.response = err.message; | ||
return resolve(); | ||
} | ||
// Done with errors. | ||
SCli.log(chalk.bgRed.white(" ERROR ") + " " + | ||
chalk.red(err.toString())); | ||
failed++; | ||
}); | ||
} | ||
else { | ||
SCli.log("Skipping " + functionData._config.sPath); | ||
functionTestSuite.setSkipped(true); | ||
catch (err) { | ||
SCli.log("-----------------"); | ||
SCli.log(chalk.bold("Failed to Run Handler - This Error Was Thrown:")); | ||
SCli.log(err); | ||
evt.data.result.status = 'error'; | ||
evt.data.result.response = err.message; | ||
} | ||
}).then(function() { | ||
} | ||
else { | ||
SCli.log("Skipping " + functionData.name); | ||
functionTestSuite.setSkipped(true); | ||
} | ||
}) | ||
.then(function() { | ||
SCli.log("-----------------"); | ||
SCli.log("-----------------"); | ||
// All done. Print a summary and write the test results | ||
SCli.log("Tests completed: " + | ||
chalk.green(String(succeeded) + " succeeded") + " / " + | ||
chalk.red(String(failed) + " failed") + " / " + | ||
chalk.white(String(count - succeeded - failed) + " skipped")); | ||
// All done. Print a summary and write the test results | ||
SCli.log("Tests completed: " + | ||
chalk.green(String(succeeded) + " succeeded") + " / " + | ||
chalk.red(String(failed) + " failed") + " / " + | ||
chalk.white(String(count - succeeded - failed) + " skipped")); | ||
if (evt.options.out) { | ||
// Write test results to file | ||
return new BbPromise(function(resolve) { | ||
junitWriter.save(evt.options.out, function() { | ||
SCli.log("Test results written to " + evt.options.out); | ||
resolve(); | ||
}); | ||
if (evt.options.out) { | ||
// Write test results to file | ||
return new BbPromise(function(resolve) { | ||
junitWriter.save(evt.options.out, function() { | ||
SCli.log("Test results written to " + evt.options.out); | ||
resolve(); | ||
}); | ||
} | ||
}).then(function() { | ||
resolve(); | ||
process.exit(); // FIXME force exit | ||
}).catch(function(err) { | ||
}); | ||
} | ||
}) | ||
.then(function() { | ||
process.exit(); // FIXME force exit | ||
}) | ||
.catch(function(err) { | ||
SCli.log("-----------------"); | ||
SCli.log("-----------------"); | ||
SCli.log(chalk.bold("Failed to Run Tests - This Error Was Thrown:")); | ||
SCli.log(err); | ||
evt.data.result.status = 'error'; | ||
evt.data.result.response = err.message; | ||
return resolve(); | ||
}).finally(function() { | ||
process.env.SERVERLESS_TEST = undefined; | ||
}); | ||
SCli.log(chalk.bold("Failed to Run Tests - This Error Was Thrown:")); | ||
SCli.log(err); | ||
evt.data.result.status = 'error'; | ||
evt.data.result.response = err.message; | ||
}) | ||
.finally(function() { | ||
process.env.SERVERLESS_TEST = undefined; | ||
}); | ||
@@ -267,0 +272,0 @@ } |
{ | ||
"name": "serverless-test-plugin", | ||
"version": "0.1.5", | ||
"version": "0.2.0", | ||
"engines": { | ||
@@ -30,10 +30,9 @@ "node": ">=4.0" | ||
"bin": {}, | ||
"devDependencies": { | ||
}, | ||
"devDependencies": {}, | ||
"dependencies": { | ||
"chalk": "^1.1.0", | ||
"bluebird": "^3.0.6", | ||
"junitwriter": "^0.3.1", | ||
"intercept-stdout": "^0.1.2" | ||
"bluebird": "^3.3.5", | ||
"chalk": "^1.1.3", | ||
"intercept-stdout": "^0.1.2", | ||
"junitwriter": "^0.3.1" | ||
} | ||
} |
@@ -21,3 +21,3 @@ #Serverless Test Plugin | ||
**Note:** Serverless *v0.1.4* or higher is required. | ||
**Note:** Serverless *v0.5.0* or higher is required. | ||
@@ -49,10 +49,14 @@ | ||
Test an individual function: | ||
``` | ||
serverless function test <function> | ||
``` | ||
To test all functions in the current path, invoke the plugin without any function name: | ||
``` | ||
serverless function test <component>/<module>/<function> | ||
serverless function test | ||
``` | ||
Test all functions in the project: | ||
To test all functions in the project specify the `--all` parameter: | ||
``` | ||
@@ -63,4 +67,11 @@ serverless function test --all | ||
Test all functions and output results into a JUnit compatible XML: | ||
You can also specify a stage and/or a region for your tests. If none is specified, the | ||
first stage and region defined in your `_meta` folder will be used: | ||
``` | ||
serverless function test <function> --stage dev --region us-east-1 | ||
``` | ||
To test all functions and output results into a JUnit compatible XML, specify the | ||
`--out` parameter with a target file name: | ||
``` | ||
@@ -71,8 +82,14 @@ serverless function test --all --out test_results/report.xml | ||
To detect whether your code runs in a test environment or not, check for the `SERVERLESS_TEST` environment variable: | ||
Sometimes it is desirable to mock certain behavior in your code depending on whether it is running in a | ||
test automation script or on an actual server. For this reason the `serverless-test-plugin` | ||
introduces a dedicated environment variable `SERVERLESS_TEST`: | ||
``` | ||
if (process.env.SERVERLESS_TEST) { | ||
console.log("This code runs as part of an intergration test."); | ||
console.log("This code runs as part of an integration test."); | ||
} | ||
else { | ||
console.log("This code does NOT run as part of an integration test..") | ||
} | ||
``` |
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Dynamic require
Supply chain riskDynamic require can indicate the package is performing dangerous or unsafe dynamic code execution.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Dynamic require
Supply chain riskDynamic require can indicate the package is performing dangerous or unsafe dynamic code execution.
Found 1 instance in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
12975
5
229
92
4
Updatedbluebird@^3.3.5
Updatedchalk@^1.1.3