Repository: Codeception/CodeceptJS
Pull request #568 (2017-06-22 09:35:23)

Fix work with promise chains
Our goals:
- [x] Fix https://github.com/Codeception/CodeceptJS/issues/543 - After block not properly executed if Scenario fails
- [x] Expected behavior in promise chains: `_beforeSuite` hooks from helpers -> `BeforeSuite` from test -> `_before` hooks from helpers -> `Before` from test - > Test steps -> `_failed` hooks from helpers (if test failed) -> `After` from test -> `_after` hooks from helpers -> `AfterSuite` from test -> `_afterSuite` hook from helpers.
- [x] if during test we got errors from any hook (in test or in helper) - stop complete this suite and go to another
- [x] if during test we got error from Selenium server - stop complete this suite and go to another
- [x] if `restart` option is false - close all tabs expect one in `_after` (ready for webdriverIO, SeleniumWebdriver and Protractor. For Nightmare it will be hard to add (I found only this way https://github.com/rosshinkley/nightmare-window-manager)
- [x] Complete `_after`, `_afterSuite` hooks even After/AfterSuite from test was failed
- [x] Don't close browser between suites, when `restart` option is false. We should start browser only one time and close it only after all tests.
- [x] Close tabs and clear local storage, if `keepCookies` flag is enabled

Bonus:
- [x] Add step execution time in debug mode
- [x] Fix https://github.com/Codeception/CodeceptJS/issues/548

DavertMik: Please fix `test/runner/interface_test.js`, it tests the order of events
DavertMik: Wow, awesome work. We are getting closer and closer to 1.0 release!
DavertMik: Please try to use interactive shell to check that nothing was broken for it.
Also, execute one test with `within` block to be sure this changes to recorder doesn't break them.

Patch:
diff --git a/.travis.yml b/.travis.yml
index 645e172f..82d1c4a6 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -27,7 +27,9 @@ before_script:
 - sh -e /etc/init.d/xvfb start
 - chmod -R 777 test/data
 - php -S 127.0.0.1:8000 -t test/data/app >/dev/null 2>&1 &
+- npm run json-server &
 - sleep 10
 script:
 - gulp test
+- './node_modules/.bin/mocha test/rest'
 - './node_modules/.bin/mocha test/helper/${HELPER}_test.js'
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 12cb3c49..7b7f433d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,4 +1,4 @@
-## 0.6.3
+ ## 0.6.3
 
 * Errors are printed in non-verbose mode. Shows "Selenium not started" and other important errors.
 * Allowed to set custom test options:
diff --git a/RoboFile.php b/RoboFile.php
index 6084e980..94f0f168 100644
--- a/RoboFile.php
+++ b/RoboFile.php
@@ -34,6 +34,10 @@ class RoboFile extends \Robo\Tasks
 
     function testServer()
     {
+        $this->taskExec('npm run json-server')
+            ->background()
+            ->run();
+
         $this->taskServer(8000)
             ->dir('test/data/app')
             ->run();
diff --git a/docs/data.md b/docs/data.md
new file mode 100644
index 00000000..865e4c5b
--- /dev/null
+++ b/docs/data.md
@@ -0,0 +1,132 @@
+# Data Management
+
+Managing data for tests is always a tricky issue. How isolate data between tests, how to prepare data for different tests, etc.
+There are different approaches to solve it:
+
+1. reset database completely between tests
+2. create unique non-intersecting data sets per each test
+3. create and delete data for a test
+
+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.
+
+## REST
+
+REST helper uses [Unirest](http://unirest.io/nodejs.html) library to send HTTP requests to application.
+However, it doesn't provide tools for testing APIs, so it should be paired with WebDriverIO, Nightmare or Protractor helpers for browser testing.
+
+Enable REST helper in global config. It is recommended to set `endpoint`, a base URL for all API requests.
+If you need some authorization you can optionally set default headers too.
+
+See sample config.
+
+```js
+"helpers": {
+  "REST": {
+    "endpoint": "http://localhost/api/v1/",
+    "defaultHeaders": {
+      "Auth": "11111"
+    }
+  }
+
+  "WebDriverIO" : {
+    "browser": "chrome"
+  }
+}
+```
+
+REST helper provides basic methods to send requests to application:
+
+```js
+I.sendGetRequest()
+I.sendPostRequest()
+I.sendPutRequest()
+I.sendPatchRequest()
+I.sendDeleteRequest()
+```
+
+As well as a method for setting headers: `haveRequestHeaders`.
+
+Here is a usage example:
+
+```js
+// valid access token
+I.haveRequestHeaders({auth: '1111111'});
+// get the first user
+let user = yield I.sendGetRequest('/api/users/1');
+// create a post
+I.sendPostRequest('/api/posts', { author: user.id, body: 'some text' });
+// open browser page of new post
+I.amOnPage('/posts/2.html');
+```
+
+This can also be used to emulate Ajax requests:
+
+```js
+I.sendPostRequest('/update-status', {}, { http_x_requested_with: 'xmlhttprequest' });
+```
+
+## Data Generation with Factories
+
+This concept is extended by `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:
+
+```js
+let post = yield I.have('post');
+I.haveMultiple('comment', 5, { postId: post.id});
+```
+Just define how many items of any kind you need and ApiDataFactory will create them for you.
+However, to make this work some preperations 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.
+
+Install rosie and faker to create a first factory:
+
+```js
+npm i rosie faker --save-dev
+```
+
+Then create a module which will export a factory for an entity.
+
+See the example providing a factory for User generation:
+
+```js
+// factories/post.js
+var Factory = require('rosie').Factory;
+var faker = require('faker');
+
+module.exports = new Factory()
+  .attr('name', () => faker.name.findName())
+  .attr('email', () => faker.internet.email());
+```
+
+Next is to configure helper to match factories with API:
+
+```js
+ "ApiDataFactory": {
+   "endpoint": "http://user.com/api",
+   "factories": {
+     "user": {
+        "uri": "/users"
+        "factory": "./factories/user"
+     }
+   }
+ }
+```
+
+Then, calling `I.have('user')` inside a test will create a new post for you.
+This is done by sending POST request to `/api/users` URL. Response is returned and can be used in tests.
+
+At the end of a test ApiDataFactory will clean up created record for you. This is done by collecting
+ids from crated records and running `DELETE /api/users/{id}` requests at the end of a test.
+This rules can be customized in helper configuration.
+
+---
+
+### done()
+
diff --git a/lib/actor.js b/lib/actor.js
index 76be1965..6c119341 100644
--- a/lib/actor.js
+++ b/lib/actor.js
@@ -61,10 +61,17 @@ function recordStep(step, args) {
     val = step.run.apply(step, args);
   });
 
+  // ensure step result is passed to next step
+  recorder.add('return step result', () => val);
+
+  //return execution time for logging
+  recorder.add(`return execution time`, () => {
+    output.stepExecutionTime(step, global.suiteDetails);
+    return val;
+  });
+
   // run async after step hooks
   event.emit(event.step.after, step);
 
-  // ensure step result is passed to next step
-  recorder.add('return step result', () => val);
   return recorder.promise();
 }
diff --git a/lib/command/run-multiple.js b/lib/command/run-multiple.js
index ecb933dc..3f2f5c96 100644
--- a/lib/command/run-multiple.js
+++ b/lib/command/run-multiple.js
@@ -93,6 +93,7 @@ function runSuite(suite, suiteConf, browser) {
   // tweaking default output directories and for mochawesome
   overriddenConfig = replaceValue(overriddenConfig, 'output', path.join(config.output, outputDir));
   overriddenConfig = replaceValue(overriddenConfig, 'reportDir', path.join(config.output, outputDir));
+  overriddenConfig = replaceValue(overriddenConfig, 'mochaFile', path.join(config.output, outputDir, 'report.xml'));
 
   // override grep param and collect all params
   let params = ['run',
diff --git a/lib/event.js b/lib/event.js
index 1c1d5d60..0ac56569 100644
--- a/lib/event.js
+++ b/lib/event.js
@@ -11,6 +11,14 @@ module.exports = {
     passed: 'test.passed',
     failed: 'test.failed',
   },
+  hook: {
+    afterSuite: {
+      failed: 'afterSuiteHook.failed',
+    },
+    after: {
+      failed: 'afterTestHook.failed'
+    }
+  },
   suite: {
     before: 'suite.before',
     after: 'suite.after'
diff --git a/lib/helper.js b/lib/helper.js
index 7644e7a7..f5c06674 100644
--- a/lib/helper.js
+++ b/lib/helper.js
@@ -105,6 +105,15 @@ class Helper {
 
   }
 
+  /**
+   * Hook executed after all tests are executed
+   *
+   * @param suite
+   */
+  _finishTest() {
+
+  }
+
   /**
    * Access another configured helper: `this.helpers['AnotherHelper']`
    */
diff --git a/lib/helper/ApiDataFactory.js b/lib/helper/ApiDataFactory.js
new file mode 100644
index 00000000..d0e12233
--- /dev/null
+++ b/lib/helper/ApiDataFactory.js
@@ -0,0 +1,313 @@
+'use strict';
+let Helper = require('../helper');
+let REST = require('./REST');
+let fileExists = require('../utils').fileExists;
+let fileIncludes = require('../assert/include').fileIncludes;
+let fileEquals = require('../assert/equal').fileEquals;
+let assert = require('assert');
+let path = require('path');
+let fs = require('fs');
+let unirest = require('unirest');
+
+/**
+ * Helper for managing remote data using REST API.
+ * Uses data generators like [rosie](https://github.com/rosiejs/rosie) or factory girl to create new record.
+ *
+ * By defining a factory you set the rules of how data is generated.
+ * This data will be saved on server via REST API and deleted in the end of a test.
+ *
+ * ### Use Case
+ *
+ * Acceptance tests interact with a websites using UI and real browser.
+ * There is no way to create data for a specific test other than from user interface.
+ * That makes tests slow and fragile. Instead of testing a single feature you need to follow all creation/removal process.
+ *
+ * This helper solves this problem.
+ * Most of web application have API, and it can be used to create and delete test records.
+ * By combining REST API with Factories you can easily create records for tests:
+ *
+ * ```js
+ * I.have('user', { login: 'davert', email: 'davert@mail.com' });
+ * let id = yield I.have('post', { title: 'My first post'});
+ * I.haveMultiple('comment', 3, {post_id: id});
+ * ```
+ *
+ * To make this work you need
+ *
+ * 1. REST API endpoint which allows to perform create / delete requests and
+ * 2. define data generation rules
+ *
+ *
+ * ### Setup
+ *
+ * Install [Rosie](https://github.com/rosiejs/rosie) and [Faker](https://www.npmjs.com/package/faker) libraries.
+ *
+ * ```
+ * npm i rosie faker --save-dev`
+ * ```
+ *
+ * Create a factory file for a resource.
+ *
+ * See the example for Posts factories:
+ *
+ * ```js
+ * // tests/factories/posts.js
+ *
+ * var Factory = require('rosie').Factory;
+ * var faker = require('faker');
+ *
+ * module.exports = new Factory()
+ *    // no need to set id, it will be set by REST API
+ *    .attr('author', () => faker.name.findName())
+ *    .attr('title', () => faker.lorem.sentence())
+ *    .attr('body', () => faker.lorem.paragraph());
+ * ```
+ * For more options see [rosie documentation](https://github.com/rosiejs/rosie).
+ *
+ * Then configure ApiDataHelper to match factories and REST API:
+
+ * ### Configuration
+ *
+ * ApiDataFactory has following config options:
+ *
+ * * `endpoint`: base URL for the API to send requests to.
+ * * `cleanup`: should inserted records be deleted up after tests. Default: true
+ * * `factories`: list of defined factories
+ * * `REST`: configuration for REST requests
+ *
+ * See the example:
+ *
+ * ```js
+ *  "ApiDataFactory": {
+ *    "endpoint": "http://user.com/api",
+ *    "cleanup": true,
+ *    "factories": {
+ *      "post": {
+ *         "uri": "/posts"
+ *         "factory": "./factories/post"
+ *      },
+ *      "comment": {
+ *        "factory": "./factories/comment",
+ *        "create": { "post": "/comments/create" },
+ *        "delete": { "post": "/comments/delete" }
+ *      }
+ *    }
+ * }
+ * ```
+
+ * It is required to set REST API `endpoint` which is the baseUrl for all API requests.
+ * Factories are listed, factory file is expected to be passed via `factory` option.
+ *
+ * This Helper uses [REST](http://codecept.io/helpers/REST/) helper and accepts its configuration in "REST" section.
+ * So, in order to set default headers or timeout you should add:
+ *
+ * ```js
+ * "ApiDataFactory": {
+ *    "REST": {
+ *      "timeout": "100000",
+ *      "defaultHeaders": {
+ *        "auth": "111111"
+ *      }
+ *   }
+ * }
+ * ```
+ *
+ * ### Api Requests
+ *
+ * By default to create a record ApiDataFactory will use endpoint and plural factory name:
+ *
+ * * create: `POST {endpoint}/{resource} data`
+ * * delete: `DELETE {endpoint}/{resource}/id`
+ *
+ * Example (`endpoint`: `http://app.com/api`):
+ *
+ * * create: POST request to `http://app.com/api/users`
+ * * delete: DELETE request to `http://app.com/api/users/1`
+ *
+ * However this behavior can be configured with following options:
+ *
+ * * `uri`: set different resource uri. Example: `uri: account` => `http://app.com/api/account`.
+ * * `create`: override create options. Expected format: `{ method: uri }`. Example: `{ "post": "/users/create" }`
+ * * `delete`: override delete options. Expected format: `{ method: uri }`. Example: `{ "post": "/users/delete/{id}" }`
+ *
+ */
+class ApiDataFactory extends Helper {
+
+  constructor(config) {
+    super(config);
+
+    this.config = Object.assign({ cleanup: true, REST: {}, factories: {} }, this.config);
+
+    this.restHelper = new REST(Object.assign(this.config.REST, { endpoint: this.config.endpoint }));
+    this.factories = this.config.factories;
+
+    for (let factory in this.factories) {
+      let factoryConfig = this.factories[factory];
+      if (!factoryConfig.uri && !factoryConfig.create) {
+        throw new Error(
+`Uri for factory "${factory}" is not defined. Please set "uri" parameter:
+
+    "factories": {
+      "${factory}": {
+        "uri": ...
+`);
+      }
+
+      if (!factoryConfig.create) factoryConfig.create = { post: factoryConfig.uri };
+      if (!factoryConfig.delete) factoryConfig.delete = { delete: factoryConfig.uri + '/{id}' };
+
+      this.factories[factory] = factoryConfig;
+    }
+
+    this.created = {};
+    Object.keys(this.factories).forEach((f) => this.created[f] = []);
+  }
+
+  static _checkRequirements() {
+    try {
+      requireg("unirest");
+      requireg("rosie");
+    } catch(e) {
+      return ["unirest", "rosie"];
+    }
+  }
+
+  _after() {
+    if (!this.config.cleanup) {
+      return Promise.resolve();
+    }
+    let promises = [];
+
+    // clean up all created items
+    for(let factoryName in this.created) {
+      this.debug(`Deleting ${this.created[factoryName].length} ${factoryName}(s)`);
+      this.created[factoryName].forEach((id) => promises.push(this._requestDelete(factoryName, id)));
+    }
+
+    return Promise.all(promises);
+  }
+
+  /**
+   * Generates a new record using factory and saves API request to store it.
+   *
+   * ```js
+   * // create a user
+   * I.have('user');
+   * // create user with defined email
+   * I.have('user', { email: 'user@user.com'});
+   * ```
+   *
+   * @param {*} factory factory to use
+   * @param {*} params predefined parameters
+   */
+  have(factory, params) {
+    let item = this._createItem(factory, params);
+    this.debug(`Creating ${factory} ` + JSON.stringify(item));
+    return this._requestCreate(factory, item);
+  }
+
+  /**
+   * Generates bunch of records and saves multiple API requests to store them.
+   *
+   * ```js
+   * // create 3 posts
+   * I.have('post', 3);
+   *
+   * // create 3 posts by one author
+   * I.have('post', 3, { author: 'davert' });
+   * ```
+   *
+   * @param {*} factory
+   * @param {*} times
+   * @param {*} params
+   */
+  haveMultiple(factory, times, params) {
+    let promises = [];
+    for (let i = 0; i < times; i++) {
+      promises.push(this.have(factory, params));
+    }
+    return Promise.all(promises);
+  }
+
+  _createItem(model, data) {
+    try {
+      let path = this.factories[model].factory;
+      let builder = require(path);
+      return builder.build(data);
+    } catch (err) {
+      throw new Error(
+`Couldn't load factory file from ${path}, check that
+
+  "factories": {
+    "${model}": {
+      "factory": "./path/to/factory"
+
+points to valid factory file.
+Factory file should export an object with build method.
+
+Current file error: ` + err.message);
+    }
+  }
+
+
+  /**
+   * Fetches id of a record after it was created by `have`.
+   * By default fetched `id` attribute from JSON body.
+   *
+   * Customize it from custom Helper file:
+   *
+   * ```js
+   * this.helpers['ApiDataFactory']._fetchId = (body, factory) {
+   *    return body[factory][0].id;
+   * }
+   * ```
+   *
+   * @param {*} body
+   * @param {*} factory
+   */
+  _fetchId(body, factory) {
+    if (body.id) return body.id;
+    if (body[factory] && body[factory].id) return body[factory].id;
+    return null;
+  }
+
+  /**
+   * Executes request to create a record in API.
+   * Can be replaced from a in custom helper.
+   *
+   * @param {*} factory
+   * @param {*} data
+   */
+  _requestCreate(factory, data) {
+    let method = Object.keys(this.factories[factory].create)[0];
+    let url = this.factories[factory].create[method];
+    let request = unirest[method](this.restHelper._url(url)).type('json').send(data);
+
+    return this.restHelper._executeRequest(request).then((resp) => {
+      let id = this._fetchId(resp.body, factory);
+      this.created[factory].push(id);
+      return resp.body;
+    });
+  }
+
+  /**
+   * Executes request to delete a record in API
+   * Can be replaced from a custom helper.
+   *
+   * @param {*} factory
+   * @param {*} id
+   */
+  _requestDelete(factory, id) {
+    let method = Object.keys(this.factories[factory].delete)[0];
+    let url = this.factories[factory].delete[method].replace('{id}', id);
+
+    url = this.restHelper._url(url);
+
+    return this.restHelper._executeRequest(unirest(method, url)).then(() => {
+      let idx = this.created[factory].indexOf(id);
+      this.created[factory].splice(idx, 1);
+    });
+  }
+}
+
+module.exports = ApiDataFactory;
diff --git a/lib/helper/Appium.js b/lib/helper/Appium.js
index 8d416216..73c2025a 100644
--- a/lib/helper/Appium.js
+++ b/lib/helper/Appium.js
@@ -51,7 +51,7 @@ let browserCap;
  * ```js
  * {
  *   helpers: {
- *       WebDriverIO: {
+ *       Appium: {
  *           desiredCapabilities: {
  *               platformName: "Android",
  *               appPackage: "com.example.android.myApp",
@@ -93,7 +93,7 @@ class Appium extends WebdriverIO {
       restart: true,
       manualStart: false,
       timeouts: {
-        script: 1000 // ms
+        script: 0 // ms
       }
     };
 
@@ -133,16 +133,6 @@ class Appium extends WebdriverIO {
     return this.browser;
   }
 
-  _after() {
-    if (this.options.restart) return this.browser.end();
-    if (this.options.desiredCapabilities.browserName) {
-      this.debugSection('Session', 'cleaning cookies and localStorage');
-      return this.browser.execute('localStorage.clear();').then(() => {
-        return this.browser.deleteCookie();
-      });
-    }
-  }
-
   /**
    * Check if an app is installed.
    *
diff --git a/lib/helper/Nightmare.js b/lib/helper/Nightmare.js
index ea87aadf..73669bb1 100644
--- a/lib/helper/Nightmare.js
+++ b/lib/helper/Nightmare.js
@@ -55,6 +55,8 @@ class Nightmare extends Helper {
       js_errors: null
     };
 
+    this.isRunning = false;
+
     // override defaults with config
     Object.assign(this.options, config);
 
@@ -172,7 +174,8 @@ class Nightmare extends Helper {
   }
 
   _beforeSuite() {
-    if (!this.options.restart) {
+    if (!this.options.restart && !this.isRunning) {
+      this.isRunning = true;
       return this._startBrowser();
     }
   }
@@ -189,10 +192,13 @@ class Nightmare extends Helper {
       return this._stopBrowser();
     }
     if (this.options.keepCookies) return;
-    return this.browser.cookies.clearAll();
+    return Promise.all([this.browser.cookies.clearAll(), this.executeScript('localStorage.clear();')]);
   }
 
   _afterSuite() {
+  }
+
+  _finishTest() {
     if (!this.options.restart) {
       this._stopBrowser();
     }
@@ -822,9 +828,9 @@ class Nightmare extends Helper {
     }, lctype(locator), lcval(locator));
   }
 
-    /**
-     * {{> ../webapi/saveScreenshot }}
-     */
+  /**
+   * {{> ../webapi/saveScreenshot }}
+   */
   saveScreenshot(fileName, fullPage = this.options.fullPageScreenshots) {
     let outputFile = path.join(global.output_dir, fileName);
     this.debug('Screenshot is saving to ' + outputFile);
@@ -837,9 +843,9 @@ class Nightmare extends Helper {
       height: document.body.scrollHeight,
       width: document.body.scrollWidth
     })).then(({
-        width,
-        height
-      }) => {
+                width,
+                height
+              }) => {
       this.browser.viewport(width, height);
       return this.browser.screenshot(outputFile);
     });
@@ -907,7 +913,7 @@ function *proceedSeeInField(assertType, field, value) {
   let fieldVal = yield this.browser.evaluate(function (el) {
     return codeceptjs.fetchElement(el).value;
   }
-  , el);
+    , el);
   if (tag == 'select') {
     // locate option by values and check them
     let text = yield this.browser.evaluate(function (el, val) {
diff --git a/lib/helper/REST.js b/lib/helper/REST.js
index 4efeedd1..723e394b 100644
--- a/lib/helper/REST.js
+++ b/lib/helper/REST.js
@@ -7,6 +7,18 @@ let headers = '';
 let payload = '';
 let request;
 
+/**
+ * REST helper allows to send additional requests to the REST API during acceptance tests.
+ * [Unirest](http://unirest.io/nodejs.html) library is used to perform requests.
+ *
+ * ## Configuration
+ *
+ * * endpoint: API base URL
+ * * timeout: timeout for requests in milliseconds. 10000 by default
+ * * defaultHeaders: a list of default headers
+ * * resetHeaders: set to true to reset headers  between requests. Disabled by default
+ *
+ */
 class REST extends Helper {
 
   constructor(config) {
@@ -15,10 +27,12 @@ class REST extends Helper {
 
     this.options = {
       timeout: 10000,
-      reset_headers: false,
+      resetHeaders: false,
+      defaultHeaders: {},
       endpoint: ''
     };
-    Object.assign(this.options, config);
+    this.options = Object.assign(this.options, config);
+    headers = this.options.defaultHeaders;
   }
 
   static _checkRequirements() {
@@ -29,36 +43,122 @@ class REST extends Helper {
     }
   }
 
-  setRestTimeout(newTimeout) {
+  setRequestTimeout(newTimeout) {
     this.options.timeout = newTimeout;
   }
 
+  /**
+   * Set headers for the request
+   *
+   * ```js
+   * I.haveRequestHeaders({
+   *    'Accept': 'application/json',
+   *    'User-Agent': 'Unirest Node.js'
+   * });
+   * ```
+   *
+   * @param {*} customHeaders
+   */
   haveRequestHeaders(customHeaders) {
     if (!customHeaders) {
       throw new Error('Cannot send empty headers.');
     }
-    headers = customHeaders;
+    headers = Object.assign(headers, customHeaders);
   }
 
   _cleanRequestHeaders() {
-    if (this.options.reset_headers) {
-      this.headers = {};
+    if (this.options.resetHeaders) {
+      headers = this.options.defaultHeaders;
     }
   }
+
   _url(url) {
     return this.options.endpoint + url;
   }
 
-  sendGet(url) {
-    request = unirest.get(this._url(url));
+  /**
+   * Send GET request to REST API
+   *
+   * ```js
+   * I.sendGetRequest('/api/users.json');
+   * ```
+   *
+   * @param {*} url
+   * @param object headers
+   */
+  sendGetRequest(url, headers = {}) {
+    request = unirest.get(this._url(url)).headers(headers);
+    return this._executeRequest(request);
+  }
+
+  /**
+   * Sends POST request to API.
+   *
+   * ```js
+   * I.sendPostRequest('/api/users.json', { "email": "user@user.com" });
+   * ```
+   *
+   * @param {*} url
+   * @param {*} payload
+   * @param object headers
+   */
+  sendPostRequest(url, payload = {}, headers = {}) {
+    request = unirest.post(this._url(url)).send(payload).headers(headers);
     return this._executeRequest(request);
   }
 
-  sendPost(url, payload = {}) {
-    request = unirest.post(this._url(url), payload);
+  /**
+   * Sends PATCH request to API.
+   *
+   * ```js
+   * I.sendPatchRequest('/api/users.json', { "email": "user@user.com" });
+   * ```
+   *
+   * @param string url
+   * @param object payload
+   * @param object headers
+   */
+  sendPatchRequest(url, payload = {}, headers = {}) {
+    request = unirest.patch(this._url(url)).send(payload).headers(headers);
     return this._executeRequest(request);
   }
 
+  /**
+   * Sends PUT request to API.
+   *
+   * ```js
+   * I.sendPutRequest('/api/users.json', { "email": "user@user.com" });
+   * ```
+   *
+   * @param string url
+   * @param object payload
+   * @param object headers
+   */
+  sendPutRequest(url, payload = {}, headers = {}) {
+    request = unirest.put(this._url(url)).send(payload).headers(headers);
+    return this._executeRequest(request);
+  }
+
+  /**
+   * Sends DELETE request to API.
+   *
+   * ```js
+   * I.sendDeleteRequest('/api/users/1');
+   * ```
+   *
+   * @param {*} url
+   * @param {*} payload
+   */
+  sendDeleteRequest(url, headers = {}) {
+    request = unirest.delete(this._url(url)).headers(headers);
+    return this._executeRequest(request);
+  }
+
+  /**
+   * Executes unirest request
+   *
+   * @param {*} request
+   */
   _executeRequest(request) {
     if (headers && 'auth' in headers) {
       request.auth(headers.auth);
@@ -71,13 +171,13 @@ class REST extends Helper {
     });
   }
 }
+
 module.exports = REST;
 
 function* executeRequest(request) {
   return new Promise(function (resolve, reject) {
     request
       .headers(headers)
-      .send(payload)
       .end(function (response) {
         return resolve(response) || reject(response);
       });
diff --git a/lib/helper/SeleniumWebdriver.js b/lib/helper/SeleniumWebdriver.js
index cdacf5d0..f3c105c9 100644
--- a/lib/helper/SeleniumWebdriver.js
+++ b/lib/helper/SeleniumWebdriver.js
@@ -78,6 +78,9 @@ class SeleniumWebdriver extends Helper {
       manualStart: false,
       capabilities: {}
     };
+
+    this.isRunning = false;
+
     if (this.options.waitforTimeout) {
       console.log(`waitforTimeout is deprecated in favor of waitForTimeout, please update config`);
       this.options.waitForTimeout = this.options.waitforTimeout;
@@ -132,8 +135,9 @@ class SeleniumWebdriver extends Helper {
   }
 
   _beforeSuite() {
-    if (!this.options.restart && !this.options.manualStart) {
+    if (!this.options.restart && !this.options.manualStart && !this.isRunning) {
       this.debugSection('Session', 'Starting singleton browser session');
+      this.isRunning = true;
       return this._startBrowser();
     }
   }
@@ -146,13 +150,16 @@ class SeleniumWebdriver extends Helper {
 
   _after() {
     if (this.options.restart) return this.browser.quit();
-    if (this.options.keepCookies) return;
+    if (this.options.keepCookies) return Promise.all([this.browser.executeScript('localStorage.clear();'), this.closeOtherTabs()]);
     // if browser should not be restarted
     this.debugSection('Session', 'cleaning cookies and localStorage');
-    return Promise.all([this.browser.manage().deleteAllCookies(), this.browser.executeScript('localStorage.clear();')]);
+    return Promise.all([this.browser.manage().deleteAllCookies(), this.browser.executeScript('localStorage.clear();'), this.closeOtherTabs()]);
   }
 
   _afterSuite() {
+  }
+
+  _finishTest() {
     if (!this.options.restart) return this.browser.quit();
   }
 
@@ -667,6 +674,27 @@ class SeleniumWebdriver extends Helper {
     return this.browser.manage().window().setSize(width, height);
   }
 
+  /**
+   * Close all tabs expect for one.
+   *
+   * ```js
+   * I.closeOtherTabs();
+   * ```
+   */
+  closeOtherTabs() {
+    let client = this.browser;
+
+    return client.getAllWindowHandles().then(function (handles){
+      for (var i = 1; i < handles.length; i++) {
+        client.switchTo().window(handles[i]).then(function (){
+          client.close();
+        });
+      }
+      return client.switchTo().window(handles[0]);
+    });
+  }
+
+
   /**
    * {{> ../webapi/wait }}
    */
diff --git a/lib/helper/WebDriverIO.js b/lib/helper/WebDriverIO.js
index 26ef1f81..d0f7265a 100644
--- a/lib/helper/WebDriverIO.js
+++ b/lib/helper/WebDriverIO.js
@@ -210,6 +210,8 @@ class WebDriverIO extends Helper {
       }
     };
 
+    this.isRunning = false;
+
     // override defaults with config
     Object.assign(this.options, config);
 
@@ -256,8 +258,9 @@ class WebDriverIO extends Helper {
   }
 
   _beforeSuite() {
-    if (!this.options.restart && !this.options.manualStart) {
+    if (!this.options.restart && !this.options.manualStart && !this.isRunning) {
       this.debugSection('Session', 'Starting singleton browser session');
+      this.isRunning = true;
       return this._startBrowser();
     }
   }
@@ -295,12 +298,17 @@ class WebDriverIO extends Helper {
 
   _after() {
     if (this.options.restart) return this.browser.end();
-    if (this.options.keepCookies) return;
-    this.debugSection('Session', 'cleaning cookies and localStorage');
-    return Promise.all([this.browser.deleteCookie(), this.browser.execute('localStorage.clear();')]);
+    if (this.options.keepCookies) return Promise.all([this.browser.execute('localStorage.clear();'), this.closeOtherTabs()]);
+    if (this.options.desiredCapabilities.browserName) {
+      this.debugSection('Session', 'cleaning cookies and localStorage');
+      return Promise.all([this.browser.deleteCookie(), this.browser.execute('localStorage.clear();'), this.closeOtherTabs()]);
+    }
   }
 
   _afterSuite() {
+  }
+
+  _finishTest() {
     if (!this.options.restart) return this.browser.end();
   }
 
@@ -821,12 +829,12 @@ class WebDriverIO extends Helper {
    * I.seeNumberOfVisibleElements('.buttons', 3);
    * ```
    */
-  seeNumberOfVisibleElements(selector, num) {
-    return this.browser.isVisible(withStrictLocator(selector))
+  seeNumberOfVisibleElements(locator, num) {
+    return this.browser.isVisible(withStrictLocator(locator))
       .then(function (res) {
         if (!Array.isArray(res)) res = [res];
         res = res.filter((val) => val == true);
-        return truth(`elements of ${locator}`, 'to be seen').assert.equal(res.length, num);
+        return assert.equal(res.length, num);
       });
   }
 
@@ -922,10 +930,10 @@ class WebDriverIO extends Helper {
       return this.browser.saveScreenshot(outputFile);
     }
     return this.browser.execute(function () {
-      return ({
+      return {
         height: document.body.scrollHeight,
         width: document.body.scrollWidth
-      })
+      };
     }).then(({
       width,
       height
@@ -1088,6 +1096,24 @@ class WebDriverIO extends Helper {
     );
   }
 
+  /**
+   * Close all tabs expect for one.
+   * Appium: support web test
+   *
+   * ```js
+   * I.closeOtherTabs();
+   * ```
+   */
+  closeOtherTabs() {
+    let client = this.browser;
+    return client.getTabIds().then(function (handles) {
+      for (var i = 1; i < handles.length; i++) {
+        this.close(handles[i]);
+      }
+      return this.switchTab();
+    });
+  }
+
   /**
    * {{> ../webapi/wait }}
    * Appium: support
@@ -1122,13 +1148,13 @@ class WebDriverIO extends Helper {
     sec = sec || this.options.waitForTimeout;
     context = context || 'body';
     return this.browser.waitUntil(function () {
-        return this.getText(withStrictLocator(context)).then(function (source) {
-          if (Array.isArray(source)) {
-            return source.filter(part => part.indexOf(text) >= 0).length > 0;
-          }
-          return source.indexOf(text) >= 0;
-        });
-      }, sec * 1000)
+      return this.getText(withStrictLocator(context)).then(function (source) {
+        if (Array.isArray(source)) {
+          return source.filter(part => part.indexOf(text) >= 0).length > 0;
+        }
+        return source.indexOf(text) >= 0;
+      });
+    }, sec * 1000)
       .catch((e) => {
         if (e.type === 'WaitUntilTimeoutError') {
           return proceedSee.call(this, 'assert', text, withStrictLocator(context));
@@ -1363,15 +1389,15 @@ function withStrictLocator(locator) {
   locator.toString = () => `{${key}: '${value}'}`;
 
   switch (key) {
-    case 'by':
-    case 'xpath':
-      return value;
-    case 'css':
-      return value;
-    case 'id':
-      return '#' + value;
-    case 'name':
-      return `[name="${value}"]`;
+  case 'by':
+  case 'xpath':
+    return value;
+  case 'css':
+    return value;
+  case 'id':
+    return '#' + value;
+  case 'name':
+    return `[name="${value}"]`;
   }
 }
 
diff --git a/lib/interfaces/bdd.js b/lib/interfaces/bdd.js
index 99cabd8a..3a8e8fa3 100644
--- a/lib/interfaces/bdd.js
+++ b/lib/interfaces/bdd.js
@@ -24,6 +24,7 @@ var escapeRe = require('escape-string-regexp');
 module.exports = function (suite) {
   var suites = [suite];
   suite.timeout(0);
+  var afterAllHooks, afterEachHooks, afterAllHooksAreLoaded, afterEachHooksAreLoaded;
 
   suite.on('pre-require', function (context, file, mocha) {
     var common = require('mocha/lib/interfaces/common')(suites, context);
@@ -45,6 +46,12 @@ module.exports = function (suite) {
         suites.shift();
       }
       if (!opts) opts = {};
+
+      afterAllHooks = [];
+      afterEachHooks = [];
+      afterAllHooksAreLoaded = false;
+      afterEachHooksAreLoaded = false;
+
       var suite = Suite.create(suites[0], title);
       suite.timeout(0);
 
@@ -54,28 +61,28 @@ module.exports = function (suite) {
       suite.file = file;
       suites.unshift(suite);
       suite.beforeEach('codeceptjs.before', scenario.setup);
-      suite.afterEach('finialize codeceptjs', scenario.teardown);
+      afterEachHooks.push(['finialize codeceptjs', scenario.teardown]);
 
       suite.beforeAll('codeceptjs.beforeSuite', () => scenario.suiteSetup(suite));
-      suite.afterAll('codeceptjs.afterSuite', () => scenario.suiteTeardown(suite));
+      afterAllHooks.push(['codeceptjs.afterSuite', () => scenario.suiteTeardown(suite)]);
 
       return suite;
     };
 
     context.BeforeSuite = function (fn) {
-      suites[0].beforeAll('BeforeSuite', scenario.injected(fn, suites[0]));
+      suites[0].beforeAll('BeforeSuite', scenario.injected(fn, suites[0], 'beforeSuite'));
     };
 
     context.AfterSuite = function (fn) {
-      suites[0].afterAll('AfterSuite', scenario.injected(fn, suites[0]));
+      afterAllHooks.unshift(['AfterSuite', scenario.injected(fn, suites[0], 'afterSuite')]);
     };
 
     context.Background = context.Before = function (fn) {
-      suites[0].beforeEach('Before', scenario.injected(fn, suites[0]));
+      suites[0].beforeEach('Before', scenario.injected(fn, suites[0], 'before'));
     };
 
     context.After = function (fn) {
-      suites[0].afterEach('After', scenario.injected(fn, suites[0]));
+      afterEachHooks.unshift(['After', scenario.injected(fn, suites[0]), 'after']);
     };
 
     /**
@@ -88,7 +95,22 @@ module.exports = function (suite) {
         fn = opts;
         opts = {};
       }
-
+      /**
+       * load hooks from arrays to suite to prevent reordering
+       *
+       */
+      if (!afterAllHooksAreLoaded) {
+        afterAllHooks.forEach(function (hook) {
+          suites[0].afterAll(hook[0], hook[1]);
+        });
+        afterAllHooksAreLoaded = true;
+      }
+      if (!afterEachHooksAreLoaded) {
+        afterEachHooks.forEach(function (hook) {
+          suites[0].afterEach(hook[0], hook[1]);
+        });
+        afterEachHooksAreLoaded = true;
+      }
       var suite = suites[0];
       if (suite.pending) {
         fn = null;
diff --git a/lib/listener/helpers.js b/lib/listener/helpers.js
index 6f361956..a8820bfd 100644
--- a/lib/listener/helpers.js
+++ b/lib/listener/helpers.js
@@ -36,6 +36,14 @@ module.exports = function () {
     runAsyncHelpersHook('_afterSuite', suite, true);
   });
 
+  event.dispatcher.on(event.hook.after.failed, function (suite) {
+    runAsyncHelpersHook('_after', suite, true);
+  });
+
+  event.dispatcher.on(event.hook.afterSuite.failed, function (suite) {
+    runAsyncHelpersHook('_afterSuite', suite, true);
+  });
+
   event.dispatcher.on(event.test.started, function (test) {
     runHelpersHook('_test', test);
     recorder.catch((e) => error(e));
@@ -48,7 +56,7 @@ module.exports = function () {
   event.dispatcher.on(event.test.failed, function (test) {
     runAsyncHelpersHook('_failed', test, true);
     // should not fail test execution, so errors should be catched
-    recorder.catch((e) => error(e));
+    recorder.catchWithoutStop((e) => error(e));
   });
 
   event.dispatcher.on(event.test.after, function () {
@@ -62,4 +70,8 @@ module.exports = function () {
   event.dispatcher.on(event.step.after, function (step) {
     runAsyncHelpersHook('_afterStep', step);
   });
+
+  event.dispatcher.on(event.all.result, function () {
+    runAsyncHelpersHook('_finishTest', {}, true);
+  });
 };
diff --git a/lib/listener/steps.js b/lib/listener/steps.js
index d73ff169..8aa53d18 100644
--- a/lib/listener/steps.js
+++ b/lib/listener/steps.js
@@ -1,5 +1,7 @@
 'use strict';
 const event = require('../event');
+const recorder = require('../recorder');
+const output = require('../output');
 
 let currentTest;
 let steps;
diff --git a/lib/output.js b/lib/output.js
index fc7aa2d5..58f87c9f 100644
--- a/lib/output.js
+++ b/lib/output.js
@@ -14,6 +14,7 @@ let styles = {
 
 let outputLevel = 0;
 let outputProcess = '';
+let newline = true;
 
 module.exports = {
   colors,
@@ -73,9 +74,24 @@ module.exports = {
     if (outputLevel === 0) return;
     if (!step) return;
     let sym = process.platform === 'win32' ? '*' : '•';
+    if (outputLevel >= 2) {
+      newline = false;
+      return process.stdout.write(`${' '.repeat(this.stepShift)} ${sym} ${step.toString()}`);
+    }
     print(' '.repeat(this.stepShift), `${sym} ${step.toString()}`);
   },
 
+  /**
+   * Print a step execution time
+   */
+  stepExecutionTime: function (step) {
+    if (outputLevel < 2) return;
+    if (!step) return;
+    step.endTime = new Date();
+    newline = true;
+    process.stdout.write(` ${styles.debug(" (" + (step.endTime - step.startTime) / 1000 + " sec)\n")}`);
+  },
+
   suite: {
     started: (suite) => {
       if (!suite.title) return;
@@ -140,6 +156,10 @@ function print(...msg) {
   if (outputProcess) {
     msg.unshift(outputProcess);
   }
+  if (!newline) {
+    console.log();
+    newline = true;
+  }
   console.log.apply(this, msg);
 }
 
diff --git a/lib/recorder.js b/lib/recorder.js
index d6d976d6..2a69345d 100644
--- a/lib/recorder.js
+++ b/lib/recorder.js
@@ -26,6 +26,16 @@ module.exports = {
     this.reset();
   },
 
+  isRunning() {
+    return running;
+  },
+
+  startUnlessRunning() {
+    if (!this.isRunning()) {
+      this.start();
+    }
+  },
+
   /**
    * Add error handler to catch rejected promises
    *
@@ -115,6 +125,20 @@ module.exports = {
     });
   },
 
+  catchWithoutStop(customErrFn) {
+    return promise = promise.catch((err) => {
+      log(currentQueue() + `Error | ${err}`);
+      if (!(err instanceof Error)) { // strange things may happen
+        err = new Error('[Wrapped Error] ' + JSON.stringify(err)); // we should be prepared for them
+      }
+      if (customErrFn) {
+        customErrFn(err);
+      } else if (errFn) {
+        errFn(err);
+      }
+    });
+  },
+
   /**
    * Adds a promise which throws an error into a chain
    *
@@ -167,4 +191,3 @@ function currentQueue() {
   if (sessionId) session = `<${sessionId}> `;
   return `[${queueId}] ${session}`;
 }
-
diff --git a/lib/scenario.js b/lib/scenario.js
index 6bdbaca2..856cebf3 100644
--- a/lib/scenario.js
+++ b/lib/scenario.js
@@ -71,15 +71,19 @@ module.exports.test = (test) => {
 /**
  * Injects arguments to function from controller
  */
-module.exports.injected = function (fn, suite) {
+module.exports.injected = function (fn, suite, hookName) {
   return function () {
     try {
+      recorder.startUnlessRunning();
       fn.apply(this, getInjectedArguments(fn));
     } catch (err) {
       recorder.throw(err);
     }
     recorder.catch((err) => {
       event.emit(event.test.failed, suite, err); // emit
+      if (hookName === 'after' || hookName === 'afterSuite') {
+        event.emit(event.hook[hookName].failed, suite);
+      }
       throw err;
     });
     return recorder.promise();
@@ -90,21 +94,22 @@ module.exports.injected = function (fn, suite) {
  * Starts promise chain, so helpers could enqueue their hooks
  */
 module.exports.setup = function () {
-  recorder.start();
+  recorder.startUnlessRunning();
   event.emit(event.test.before);
 };
 
 module.exports.teardown = function () {
+  recorder.startUnlessRunning();
   event.emit(event.test.after);
 };
 
 module.exports.suiteSetup = function (suite) {
-  recorder.start();
+  recorder.startUnlessRunning();
   event.emit(event.suite.before, suite);
 };
 
 module.exports.suiteTeardown = function (suite) {
-  recorder.start();
+  recorder.startUnlessRunning();
   event.emit(event.suite.after, suite);
 };
 
diff --git a/lib/step.js b/lib/step.js
index 055a3da9..4c1135da 100644
--- a/lib/step.js
+++ b/lib/step.js
@@ -22,6 +22,7 @@ class Step {
   }
 
   run() {
+    this.startTime = new Date();
     this.args = Array.prototype.slice.call(arguments);
     let result;
     try {
diff --git a/package.json b/package.json
index 81d1f709..45ab1f64 100644
--- a/package.json
+++ b/package.json
@@ -43,6 +43,7 @@
     "chai-as-promised": "^5.2.0",
     "co-mocha": "^1.1.2",
     "documentation": "^4.0.0-beta1",
+    "faker": "^4.1.0",
     "git-guppy": "^1.0.1",
     "gulp": "^3.6.0",
     "gulp-coveralls": "^0.1.0",
@@ -56,18 +57,20 @@
     "gulp-mustache": "^2.2.0",
     "gulp-plumber": "^1.0.0",
     "guppy-pre-commit": "^0.4.0",
+    "json-server": "^0.10.1",
     "nightmare": "^2.5.2",
     "nightmare-upload": "^0.1.1",
     "protractor": ">4.0.9 <6.0",
+    "rosie": "^1.6.0",
     "selenium-webdriver": "^2.53.1",
     "sinon": "^1.17.2",
     "unirest": "^0.5.1",
     "webdriverio": ">3.4.0 <5.0.0"
-
   },
   "scripts": {
     "prepublish": "gulp prepublish",
-    "test": "gulp"
+    "test": "gulp",
+    "json-server": "./node_modules/json-server/bin/index.js test/data/rest/db.json -p 8010 --watch -m test/data/rest/headers.js"
   },
   "license": "MIT"
 }


Test patch:
diff --git a/test/data/rest/db.json b/test/data/rest/db.json
new file mode 100644
index 00000000..838c80ee
--- /dev/null
+++ b/test/data/rest/db.json
@@ -0,0 +1,1 @@
+{"comments":[],"posts":[{"id":1,"title":"json-server","author":"davert"}]}
\ No newline at end of file
diff --git a/test/data/rest/headers.js b/test/data/rest/headers.js
new file mode 100644
index 00000000..dbbf2637
--- /dev/null
+++ b/test/data/rest/headers.js
@@ -0,0 +1,4 @@
+module.exports = (req, res, next) => {
+  if (req.path != '/headers') return next();
+  res.json(req.headers);
+}
\ No newline at end of file
diff --git a/test/data/rest/posts_factory.js b/test/data/rest/posts_factory.js
new file mode 100644
index 00000000..5d7bd81b
--- /dev/null
+++ b/test/data/rest/posts_factory.js
@@ -0,0 +1,7 @@
+var Factory = require('rosie').Factory;
+var faker = require('faker');
+
+module.exports = new Factory()
+  .attr('author', () => faker.name.findName())
+  .attr('title', () => faker.lorem.sentence())
+  .attr('body', () => faker.lorem.paragraph());
\ No newline at end of file
diff --git a/test/helper/Appium_test.js b/test/helper/Appium_test.js
index a9979954..c5210f27 100644
--- a/test/helper/Appium_test.js
+++ b/test/helper/Appium_test.js
@@ -36,6 +36,7 @@ describe('Appium', function () {
       },
       host: 'ondemand.saucelabs.com',
       port: 80,
+      // port: 4723,
       user: process.env.SAUCE_USERNAME,
       key: process.env.SAUCE_ACCESS_KEY,
     });
@@ -367,6 +368,17 @@ describe('Appium', function () {
       return app.see('EN Button', '~buttonTestCD')
         .then(() => app.dontSee('Welcome', '~buttonTestCD'));
     });
+
+    it('should work with webview', function*() {
+      yield app.click('~buttonStartWebviewCD');
+      return app.see('Hello', 'android.webkit.WebView');
+    });
+
+    it.only('should work inside web view as normally', function*() {
+      yield app.click('~buttonStartWebviewCD');
+      yield app.switchToContext('WEBVIEW_io.selendroid.testapp');
+      return app.see('Preferred car');
+    })
   });
 
   describe('#pressKey', () => {
@@ -379,7 +391,7 @@ describe('Appium', function () {
         100, 700);
       yield app.click('android.widget.Button');
       return app.see('1',
-        '//android.widget.TextView[@resource-id="io.selendroid.testapp:id/label_email_data"]');
+        '#io.selendroid.testapp:id/label_email_data');
     });
   });
 
diff --git a/test/rest/ApiDataFactory_test.js b/test/rest/ApiDataFactory_test.js
new file mode 100644
index 00000000..98d373ab
--- /dev/null
+++ b/test/rest/ApiDataFactory_test.js
@@ -0,0 +1,157 @@
+'use strict';
+let ApiDataFactory = require('../../lib/helper/ApiDataFactory');
+let should = require('chai').should();
+let api_url = 'http://127.0.0.1:8010';
+let assert = require('assert');
+let path = require('path');
+let fs = require('fs');
+let fileExists = require('../../lib/utils').fileExists;
+let AssertionFailedError = require('../../lib/assert/error');
+let expectError = require('../../lib/utils').test.expectError;
+let I;
+let dbFile = path.join(__dirname, '/../data/rest/db.json');
+require('co-mocha')(require('mocha'));
+
+const data = {
+  "comments": [],
+  "posts": [
+    {
+      "id": 1,
+      "title": "json-server",
+      "author": "davert"
+    }
+  ],
+}
+
+let getDataFromFile = () => JSON.parse(fs.readFileSync(dbFile));
+
+describe('ApiDataFactory', function () {
+
+  before(function() {
+    I = new ApiDataFactory({
+      endpoint: api_url,
+      factories: {
+        post: {
+          factory: path.join(__dirname, '/../data/rest/posts_factory.js'),
+          uri: "/posts"
+
+        }
+      }
+    });
+  });
+
+  beforeEach((done) => {
+    try {
+      fs.writeFileSync(dbFile, JSON.stringify(data));
+    } catch (err) {}
+    setTimeout(done, 500);
+  });
+
+  afterEach(() => {
+    return I._after();
+  });
+
+  describe('create and cleanup records', function() {
+
+    it('should create a new post', function*() {
+      yield I.have('post');
+      let resp = yield I.restHelper.sendGetRequest('/posts');
+      resp.body.length.should.eql(2);
+    });
+
+    it('should create a new post with predefined field', function*() {
+      yield I.have('post', { author: 'Tapac' });
+      let resp = yield I.restHelper.sendGetRequest('/posts/1');
+      resp.body.author.should.eql('davert');
+      resp = yield I.restHelper.sendGetRequest('/posts/2');
+      resp.body.author.should.eql('Tapac');
+    });
+
+    it('should cleanup created data', function*() {
+      yield I.have('post', { author: 'Tapac' });
+      let resp = yield I.restHelper.sendGetRequest('/posts/2');
+      resp.body.author.should.eql('Tapac');
+      yield I._after();
+      resp = yield I.restHelper.sendGetRequest('/posts/2');
+      resp.body.should.be.empty;
+      resp = yield I.restHelper.sendGetRequest('/posts');
+      resp.body.length.should.eql(1);
+
+    });
+
+    it('should create multiple posts and cleanup after', function*() {
+      let resp = yield I.restHelper.sendGetRequest('/posts');
+      resp.body.length.should.eql(1);
+      yield I.haveMultiple('post', 3);
+      yield new Promise((done) => setTimeout(done, 500));
+      resp = yield I.restHelper.sendGetRequest('/posts');
+      resp.body.length.should.eql(4);
+      yield I._after();
+      yield new Promise((done) => setTimeout(done, 500));
+      resp = yield I.restHelper.sendGetRequest('/posts');
+      resp.body.length.should.eql(1);
+    });
+
+    it('should create with different api', function*() {
+      I = new ApiDataFactory({
+        endpoint: api_url,
+        factories: {
+          post: {
+            factory: path.join(__dirname, '/../data/rest/posts_factory.js'),
+            uri: "/posts",
+            create: { post: '/comments' },
+            delete: { delete: '/comments/{id}'
+          }
+        }
+      }
+      });
+      yield I.have('post');
+      let resp = yield I.restHelper.sendGetRequest('/posts');
+      resp.body.length.should.eql(1);
+      resp = yield I.restHelper.sendGetRequest('/comments');
+      resp.body.length.should.eql(1);
+    });
+
+    it('should not remove records if cleanup:false', function*() {
+      I = new ApiDataFactory({
+        endpoint: api_url,
+        cleanup: false,
+        factories: {
+          post: {
+            factory: path.join(__dirname, '/../data/rest/posts_factory.js'),
+            uri: "/posts"
+          }
+        }
+      });
+      yield I.have('post');
+      let resp = yield I.restHelper.sendGetRequest('/posts');
+      resp.body.length.should.eql(2);
+      yield I._after();
+      yield new Promise((done) => setTimeout(done, 500));
+      resp = yield I.restHelper.sendGetRequest('/posts');
+      resp.body.length.should.eql(2);
+    });
+
+    it ('should send default headers', function*() {
+      I = new ApiDataFactory({
+        endpoint: api_url,
+        REST: {
+          defaultHeaders: {
+            'auth': '111'
+          }
+        },
+        factories: {
+          post: {
+            factory: path.join(__dirname, '/../data/rest/posts_factory.js'),
+            create: { post: '/headers' }
+          }
+        }
+      });
+      let resp = yield I.have('post');
+      resp.should.have.property('authorization');
+      resp.should.have.property('auth');
+      resp.auth.should.eql('111');
+
+    });
+  });
+});
\ No newline at end of file
diff --git a/test/rest/REST_test.js b/test/rest/REST_test.js
new file mode 100644
index 00000000..c29c7a43
--- /dev/null
+++ b/test/rest/REST_test.js
@@ -0,0 +1,106 @@
+'use strict';
+let REST = require('../../lib/helper/REST');
+let should = require('chai').should();
+let api_url = 'http://127.0.0.1:8010';
+let assert = require('assert');
+let path = require('path');
+let fs = require('fs');
+let fileExists = require('../../lib/utils').fileExists;
+let AssertionFailedError = require('../../lib/assert/error');
+let expectError = require('../../lib/utils').test.expectError;
+let I;
+let dbFile = path.join(__dirname, '/../data/rest/db.json');
+require('co-mocha')(require('mocha'));
+
+const data = {
+  "posts": [
+    {
+      "id": 1,
+      "title": "json-server",
+      "author": "davert"
+    }
+  ],
+  "user": {
+    "name": "davert"
+  }
+}
+
+describe('REST', function () {
+
+  before(function() {
+    I = new REST({
+      endpoint: api_url,
+      defaultHeaders: {
+        'X-Test': 'test'
+      }
+    });
+  });
+
+  beforeEach((done) => {
+    try {
+      fs.writeFileSync(dbFile, JSON.stringify(data));
+    } catch (err) {}
+    setTimeout(done, 700);
+  })
+
+  describe('basic requests', function() {
+    it('should send GET requests', () => {
+      return I.sendGetRequest('/user').then((response) => {
+        response.body.name.should.eql('davert');
+      });
+    });
+
+    it('should send POST requests', () => {
+      return I.sendPostRequest('/user', { name: 'john' }).then((response) => {
+        response.body.name.should.eql('john');
+      });
+    });
+
+    it('should send PUT requests', () => {
+      return I.sendPutRequest('/posts/1', { author: 'john' }).then((response) => {
+        response.body.author.should.eql('john');
+        return I.sendGetRequest('/posts/1').then((response) => {
+          response.body.author.should.eql('john');
+        });
+      });
+    });
+
+    it('should send DELETE requests', () => {
+      return I.sendDeleteRequest('/posts/1').then((response) => {
+        return I.sendGetRequest('/posts').then((response) => {
+          response.body.should.be.empty;
+        });
+      });
+    });
+
+  });
+
+  describe('headers', function() {
+    it ('should send request headers', () => {
+      return I.sendGetRequest('/headers', { 'Content-Type': 'application/json'}).then((resp) => {
+        resp.body.should.have.property('content-type');
+        resp.body['content-type'].should.eql('application/json');
+
+        resp.body.should.have.property('x-test');
+        resp.body['x-test'].should.eql('test');
+      });
+    });
+
+    it ('should set request headers', function*() {
+      I.haveRequestHeaders({HTTP_X_REQUESTED_WITH: 'xmlhttprequest'});
+      yield I.sendGetRequest('/headers', { 'Content-Type': 'application/json'}).then((resp) => {
+        resp.body.should.have.property('content-type');
+        resp.body['content-type'].should.eql('application/json');
+
+        resp.body.should.have.property('x-test');
+        resp.body['x-test'].should.eql('test');
+
+        resp.body.should.have.property('http_x_requested_with');
+        resp.body['http_x_requested_with'].should.eql('xmlhttprequest');
+      });
+
+
+    });
+  });
+
+});
\ No newline at end of file
diff --git a/test/runner/interface_test.js b/test/runner/interface_test.js
index 55310432..70ba75b7 100644
--- a/test/runner/interface_test.js
+++ b/test/runner/interface_test.js
@@ -30,27 +30,20 @@ describe('CodeceptJS Interface', () => {
       var queue1 = stdout.match(/\[1\] .+/g);
       queue1.should.eql([
         "[1] Starting recording promises",
-        "[1] Queued | hook FileSystem._beforeSuite()"
-      ]);
-
-      var queue2 = stdout.match(/\[2\] .+/g);
-      queue2.should.eql([
-        `[2] Starting recording promises`,
-        `[2] Queued | hook FileSystem._before()`,
-        `[2] Queued | amInPath: "."`,
-        `[2] Queued | return step result`,
-        `[2] Queued | say hello world`,
-        `[2] Queued | seeFile: "codecept.json"`,
-        `[2] Queued | return step result`,
-        `[2] Queued | fire test.passed`,
-        `[2] Queued | finish test`,
-        `[2] Queued | hook FileSystem._after()`
-      ]);
-
-      var queue3 = stdout.match(/\[3\] .+/g);
-      queue3.should.eql([
-        `[3] Starting recording promises`,
-        `[3] Queued | hook FileSystem._afterSuite()`
+        "[1] Queued | hook FileSystem._beforeSuite()",
+        `[1] Queued | hook FileSystem._before()`,
+        `[1] Queued | amInPath: "."`,
+        `[1] Queued | return step result`,
+        `[1] Queued | return execution time`,
+        `[1] Queued | say hello world`,
+        `[1] Queued | seeFile: "codecept.json"`,
+        `[1] Queued | return step result`,
+        `[1] Queued | return execution time`,
+        `[1] Queued | fire test.passed`,
+        `[1] Queued | finish test`,
+        `[1] Queued | hook FileSystem._after()`,
+        `[1] Queued | hook FileSystem._afterSuite()`,
+        `[1] Queued | hook FileSystem._finishTest()`,
       ]);
 
       let lines = stdout.match(/\S.+/g);
@@ -58,7 +51,6 @@ describe('CodeceptJS Interface', () => {
       // before hooks
       let beforeStep = [
         `Emitted | step.before (I am in path ".")`,
-        `[2] Queued | amInPath: "."`,
         `Emitted | step.after (I am in path ".")`,
         `Emitted | step.start (I am in path ".")`,
         `• I am in path "."`
@@ -82,4 +74,4 @@ describe('CodeceptJS Interface', () => {
     });
   });
 
-});
\ No newline at end of file
+});