Repository: Codeception/CodeceptJS
Pull request #1841 (2019-08-12 11:51:26)

doc: Add graphql helpers and update data factory in data.md


Patch:
diff --git a/bin/codecept.js b/bin/codecept.js
index a157b293..5e20c69f 100755
--- a/bin/codecept.js
+++ b/bin/codecept.js
@@ -143,6 +143,22 @@ program.command('run-multiple [suites...]')
 
   .action(require('../lib/command/run-multiple'));
 
+program.command('dry-run [test]')
+  .description('Executes tests multiple')
+  .option('-p, --plugins <k=v,k2=v2,...>', 'enable plugins, comma-separated')
+  .option('--bootstrap', 'enable bootstrap script for dry-run')
+  .option('-c, --config [file]', 'configuration file to be used')
+  .option('--all', 'run all suites')
+  .option('--features', 'run only *.feature files and skip tests')
+  .option('--tests', 'run only JS test files and skip features')
+  .option('-g, --grep <pattern>', 'only run tests matching <pattern>')
+  .option('-f, --fgrep <string>', 'only run tests containing <string>')
+  .option('-i, --invert', 'inverts --grep and --fgrep matches')
+  .option('--steps', 'show step-by-step execution')
+  .option('--verbose', 'output internal logging information')
+  .option('--debug', 'output additional information')
+  .action(require('../lib/command/dryRun'));
+
 program.on('command:*', (cmd) => {
   console.log(`\nUnknown command ${cmd}\n`);
   program.outputHelp();
diff --git a/docs/angular.md b/docs/angular.md
index f5620e88..702a11ac 100644
--- a/docs/angular.md
+++ b/docs/angular.md
@@ -139,6 +139,7 @@ exports.config = {
   // inside condecept.conf.js
   plugins: {
     wdio: {
+        enabled: true,
         services: ['selenium-standalone']
     }
   }
diff --git a/docs/commands.md b/docs/commands.md
index 869fa3fb..8b1e53a1 100644
--- a/docs/commands.md
+++ b/docs/commands.md
@@ -13,78 +13,104 @@ Executes tests. Requires `codecept.conf.js` config to be present in provided pat
 Run all tests from current dir
 
 ```sh
-codeceptjs run
+npx codeceptjs run
 ```
 
 Load config and run tests from `test` dir
 
 ```sh
-codeceptjs run -c test
+npx codeceptjs run -c test
 ```
 
 Run only tests with "signin" word in name
 
 ```sh
-codeceptjs run --grep "signin"
+npx codeceptjs run --grep "signin"
 ```
 
 Run all tests without "@IEOnly" word in name
 
 ```sh
-codeceptjs run --grep "@IEOnly" --invert
+npx codeceptjs run --grep "@IEOnly" --invert
 ```
 
 Run single test [path to codecept.js] [test filename]
 
 ```sh
-codeceptjs run github_test.js
+npx codeceptjs run github_test.js
 ```
 
 Run single test with steps printed
 
 ```sh
-codeceptjs run github_test.js --steps
+npx codeceptjs run github_test.js --steps
 ```
 
 Run single test in debug mode
 
 ```sh
-codeceptjs run github_test.js --debug
+npx codeceptjs run github_test.js --debug
 ```
 
 Run test with internal logs printed (global promises, and events).
 
 ```sh
-codeceptjs run github_test.js --verbose
+npx codeceptjs run github_test.js --verbose
 ```
 
 Select config file manually (`-c` or `--config` option)
 
 ```sh
-codeceptjs run -c my.codecept.conf.js
-codeceptjs run --config path/to/codecept.json
+npx codeceptjs run -c my.codecept.conf.js
+npx codeceptjs run --config path/to/codecept.json
 ```
 
 Override config on the fly. Provide valid JSON which will be merged into current config:
 
 ```sh
-codeceptjs run --override '{ "helpers": {"WebDriver": {"browser": "chrome"}}}'
+npx codeceptjs run --override '{ "helpers": {"WebDriver": {"browser": "chrome"}}}'
 ```
 
 Run tests and produce xunit report:
 
 ```sh
-codeceptjs run --reporter xunit
+npx codeceptjs run --reporter xunit
 ```
 
 Use any of [Mocha reporters](https://github.com/mochajs/mocha/tree/master/lib/reporters) used.
 
+## Dry Run
+
+Prints test scenarios without executing them
+
+```
+npx codeceptjs dry-run
+```
+
+When passed `--steps` or `--debug` option runs tests, disabling all plugins and helpers, so you can get step-by-step report with no tests actually executed.
+
+```
+npx codeceptjs dry-run --steps
+```
+
+If a plugin needs to be enabled in `dry-run` mode, pass its name in `-p` option:
+
+```
+npx codeceptjs dry-run --steps -p allure
+```
+
+To enable bootstrap script in dry-run mode, pass in `--bootstrap` option when running with `--steps` or `--debug`
+
+```
+nox codeceptjs dry-run --steps --bootstrap
+```
+
 ## Run multiple
 
 Run multiple suites.
 
 ```sh
-codeceptjs run-multiple smoke:chrome regression:firefox
+npx codeceptjs run-multiple smoke:chrome regression:firefox
 ```
 
 ## Init
@@ -92,13 +118,13 @@ codeceptjs run-multiple smoke:chrome regression:firefox
 Creates `codecept.conf.js` file in current directory:
 
 ```sh
-codeceptjs init
+npx codeceptjs init
 ```
 
 Or in provided path
 
 ```sh
-codecept init test
+npx codecept init test
 ```
 
 ## Migrate
@@ -106,7 +132,7 @@ codecept init test
 Migrate your current `codecept.json` to `codecept.conf.js`
 
 ```sh
-codeceptjs migrate
+npx codeceptjs migrate
 ```
 
 ## Shell
@@ -114,7 +140,7 @@ codeceptjs migrate
 Interactive shell. Allows to try `I.` commands in runtime
 
 ```sh
-codeceptjs shell
+npx codeceptjs shell
 ```
 
 ## Generators
@@ -122,19 +148,19 @@ codeceptjs shell
 Create new test
 
 ```sh
-codeceptjs generate:test
+npx codeceptjs generate:test
 ```
 
 Create new pageobject
 
 ```sh
-codeceptjs generate:pageobject
+npx codeceptjs generate:pageobject
 ```
 
 Create new helper
 
 ```sh
-codeceptjs generate:helper
+npx codeceptjs generate:helper
 ```
 
 ## TypeScript Definitions
@@ -142,8 +168,8 @@ codeceptjs generate:helper
 TypeScript Definitions allows IDEs to provide autocompletion when writing tests.
 
 ```sh
-codeceptjs def
-codeceptjs def --config path/to/codecept.json
+npx codeceptjs def
+npx codeceptjs def --config path/to/codecept.json
 ```
 
 After doing that IDE should provide autocompletion for `I` object inside `Scenario` and `within` blocks.
@@ -151,8 +177,8 @@ After doing that IDE should provide autocompletion for `I` object inside `Scenar
 Add optional parameter `output` (or shortcat `-o`), if you want to place your definition file in specific folder:
 
 ```sh
-codeceptjs def --output ./tests/typings
-codeceptjs def -o ./tests/typings
+npx codeceptjs def --output ./tests/typings
+npx codeceptjs def -o ./tests/typings
 ```
 
 ## List Commands
@@ -160,5 +186,5 @@ codeceptjs def -o ./tests/typings
 Prints all available methods of `I` to console
 
 ```sh
-codeceptjs list
+npx codeceptjs list
 ```
diff --git a/docs/data.md b/docs/data.md
index 0b6eaa9b..cd26afe6 100644
--- a/docs/data.md
+++ b/docs/data.md
@@ -16,8 +16,8 @@ There are different approaches to solve it:
 The most efficient way would be to allow test to control its data, i.e. the 3rd option.
 However, accessing database directly is not a good idea as database vendor, schema and data are used by application internally and are out of scope of acceptance test.
 
-Today all modern web applications have REST API. So it is a good idea to use it to create data for a test and delete it after.
-API supposed to be a stable interface and it can be used by acceptance tests. CodeceptJS provides 2 helpers for Data Management via REST API.
+Today all modern web applications have REST or GraphQL API . So it is a good idea to use it to create data for a test and delete it after.
+API is supposed to be a stable interface and it can be used by acceptance tests. CodeceptJS provides 4 helpers for Data Management via REST and GraphQL API.
 
 ## REST
 
@@ -36,7 +36,6 @@ helpers: {
       'Auth': '11111',
       'Content-Type': 'application/json',
       'Accept': 'application/json',
-      },
     },
   },
   WebDriver : {
@@ -89,20 +88,90 @@ I.sendPostRequest('/update-status', {}, { http_x_requested_with: 'xmlhttprequest
 
 > See complete reference on [REST](http://codecept.io/helpers/REST) helper
 
-## Data Generation with Factories
+## GraphQL
+
+[GraphQL helper](http://codecept.io/helpers/GraphQL/) allows sending GraphQL queries and mutations to application, over Http.
+This is a tool to make shortcuts and create your data pragmatically via GraphQL endpoint. However, it doesn't provide tools for testing the endpoint, so it should be paired with WebDriver, Nightmare or Protractor helpers for browser testing.
+
+Enable GraphQL helper in the config. It is recommended to set `endpoint`, the URL to which the requests go to. If you need some authorization you can optionally set default headers too.
 
-This concept is extended by [ApiDataFactory](http://codecept.io/helpers/ApiDataFactory/) helper.
-It builds data according to defined rules and uses API to store them and automatically clean them up after a test,
-This way setting data for a test is as simple as writing:
+See the sample config:
 
 ```js
-// inside async function
-let post = await I.have('post');
-I.haveMultiple('comment', 5, { postId: post.id});
+helpers: {
+  GraphQL: {
+    endpoint: "http://localhost/graphql/",
+    defaultHeaders: {
+      'Auth': '11111',
+      'Content-Type': 'application/json',
+      'Accept': 'application/json',
+    },
+  },
+  WebDriver : {
+    url: 'http://localhost',
+    browser: 'chrome'
+  }
+}
+```
+
+GraphQL helper provides two basic methods to queries and mutations to application:
+
+```js
+I.sendQuery()
+I.sendMutation()
 ```
 
-Just define how many items of any kind you need and ApiDataFactory will create them for you.
-However, to make this work some preparations required.
+As well as a method for setting headers: `haveRequestHeaders`.
+
+Here is a usage example:
+
+```js
+let postData = null;
+
+Scenario('check post page', async (I)  => {
+  // valid access token
+  I.haveRequestHeaders({auth: '1111111'});
+  // get the first user
+  let response = await I.sendQuery('{ user(id:1) { id }}');
+  let user = response.data;
+  // create a post and save its Id
+  response = await I.sendMutation(
+    'mutation createPost($input: PostInput!) { createPost(input: $input) { id }}',
+    { 
+      input : {
+        author: user.data.id,
+        body: 'some text',
+      }
+    },
+  );
+  postData = response.data.data['createPost'];
+  // open browser page of new post
+  I.amOnPage(`/posts/${postData.slug}.html`);
+  I.see(postData.body, 'p.body');
+});
+
+// cleanup created data
+After((I) => {
+  I.sendMutation(
+    'mutation deletePost($id: ID!) { deletePost(id: $id) }',
+    { id: postData.id},
+  );
+});
+```
+
+> See complete reference on [GraphQL](http://codecept.io/helpers/GraphQL) helper
+
+## Data Generation with Factories
+
+This concept is extended by: 
+- [ApiDataFactory](http://codecept.io/helpers/ApiDataFactory/) helper, and,
+- [GraphQLDataFactory](http://codecept.io/helpers/GraphQLDataFactory/) helper.
+
+These helpers build data according to defined rules and use REST API or GraphQL mutations to store them and automatically clean them up after a test.
+
+Just define how many items of any kind you need and the data factory helper will create them for you. 
+
+To make this work some preparations are required.
 
 At first, you need data generation libraries which are [Rosie](https://github.com/rosiejs/rosie) and [Faker](https://www.npmjs.com/package/faker). Faker can generate random names, emails, texts, and Rosie uses them
 to generate objects using factories.
@@ -114,6 +183,22 @@ npm i rosie faker --save-dev
 ```
 
 Then create a module which will export a factory for an entity.
+And add that module as a part of the configuration for the helper.
+
+Please look at the respective Factory sections for examples for factory modules and configuration.
+
+### API Data Factory
+
+This helper uses REST API to store the built data and automatically clean them up after a test,
+The way for setting data for a test is as simple as writing:
+
+```js
+// inside async function
+let post = await I.have('post');
+I.haveMultiple('comment', 5, { postId: post.id});
+```
+
+After completing the preparations under 'Data Generation with Factories', create a factory module which will export a factory.
 
 See the example providing a factory for User generation:
 
@@ -154,15 +239,80 @@ This rules can be customized in helper configuration.
 
 > See complete reference on [ApiDataFactory](http://codecept.io/helpers/ApiDataFactory) helper
 
-## API Requests Using Browser Session
+### GraphQL Data Factory
+
+The helper uses GraphQL mutations to store the built data and automatically clean them up after a test.
+This way for setting data for a test is as simple as writing:
+
+```js
+// inside async function
+let post = await I.mutateData('createPost');
+I.mutateMultiple('createComment', 5, { postId: post.id});
+```
+
+
+
+After completing the preparations under 'Data Generation with Factories', create a factory module which will export a factory.
+
+The object built by the factory is sent as the variables object along with the mutation. So make sure it matches the argument type as detailed in the GraphQL schema. You may want to pass a constructor to the factory to achieve that.
+
+See the example providing a factory for User generation:
 
-Both REST and ApiDataFactory helpers allow override requests before sending.
-This feature can be used to fetch current browser cookies and set them to REST API client.
+```js
+// factories/post.js
+var Factory = require('rosie').Factory;
+var faker = require('faker');
+
+module.exports = new Factory((buildObj) => {
+  return {
+    input: { ...buildObj },
+  }
+})
+  .attr('name', () => faker.name.findName())
+  .attr('email', () => faker.internet.email());
+```
+
+Next is to configure helper to match factories with API:
+
+```js
+GraphQLDataFactory: {
+  endpoint: "http://user.com/graphql",
+  cleanup: true,
+  headers: {
+    'Content-Type': 'application/json',
+    'Accept': 'application/json',
+  },
+  factories: {
+    createUser: {
+      query: 'mutation createUser($input: UserInput!) { createUser(input: $input) { id name }}',
+      factory: './factories/users',
+      revert: (data) => ({
+        query: 'mutation deleteUser($id: ID!) { deleteUser(id: $id) }',
+        variables: { id : data.id},
+      }),
+    },
+  }
+```
+
+Then, calling `I.mutateData('createUser')` inside a test will create a new user for you.
+This is done by sending a GraphQL mutation request over Http to `/graphql` endpoint. Response is returned and can be used in tests.
+
+At the end of a test GraphQLDataFactory will clean up created record for you. This is done by collecting
+data from crated records, creating deletion mutation objects by passing the data to the `revert` function provided, and sending deletion mutation objects as requests at the end of a test.
+This behavior is according the `revert` function be customized in helper configuration.
+The revert function returns an object, that contains the query for deletion, and the variables object to go along with it.
+
+> See complete reference on [GraphQLDataFactory](http://codecept.io/helpers/GraphQLDataFactory) helper
+
+## Requests Using Browser Session
+
+All the REST, GraphQL, GraphQLDataFactory, and ApiDataFactory helpers allow override requests before sending.
+This feature can be used to fetch current browser cookies and set them to REST API or GraphQL client.
 By doing this we can make requests within the current browser session without a need of additional authentication.
 
-> Sharing browser session with ApiDataFactory can be especially useful when you test Single Page Applications
+> Sharing browser session with ApiDataFactory or GraphQLDataFactory can be especially useful when you test Single Page Applications
 
-Let's see how to configure ApiDataFactory alongside with WebDriver to share cookies:
+For example, let's see how to configure ApiDataFactory alongside with WebDriver to share cookies:
 
 ```js
   ApiDataFactory: {
@@ -191,5 +341,7 @@ Let's see how to configure ApiDataFactory alongside with WebDriver to share cook
 
 In this case we are accessing WebDriver helper. However, you can replace WebDriver with any helper you use.
 
+The same can be done with GraphQLDataFactory.
+
 The order of helpers is important! ApiDataFactory will clean up created users after a test,
 so it needs browser to be still opened to obtain its cookies.
diff --git a/docs/plugins.md b/docs/plugins.md
index e542f6a2..3bfd57d1 100644
--- a/docs/plugins.md
+++ b/docs/plugins.md
@@ -69,7 +69,7 @@ const allure = codeceptjs.container.plugins('allure');
 ## autoDelay
 
 Sometimes it takes some time for a page to respond to user's actions.
-Depending on app's perfromance this can be either slow or fast.
+Depending on app's performance this can be either slow or fast.
 
 For instance, if you click a button and nothing happens - probably JS event is not attached to this button yet
 Also, if you fill field and input validation doesn't accept your input - maybe because you typed value too fast.
diff --git a/docs/reports.md b/docs/reports.md
index ea4cd7fd..b0ff304f 100644
--- a/docs/reports.md
+++ b/docs/reports.md
@@ -5,8 +5,6 @@ title: Reporters
 
 ## Cli
 
-(default)
-
 By default CodeceptJS provides cli reporter with console output.
 Test names and failures will be printed to screen.
 
@@ -31,9 +29,15 @@ GitHub --
 
 
   Run with --verbose flag to see NodeJS stacktrace
+
+```
+npx codeceptjs run --stepsutput add `--steps` option to `run` command:
+```
+```
+npx codeceptjs run --steps
 ```
 
-For dynamic step-by-step output add `--steps` option to `run` command:
+Output:
 
 ```sh
 GitHub --
@@ -65,10 +69,22 @@ GitHub --
  ✖ FAILED in 1260ms
 ```
 
-To get additional information about test execution use `--debug` option. This will show execution steps
+To get additional information about test execution use `--debug` option.
+
+
+```
+npx codeceptjs run --debug
+```
+
+
+This will show execution steps
 as well as notices from test runner. To get even more information with more technical details like error stack traces,
 and global promises, or events use `--verbose` mode.
 
+```
+npx codeceptjs run --verbose
+```
+
 ```sh
 GitHub --
  register
@@ -86,11 +102,46 @@ GitHub --
 
 Please use verbose output when reporting issues to GitHub.
 
+### Dry Run
 
-## Allure
+There is a way to list all tests and their steps without actually executing them. Execute tests in `dry-run` mode to see all available tests:
 
-(recommended)
+```
+npx codeceptjs dry-run
+```
 
+Output:
+
+```
+Tests from /home/davert/projects/codeceptjs/examples:
+
+Business rules --
+  ☐ do something
+Google --
+  ☐ test @123
+GitHub -- /home/davert/projects/codeceptjs/examples/github_test.js
+  ☐ Visit Home Page @retry
+  ☐ search @grop
+  ☐ signin @normal @important @slow
+  ☐ signin2
+  ☐ register
+
+  Total: 3 suites | 7 tests
+
+--- DRY MODE: No tests were executed ---
+```
+
+Pass `--steps` or `--debug` option as in `run` command to also get steps and substeps to be printed. In this mode **tests will be executed** but all helpers and plugins disabled, so no real actions will be performed.
+
+```
+npx codecepjs dry-run --debug
+```
+
+> ℹ If you use custom JavaScript code inside tests, or rely on values from `grab*` commands, dry-run may produce error output.
+
+## Allure
+
+> ℹ  We recommend using Allure reports on CI. Allure is one of the best open-source reporters designed to collect and show test reports in nicest way.
 
 [Allure reporter](http://allure.qatools.ru/#) is a tool to store and display test reports.
 It provides nice web UI which contains all important information on test execution.
@@ -98,7 +149,7 @@ CodeceptJS has built-in support for Allure reports. Inside reports you will have
 
 ![](https://user-images.githubusercontent.com/220264/45676511-8e052800-bb3a-11e8-8cbb-db5f73de2add.png)
 
-*Disclaimer: Allure is a standalone tool. Please refer to [Allure documentation](https://docs.qameta.io/allure/) to learn more about using Allure reports.*
+> ▶ Allure is a standalone tool. Please refer to [Allure documentation](https://docs.qameta.io/allure/) to learn more about using Allure reports.
 
 Allure requires **Java 8** to work. Then Allure can be installed via NPM:
 
@@ -109,16 +160,16 @@ npm install -g allure-commandline --save-dev
 Add [Allure plugin](https://codecept.io/plugins/#allure) in config under `plugins` section.
 
 ```js
-"plugins": {
-    "allure": {
-    }
+plugins: {
+    allure: {
+  }
 }
 ```
 
 Run tests with allure plugin enabled:
 
 ```
-codeceptjs run --plugins allure
+npx codeceptjs run --plugins allure
 ```
 
 (optionally) To enable allure plugin permanently include `"enabled": true` into plugin config:
@@ -140,6 +191,11 @@ allure serve output
 
 Allure reporter aggregates data from other plugins like [*stepByStepReport*](https://codecept.io/plugins/#stepByStepReport) and [*screenshotOnFail*](https://codecept.io/plugins/#screenshotOnFail)
 
+Allure reports can also be generated for `dry-run` command. So you can get the first report with no tests actually being executed. Enable allure plugin in dry-run options, and pass `--debug` option to print all tests on screen.
+
+```
+npx codeceptjs dry-run --debug -p allure
+```
 
 ## XML
 
@@ -301,7 +357,7 @@ Configure mocha-multi with reports that you want:
 Execute CodeceptJS with mocha-multi reporter:
 
 ```sh
-codeceptjs run --reporter mocha-multi
+npx codeceptjs run --reporter mocha-multi
 ```
 
 This will give you cli with steps in console and HTML report in `output` directory.
diff --git a/lib/command/dryRun.js b/lib/command/dryRun.js
new file mode 100644
index 00000000..11f50064
--- /dev/null
+++ b/lib/command/dryRun.js
@@ -0,0 +1,80 @@
+const getConfig = require('./utils').getConfig;
+const getTestRoot = require('./utils').getTestRoot;
+const Config = require('../config');
+const Codecept = require('../codecept');
+const output = require('../output');
+const event = require('../event');
+const store = require('../store');
+const Container = require('../container');
+
+module.exports = function (test, options) {
+  const configFile = options.config;
+  let codecept;
+
+  const testRoot = getTestRoot(configFile);
+  let config = getConfig(configFile);
+  if (options.override) {
+    config = Config.append(JSON.parse(options.override));
+  }
+
+  if (config.plugins) {
+    // disable all plugins by default, they can be enabled with -p option
+    for (const plugin in config.plugins) {
+      config.plugins[plugin].enabled = false;
+    }
+  }
+
+  try {
+    codecept = new Codecept(config, options);
+    codecept.init(testRoot);
+
+    if (options.bootstrap) codecept.runBootstrap();
+
+    codecept.loadTests();
+    store.dryRun = true;
+
+    if (!options.steps && !options.verbose && !options.debug) {
+      printTests(codecept.testFiles);
+      return;
+    }
+    event.dispatcher.on(event.all.result, printFooter);
+    codecept.run(test);
+  } catch (err) {
+    console.error(err);
+    process.exit(1);
+  }
+};
+
+function printTests(files) {
+  const figures = require('figures');
+  const colors = require('chalk');
+
+  output.print(output.styles.debug(`Tests from ${global.codecept_dir}:`));
+  output.print();
+
+  const mocha = Container.mocha();
+  mocha.files = files;
+  mocha.loadFiles();
+
+  let numOfTests = 0;
+  let numOfSuites = 0;
+
+  for (const suite of mocha.suite.suites) {
+    output.print(`${colors.white.bold(suite.title)} -- ${output.styles.log(suite.file || '')}`);
+    numOfSuites++;
+
+    for (const test of suite.tests) {
+      numOfTests++;
+      output.print(`  ${output.styles.scenario(figures.checkboxOff)} ${test.title}`);
+    }
+  }
+
+  output.print('');
+  output.success(`  Total: ${numOfSuites} suites | ${numOfTests} tests  `);
+  printFooter();
+}
+
+function printFooter() {
+  output.print();
+  output.print('--- DRY MODE: No tests were executed ---');
+}
diff --git a/lib/command/run-multiple/chunk.js b/lib/command/run-multiple/chunk.js
index 5ac7d0a0..03f0f579 100644
--- a/lib/command/run-multiple/chunk.js
+++ b/lib/command/run-multiple/chunk.js
@@ -52,7 +52,7 @@ const grepFile = (file, grep) => {
 const mapFileFormats = (files) => {
   return {
     gherkin: files.filter(file => file.match(/\.feature$/)),
-    js: files.filter(file => file.match(/\.js$/)),
+    js: files.filter(file => file.match(/\.t|js$/)),
   };
 };
 
diff --git a/lib/listener/helpers.js b/lib/listener/helpers.js
index 8e911724..4e5a7344 100644
--- a/lib/listener/helpers.js
+++ b/lib/listener/helpers.js
@@ -1,6 +1,7 @@
 const event = require('../event');
 const container = require('../container');
 const recorder = require('../recorder');
+const store = require('../store');
 const error = require('../output').error;
 const { deepMerge } = require('../utils');
 /**
@@ -10,12 +11,14 @@ module.exports = function () {
   const helpers = container.helpers();
 
   const runHelpersHook = (hook, param) => {
+    if (store.dryRun) return;
     Object.keys(helpers).forEach((key) => {
       helpers[key][hook](param);
     });
   };
 
   const runAsyncHelpersHook = (hook, param, force) => {
+    if (store.dryRun) return;
     Object.keys(helpers).forEach((key) => {
       if (!helpers[key][hook]) return;
       recorder.add(`hook ${key}.${hook}()`, () => helpers[key][hook](param), force);
diff --git a/lib/pause.js b/lib/pause.js
index 47f433b3..2b57720c 100644
--- a/lib/pause.js
+++ b/lib/pause.js
@@ -18,6 +18,8 @@ let next;
  * Pauses test execution and starts interactive shell
  */
 const pause = function () {
+  if (store.dryRun) return;
+
   next = false;
   // add listener to all next steps to provide next() functionality
   event.dispatcher.on(event.step.after, () => {
diff --git a/lib/plugin/autoDelay.js b/lib/plugin/autoDelay.js
index dc5a89b7..2a300eb8 100644
--- a/lib/plugin/autoDelay.js
+++ b/lib/plugin/autoDelay.js
@@ -31,7 +31,7 @@ const defaultConfig = {
 /**
  *
  * Sometimes it takes some time for a page to respond to user's actions.
- * Depending on app's perfromance this can be either slow or fast.
+ * Depending on app's performance this can be either slow or fast.
  *
  * For instance, if you click a button and nothing happens - probably JS event is not attached to this button yet
  * Also, if you fill field and input validation doesn't accept your input - maybe because you typed value too fast.
diff --git a/lib/session.js b/lib/session.js
index aae090fe..a2b6cd23 100644
--- a/lib/session.js
+++ b/lib/session.js
@@ -2,6 +2,7 @@ const recorder = require('./recorder');
 const container = require('./container');
 const event = require('./event');
 const output = require('./output');
+const store = require('./store');
 const { isAsyncFunction } = require('./utils');
 
 const sessionColors = [
@@ -38,7 +39,7 @@ function session(sessionName, config, fn) {
         stop: () => null,
         loadVars: () => null,
         restoreVars: () => null,
-      }, helpers[helper]._session());
+      }, store.dryRun ? {} : helpers[helper]._session());
       break;
     }
 
diff --git a/lib/step.js b/lib/step.js
index 877f33aa..9ecee54f 100644
--- a/lib/step.js
+++ b/lib/step.js
@@ -36,6 +36,10 @@ class Step {
 
   run() {
     this.args = Array.prototype.slice.call(arguments);
+    if (store.dryRun) {
+      this.setStatus('success');
+      return Promise.resolve(new Proxy({}, dryRunResolver()));
+    }
     let result;
     try {
       result = this.helper[this.helperMethod].apply(this.helper, this.args);
@@ -191,3 +195,12 @@ function isTest(line) {
 function isBDD(line) {
   return line.trim().match(/^at (Given|When|Then)/);
 }
+
+function dryRunResolver() {
+  return {
+    get(target, prop) {
+      if (prop === 'toString') return () => '<VALUE>';
+      return new Proxy({}, dryRunResolver());
+    },
+  };
+}
diff --git a/lib/within.js b/lib/within.js
index e40a2c5c..2016a1db 100644
--- a/lib/within.js
+++ b/lib/within.js
@@ -1,4 +1,5 @@
 const output = require('./output');
+const store = require('./store');
 const recorder = require('./recorder');
 const container = require('./container');
 const event = require('./event');
@@ -6,7 +7,7 @@ const Step = require('./step');
 const { isAsyncFunction } = require('./utils');
 
 function within(context, fn) {
-  const helpers = container.helpers();
+  const helpers = store.dryRun ? {} : container.helpers();
   const locator = typeof context === 'object' ? JSON.stringify(context) : context;
 
   return recorder.add('register within wrapper', () => {


Test patch:
diff --git a/test/runner/dry_run_test.js b/test/runner/dry_run_test.js
new file mode 100644
index 00000000..73fde090
--- /dev/null
+++ b/test/runner/dry_run_test.js
@@ -0,0 +1,207 @@
+const assert = require('assert');
+const path = require('path');
+const expect = require('chai').expect;
+
+const exec = require('child_process').exec;
+
+const runner = path.join(__dirname, '/../../bin/codecept.js');
+const codecept_dir = path.join(__dirname, '/../data/sandbox');
+const codecept_run = `${runner} dry-run`;
+const codecept_run_config = config => `${codecept_run} --config ${codecept_dir}/${config}`;
+const config_run_override = config => `${codecept_run} --override '${JSON.stringify(config)}'`;
+const char = require('figures').checkboxOff;
+
+describe('dry-run command', () => {
+  before(() => {
+    process.chdir(codecept_dir);
+  });
+
+  it('should be executed with config path', (done) => {
+    process.chdir(__dirname);
+    exec(`${codecept_run} -c ${codecept_dir}`, (err, stdout, stderr) => {
+      stdout.should.include('Filesystem'); // feature
+      stdout.should.include('check current dir'); // test name
+      assert(!err);
+      done();
+    });
+  });
+
+  it('should list all tests', (done) => {
+    process.chdir(__dirname);
+    exec(`${codecept_run} -c ${codecept_dir}`, (err, stdout, stderr) => {
+      stdout.should.include('Filesystem'); // feature
+      stdout.should.include('check current dir'); // test name
+      stdout.should.not.include('I am in path'); // step name
+      stdout.should.not.include('I see file'); // step name
+      stdout.should.include('No tests were executed');
+      assert(!err);
+      done();
+    });
+  });
+
+  it('should not run actual steps', (done) => {
+    exec(codecept_run_config('codecept.flaky.json'), (err, stdout, stderr) => {
+      stdout.should.include('Flaky'); // feature
+      stdout.should.include('Not so flaky test'); // test name
+      stdout.should.include('Old style flaky'); // test name
+      stdout.should.not.include('[T1] Retries: 2');
+      stdout.should.not.include('[T2] Retries: 4');
+      stdout.should.not.include('[T3] Retries: 1');
+      stdout.should.include('No tests were executed');
+      assert(!err);
+      done();
+    });
+  });
+
+  it('should not run helper hooks', (done) => {
+    exec(`${codecept_run_config('codecept.testhooks.json')} --debug`, (err, stdout) => {
+      const lines = stdout.match(/\S.+/g);
+
+      expect(lines).to.not.include.members([
+        'Helper: I\'m initialized',
+        'Helper: I\'m simple BeforeSuite hook',
+        'Helper: I\'m simple Before hook',
+        'Helper: I\'m simple After hook',
+        'Helper: I\'m simple AfterSuite hook',
+      ]);
+
+      expect(lines).to.include.members([
+        'Test: I\'m simple BeforeSuite hook',
+        'Test: I\'m simple Before hook',
+        'Test: I\'m simple After hook',
+        'Test: I\'m simple AfterSuite hook',
+      ]);
+
+      stdout.should.include('OK  | 1 passed');
+      stdout.should.include('No tests were executed');
+      assert(!err);
+      done();
+    });
+  });
+
+  it('should display meta steps and substeps', (done) => {
+    exec(`${codecept_run_config('codecept.po.json')} --debug`, (err, stdout) => {
+      const lines = stdout.split('\n');
+      lines.should.include.members([
+        '  check current dir',
+        '    I: openDir ',
+        '      I am in path "."',
+        '      I see file "codecept.json"',
+        '    MyPage: hasFile ',
+        '      I see file "codecept.json"',
+        '      I see file "codecept.po.json"',
+        '    I see file "codecept.po.json"',
+      ]);
+      stdout.should.include('OK  | 1 passed');
+      stdout.should.include('No tests were executed');
+      assert(!err);
+      done();
+    });
+  });
+
+  it('should run feature files', (done) => {
+    exec(codecept_run_config('codecept.bdd.json') + ' --steps --grep "Checkout process"', (err, stdout, stderr) => { //eslint-disable-line
+      stdout.should.include('Checkout process'); // feature
+      stdout.should.include('-- before checkout --');
+      stdout.should.include('-- after checkout --');
+      // stdout.should.include('In order to buy products'); // test name
+      stdout.should.include('Given I have product with $600 price');
+      stdout.should.include('And I have product with $1000 price');
+      stdout.should.include('Then I should see that total number of products is 2');
+      stdout.should.include('And my order amount is $1600');
+      stdout.should.not.include('I add item 600'); // 'Given' actor's non-gherkin step check
+      stdout.should.not.include('I see sum 1600'); // 'And' actor's non-gherkin step check
+      stdout.should.include('No tests were executed');
+      assert(!err);
+      done();
+    });
+  });
+
+  it('should print substeps in debug mode', (done) => {
+    exec(codecept_run_config('codecept.bdd.json') + ' --debug --grep "Checkout process"', (err, stdout, stderr) => { //eslint-disable-line
+      stdout.should.include('Checkout process'); // feature
+      // stdout.should.include('In order to buy products'); // test name
+      stdout.should.include('Given I have product with $600 price');
+      stdout.should.include('I add item 600');
+      stdout.should.include('And I have product with $1000 price');
+      stdout.should.include('I add item 1000');
+      stdout.should.include('Then I should see that total number of products is 2');
+      stdout.should.include('I see num 2');
+      stdout.should.include('And my order amount is $1600');
+      stdout.should.include('I see sum 1600');
+      stdout.should.include('No tests were executed');
+      assert(!err);
+      done();
+    });
+  });
+
+  it('should run tests with different data', (done) => {
+    exec(codecept_run_config('codecept.ddt.json'), (err, stdout, stderr) => {
+      const output = stdout.replace(/in [0-9]ms/g, '').replace(/\r/g, '');
+      output.should.include(`${char} Should log accounts1 | {"login":"davert","password":"123456"}`);
+      output.should.include(`${char} Should log accounts1 | {"login":"admin","password":"666666"}`);
+      output.should.include(`${char} Should log accounts2 | {"login":"andrey","password":"555555"}`);
+      output.should.include(`${char} Should log accounts2 | {"login":"collaborator","password":"222222"}`);
+      output.should.include(`${char} Should log accounts3 | ["nick","pick"]`);
+      output.should.include(`${char} Should log accounts3 | ["jack","sacj"]`);
+      output.should.include(`${char} Should log accounts4 | {"user":"nick"}`);
+      output.should.include(`${char} Should log accounts4 | {"user":"pick"}`);
+      output.should.include(`${char} Should log array of strings | {"1"}`);
+      output.should.include(`${char} Should log array of strings | {"2"}`);
+      output.should.include(`${char} Should log array of strings | {"3"}`);
+
+      assert(!err);
+      done();
+    });
+  });
+
+  it('should display meta steps and substeps', (done) => {
+    exec(`${codecept_run_config('codecept.po.json')} --debug`, (err, stdout) => {
+      const lines = stdout.split('\n');
+      lines.should.include.members([
+        '  check current dir',
+        '    I: openDir ',
+        '      I am in path "."',
+        '      I see file "codecept.json"',
+        '    MyPage: hasFile ',
+        '      I see file "codecept.json"',
+        '      I see file "codecept.po.json"',
+        '    I see file "codecept.po.json"',
+      ]);
+      stdout.should.include('OK  | 1 passed');
+      stdout.should.include('No tests were executed');
+      assert(!err);
+      done();
+    });
+  });
+
+  it('should work with inject() keyword', (done) => {
+    exec(`${codecept_run_config('codecept.inject.po.json')} --debug`, (err, stdout) => {
+      const lines = stdout.split('\n');
+      stdout.should.include('injected');
+      lines.should.include.members([
+        '  check current dir',
+        '    I: openDir ',
+        '      I am in path "."',
+        '      I see file "codecept.json"',
+        '    MyPage: hasFile ',
+        '      I see file "codecept.json"',
+        '      I see file "codecept.po.json"',
+        '    I see file "codecept.po.json"',
+      ]);
+      stdout.should.include('OK  | 1 passed');
+      assert(!err);
+      done();
+    });
+  });
+
+  it('should inject page objects via proxy', (done) => {
+    exec(`${codecept_run_config('../inject-fail-example')} --debug`, (err, stdout) => {
+      stdout.should.include('newdomain');
+      stdout.should.include("[ 'veni', 'vedi', 'vici' ]", 'array objects work');
+      stdout.should.include('OK  | 1 passed');
+      assert(!err);
+      done();
+    });
+  });
+});