From 558213123dd978599a884a9ef08bdf4e6577d77e Mon Sep 17 00:00:00 2001 From: Davert Date: Sun, 11 Dec 2016 03:30:47 +0200 Subject: [PATCH] fixed hooks, added override runner option, runner tests included --- CHANGELOG.md | 14 ++++++- bin/codecept.js | 1 + docs/commands.md | 6 +++ docs/configuration.md | 67 ++++++++++++++++++-------------- lib/command/run.js | 6 +++ lib/command/utils.js | 26 +++++++++++++ lib/hooks.js | 8 +++- test/runner/config_test.js | 79 +++++++++++++++++++++++++++++++++----- 8 files changed, 166 insertions(+), 41 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 462becaf4..0d6d400b7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,18 @@ +## 0.4.12 + +* Bootstrap / Teardown improved with [Hooks](codecept.io/configuration/#hooks). Various options for setup/teardown provided. +* Added `--override` or `-o` option for runner to dynamically override configs. Valid JSON should be passed: + +``` +codeceptjs run -o '{ "bootstrap": "bootstrap.js"}' +codeceptjs run -o '{ "helpers": {"WebDriverIO": {"browser": "chrome"}}}' +``` + +* Added [regression tests](https://github.com/Codeception/CodeceptJS/tree/master/test/runner) for codeceptjs tests runner. + ## 0.4.11 -* Fixed regression in 0.4.11 +* Fixed regression in 0.4.10 * Added `bootstrap`/`teardown` config options to accept functions as parameters by @pscanf. See updated [config reference](http://codecept.io/configuration/) #319 ## 0.4.10 diff --git a/bin/codecept.js b/bin/codecept.js index 49e5e3ebf..3ca800da7 100755 --- a/bin/codecept.js +++ b/bin/codecept.js @@ -57,6 +57,7 @@ program.command('run [suite] [test]') .option('--steps', 'show step-by-step execution') .option('--debug', 'output additional information') .option('--verbose', 'output internal logging information') + .option('-o, --override [value]', 'override current config options') .option('--profile [value]', 'configuration profile to be used') .option('--config [file]', 'configuration file to be used') diff --git a/docs/commands.md b/docs/commands.md index 6d5dca02d..c401369e3 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -54,6 +54,12 @@ Select config file manually codeceptjs run --config my.codecept.conf.js` ``` +Override config on the fly. Provide valid JSON which will be merged into current config: + +``` +codeceptjs run --override '{ "helpers": {"WebDriverIO": {"browser": "chrome"}}}' +``` + Run tests and produce xunit report: ``` diff --git a/docs/configuration.md b/docs/configuration.md index 77163b2e9..37386c5a7 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -13,26 +13,15 @@ Here is an overview of available options with their defaults: * **helpers**: `{}` - list of enabled helpers * **mocha**: `{}` - mocha options, [reporters](http://codecept.io/reports/) can be configured here * **name**: `"tests"` - test suite name (not used) -* **bootstrap**: `"./bootstrap.js"` - an option to run code _before_ tests are run (see example in a section below). It can either be: - * a path to a js file that will be executed (via `require`) before tests. If the file exports a - function, the function is called right away with a callback parameter. When the - callback is called with no arguments, tests are executed. If instead the callback is called with an - error as first argument, test execution is aborted and the process stops. - * a function (dynamic configuration only). The function is called before tests with a callback function - as the only parameter. When the callback is called with no arguments, tests are executed. If instead - the callback is called with an error as first argument, test execution is aborted and the process stops. -* **teardown**: - an option to run code _after_ tests are run (see example in a section below). It can either be: - * a path to a js file that will be executed (via `require`) after tests. If the file exports a - function, the function is called right away with a callback parameter. - * a function (dynamic configuration only). The function is called after tests with a callback parameter. - +* **bootstrap**: `"./bootstrap.js"` - an option to run code _before_ tests are run [Hooks](#hooks)). +* **teardown**: - an option to run code _after_ tests are run (see [Hooks](#hooks)). * **translation**: - [locale](http://codecept.io/translation/) to be used to print steps output, as well as used in source code. ## Dynamic Configuration By default `codecept.json` is used for configuration. However, you can switch to JS format for more dynamic options. - Create `codecept.conf.js` file and make it export `config` property. + Create `codecept.conf.js` file and make it export `config` property: See the config example: @@ -71,39 +60,62 @@ exports.config = { (Don't copy-paste this config, it's just demo) -### Bootstrap / Teardown +## Hooks + +Hooks are implemented as `bootstrap` and `teardown` options in config. You can use them to prepare test environment before execution and cleanup after. +They can be used to launch stop webserver, selenium server, etc. There are different sync and async ways to define bootstrap and teardown functions. + +`bootstrap` and `teardown` options can be: -`bootstrap` and `teardown` options can accept either a JS file to be executed or a function in case of dynamic config. -If a file returns a function with a param it is considered to be asynchronous. This can be useful to start server in the beginning of tests and stop it after: +* JS file, executed as is (synchronously). +* JS file exporting a function; If function accepts a callback is executed asynchronously. See example: -File: `codecept.json`: +Config (`codecept.json`): ```js "bootstrap": "./bootstrap.js" - "teardown": "./teardown.js" ``` -File: `bootstrap.js`: +Bootstrap file (`bootstrap.js`): ```js -global.server = require('./app_server'); +// bootstrap.js +var server = require('./app_server'); module.exports = function(done) { server.launch(done); } ``` -File: `teardown.js`: +* JS file exporting an object with `bootstrap` and (or) `teardown` methods for corresponding hooks. + +Config (`codecept.json`): ```js -module.exports = function(done) { - server.stop(done); + "bootstrap": "./bootstrap.js" + "teardown": "./bootstrap.js" +``` + +Bootstrap file (`bootstrap.js`): + +```js +// bootstrap.js +var server = require('./app_server'); +module.exports = { + bootstrap: function(done) { + server.launch(done); + }, + teardown: function(done) { + server.stop(done); + } } ``` -In case of dynamic config bootstrap/teardown functions can be placed inside a config itself: +* JS function in case of dynamic config. If function accepts a callback is executed asynchronously. See example: + +Config JS (`codecept.conf.js`): ```js -let server = require('./app_server'); +var server = require('./app_server'); exports.config = { bootstrap: function(done) { @@ -118,9 +130,6 @@ exports.config = { ``` -If bootstrap / teardown function doesn't accept a param it is executed as is, in sync manner. -Synchronous execution also happens if bootstrap/teardown file is required but not exporting anything. - ## Profile Using values from `process.profile` you can change the config dynamically. diff --git a/lib/command/run.js b/lib/command/run.js index c659bfc3b..82c6769b8 100644 --- a/lib/command/run.js +++ b/lib/command/run.js @@ -1,6 +1,7 @@ 'use strict'; let getConfig = require('./utils').getConfig; let getTestRoot = require('./utils').getTestRoot; +let deepMerge = require('./utils').deepMerge; let Codecept = require('../codecept'); let output = require('../output'); @@ -12,6 +13,11 @@ module.exports = function (suite, test, options) { let testRoot = getTestRoot(suite); let config = getConfig(testRoot, configFile); + + // override config with options + if (options.override) { + config = deepMerge(config, JSON.parse(options.override)); + } try { codecept = new Codecept(config, options); codecept.init(testRoot, function (err) { diff --git a/lib/command/utils.js b/lib/command/utils.js index ecf543233..fb6aae834 100644 --- a/lib/command/utils.js +++ b/lib/command/utils.js @@ -12,6 +12,11 @@ module.exports.getTestRoot = function (currentPath) { module.exports.getConfig = function (testRoot, configFile) { + // using relative config + if (configFile && !path.isAbsolute(configFile)) { + configFile = path.join(testRoot, configFile); + } + let config, manualConfigFile = configFile && path.resolve(configFile), jsConfigFile = path.join(testRoot, 'codecept.conf.js'), @@ -41,8 +46,29 @@ module.exports.getConfig = function (testRoot, configFile) { process.exit(1); }; +function isObject(item) { + return (item && typeof item === 'object' && !Array.isArray(item)); +} + +function deepMerge(target, source) { + if (isObject(target) && isObject(source)) { + for (const key in source) { + if (isObject(source[key])) { + if (!target[key]) Object.assign(target, { [key]: {} }); + deepMerge(target[key], source[key]); + } else { + Object.assign(target, { [key]: source[key] }); + } + } + } + return target; +} + +module.exports.deepMerge = deepMerge; + function configWithDefaults(config) { if (!config.include) config.include = {}; if (!config.helpers) config.helpers = {}; return config; } + diff --git a/lib/hooks.js b/lib/hooks.js index 6b4b6a7c6..92032452b 100644 --- a/lib/hooks.js +++ b/lib/hooks.js @@ -2,6 +2,7 @@ let getParamNames = require('./utils').getParamNames; let fsPath = require('path'); +let fileExists = require('./utils').fileExists; module.exports = function(hook, config, done) { if (typeof config[hook] === 'string' && fileExists(fsPath.join(codecept_dir, config[hook]))) { @@ -10,6 +11,10 @@ module.exports = function(hook, config, done) { callSync(callable, done); return; } + if (typeof callable === 'object' && callable[hook]) { + callSync(callable[hook], done); + return; + } } else if (typeof config[hook] === 'function') { callSync(config[hook], done); return; @@ -27,5 +32,6 @@ function callSync(callable, done) { } function isAsync(fn) { - return getParamNames(fn).length > 0; + let params = getParamNames(fn); + return params && params.length; } \ No newline at end of file diff --git a/test/runner/config_test.js b/test/runner/config_test.js index edff02a92..6a8f17a0e 100644 --- a/test/runner/config_test.js +++ b/test/runner/config_test.js @@ -1,9 +1,13 @@ 'use strict'; let should = require('chai').should(); +let assert = require('assert'); let path = require('path'); const exec = require('child_process').exec; let runner = path.join(__dirname, '/../../bin/codecept.js'); let codecept_dir = path.join(__dirname, '/../data/sandbox') +let codecept_run = runner +' run '+codecept_dir + ' '; +let config_run_config = (config) => `${codecept_run} --config ${config}`; +let config_run_override = (config) => `${codecept_run} --override '${JSON.stringify(config)}'`; let fs; describe('CodeceptJS Runner', () => { @@ -13,31 +17,86 @@ describe('CodeceptJS Runner', () => { }); it('should be executed', (done) => { - exec(runner +' run '+codecept_dir, (err, stdout, stderr) => { + exec(codecept_run, (err, stdout, stderr) => { stdout.should.include('Filesystem'); // feature stdout.should.include('check current dir'); // test name + assert(!err); done(); - }) + }); }); - xit('should return -1 on fail', () => { - + it('should show failures and exit with 1 on fail', (done) => { + exec(config_run_config('codecept.failed.json'), (err, stdout, stderr) => { + stdout.should.include('Not-A-Filesystem'); + stdout.should.include('file is not in dir'); + stdout.should.include('FAILURES'); + err.code.should.eql(1); + done(); + }); }); - xit('should run bootstrap', () => { - + it('should run bootstrap', (done) => { + exec(config_run_config('codecept.bootstrap.sync.json'), (err, stdout, stderr) => { + stdout.should.include('Filesystem'); // feature + stdout.should.include('I am bootstrap'); + assert(!err); + done(); + }); }); - xit('should run teardown', () => { + it('should run teardown', (done) => { + exec(config_run_override({teardown: 'bootstrap.sync.js'}), (err, stdout, stderr) => { + stdout.should.include('Filesystem'); // feature + stdout.should.include('I am bootstrap'); + assert(!err); + done(); + }); + }); + it('should run async bootstrap', (done) => { + exec(config_run_override({bootstrap: 'bootstrap.async.js'}), (err, stdout, stderr) => { + stdout.should.include('Ready: 0'); + stdout.should.include('Go: 1'); + stdout.should.include('Filesystem'); // feature + assert(!err); + done(); + }); }); - xit('should be executed with config', () => { + it('should run bootstrap/teardown as object', (done) => { + exec(config_run_config('codecept.hooks.obj.json'), (err, stdout, stderr) => { + stdout.should.include('Filesystem'); // feature + stdout.should.include('I am bootstrap'); + stdout.should.include('I am teardown'); + assert(!err); + done(); + }); + }); + it('should run dynamic config', (done) => { + exec(config_run_config('config.js'), (err, stdout, stderr) => { + stdout.should.include('Filesystem'); // feature + assert(!err); + done(); + }); }); - xit('should try different configs to load', () => { + it('should run dynamic config with profile', (done) => { + exec(config_run_config('config.js') + ' --profile failed', (err, stdout, stderr) => { + stdout.should.include('FAILURES'); + stdout.should.not.include('I am bootstrap'); + assert(err.code); + done(); + }); + }); - }) + it('should run dynamic config with profile 2', (done) => { + exec(config_run_config('config.js') + ' --profile bootstrap', (err, stdout, stderr) => { + stdout.should.not.include('FAILURES'); // feature + stdout.should.include('I am bootstrap'); + assert(!err); + done(); + }); + }); }); \ No newline at end of file