Repository: mcollina/autocannon
Pull request #36 (2016-06-30 17:53:23)

begin refactoring to allow passing array of requests


Patch:
diff --git a/README.md b/README.md
index a7d3d95..4dfeccc 100644
--- a/README.md
+++ b/README.md
@@ -87,12 +87,13 @@ Start autocannon against the given target.
     * `connections`: The number of concurrent connections. _OPTIONAL_ default: `10`.
     * `duration`: The number of seconds to run the autocannon. _OPTIONAL_ default: `10`.
     * `timeout`: The number of seconds to wait for a response before . _OPTIONAL_ default: `10`.
-    * `pipelining`: The number of [pipelined requests](https://en.wikipedia.org/wiki/HTTP_pipelining) for each connection. _OPTIONAL_ default: `1`.
+    * `pipelining`: The number of [pipelined requests](https://en.wikipedia.org/wiki/HTTP_pipelining) for each connection. Will cause the `Client` API to throw when greater than 1. _OPTIONAL_ default: `1`.
     * `bailout`: The threshold of the number of errors when making the requests to the server before this instance bail's out. This instance will take all existing results so far and aggregate them into the results. If none passed here, the instance will ignore errors and never bail out. _OPTIONAL_ default: `undefined`.
     * `method`: The http method to use. _OPTIONAL_ `default: 'GET'`.
     * `body`: A `String` or a `Buffer` containing the body of the request. Leave undefined for an empty body. _OPTIONAL_ default: `undefined`.
     * `headers`: An `Object` containing the headers of the request. _OPTIONAL_ default: `{}`.
-    * `customiseRequest`: A `Function` which will be passed the `Client` object for each connection to be made. This can be used to customise each individual connection headers and body using the API shown below. The changes you make to the client in this function will take precedence over the default `body` and `headers` you pass in here. There is an example of this in the samples folder. _OPTIONAL_ default: `function noop () {}`.
+    * `setupClient`: A `Function` which will be passed the `Client` object for each connection to be made. This can be used to customise each individual connection headers and body using the API shown below. The changes you make to the client in this function will take precedence over the default `body` and `headers` you pass in here. There is an example of this in the samples folder. _OPTIONAL_ default: `function noop () {}`.
+    * `Requests`: An `Array` of `Object`s which represents the sequence of requests to make while benchmarking. Can be used in conjunction with the `body`, `headers` and `method` params above. The `Object`s in this array can have `body`, `headers`, `method`, or `path` attributes, which overwrite those that are passed in this `opts` object. Therefore, the ones in this (`opts`) object take precedence and should be viewed as defaults. _OPTIONAL_.
 * `cb`: The callback which is called on completion of the benchmark. Takes the following params. _OPTIONAL_.
     * `err`: If there was an error encountered with the run.
     * `results`: The results of the run.
@@ -140,11 +141,14 @@ Because an autocannon instance is an `EventEmitter`, it emits several events. th
 
 ### `Client` API
 
-This object is passed as the first parameter of the `response` event from an autocannon instance. You can use this to modify the body and headers of the requests you are sending while benchmarking.
+This object is passed as the first parameter of the `response` event from an autocannon instance. You can use this to modify the requests you are sending while benchmarking.
+
+* `client.setHeaders(headers)`: Used to modify the headers of the request this client iterator is currently on. `headers` should be an `Object`, or `undefined` if you want to remove your headers.
+* `client.setBody(body)`: Used to modify the body of the request this client iterator is currently on. `body` should be a `String` or `Buffer`, or `undefined` if you want to remove the body.
+* `client.setHeadersAndBody(headers, body)`: Used to modify the both the headers and body this client iterator is currently on.`headers` and `body` should take the same form as above.
+* `client.setRequest(request)`: Used to modify the both the entire request that this client iterator is currently on. Can have `headers`, `body`, `method`, or `path` as attributes. Defaults to the values passed into the autocannon instance when it was created. `Note: call this when modifying multiple request values for faster encoding`
+* `client.setRequests(newRequests)`: Used to overwrite the entire requests array that was passed into the instance on initiation. `Note: call this when modifying multiple requests for faster encoding`
 
-* `client.setHeaders(headers)`: Used to modify the headers of future requests this client makes. `headers` should be an `Object`, or `undefined` if you want to remove your headers.
-* `client.setBody(body)`: Used to modify the body of futures requests this client makes. `body` should be a `String` or `Buffer`, or `undefined` if you want to remove the body.
-* `client.setHeadersAndBody(headers, body)`: Used to modify the both the header and body of future requests this client makes.`headers` and `body` should take the same form as above. `Note: call this when modifying both headers and body for faster response encoding`
 
 Example using the autocannon events and the client API:
 
diff --git a/appveyor.yml b/appveyor.yml
new file mode 100644
index 0000000..1bd2942
--- /dev/null
+++ b/appveyor.yml
@@ -0,0 +1,40 @@
+environment:
+  matrix:
+    - nodejs_version: 4.0.0
+      platform: x64
+      msvs_toolset: 12
+    - nodejs_version: 4.0.0
+      platform: x86
+      msvs_toolset: 12
+    - nodejs_version: 5.0.0
+      platform: x64
+      msvs_toolset: 12
+    - nodejs_version: 5.0.0
+      platform: x86
+      msvs_toolset: 12
+    - nodejs_version: 6.0.0
+      platform: x64
+      msvs_toolset: 12
+    - nodejs_version: 6.0.0
+      platform: x86
+      msvs_toolset: 12
+
+os: Visual Studio 2015
+
+# Install scripts. (runs after repo cloning)
+install:
+  # Get the latest stable version of Node.js or io.js
+  - ps: Install-Product node $env:nodejs_version
+  # install modules
+  - npm install
+
+# Post-install test scripts.
+test_script:
+  # Output useful info for debugging.
+  - node --version
+  - npm --version
+  # run tests
+  - npm test
+
+# Don't actually build.
+build: off
diff --git a/lib/myhttp.js b/lib/httpClient.js
similarity index 67%
rename from lib/myhttp.js
rename to lib/httpClient.js
index 85f87d0..1fa6235 100644
--- a/lib/myhttp.js
+++ b/lib/httpClient.js
@@ -6,31 +6,23 @@ const net = require('net')
 const tls = require('tls')
 const retimer = require('retimer')
 const HTTPParser = require('http-parser-js').HTTPParser
-const methods = [
-  'GET',
-  'DELETE',
-  'POST',
-  'PUT'
-]
+const RequestIterator = require('./requestIterator')
 
 function Client (opts) {
   if (!(this instanceof Client)) {
     return new Client(opts)
   }
 
-  opts.customiseRequest = opts.customiseRequest || noop
+  opts.setupClient = opts.setupClient || noop
   opts.pipelining = opts.pipelining || 1
   opts.port = opts.port || 80
-  opts.method = opts.method || 'GET'
-
-  if (methods.indexOf(opts.method) < 0) {
-    throw new Error(`${opts.method} HTTP method is not supported`)
-  }
 
   this.opts = opts
   this.timeout = (opts.timeout || 10) * 1000
   this.secure = opts.protocol === 'https:'
   this.parser = new HTTPParser(HTTPParser.RESPONSE)
+  this.requestIterator = new RequestIterator(opts.requests, opts)
+
   this.resData = new Array(opts.pipelining)
   for (var i = 0; i < this.resData.length; i++) {
     this.resData[i] = {
@@ -44,6 +36,8 @@ function Client (opts) {
   this.cer = 0
   this.destroyed = false
 
+  opts.setupClient(this)
+
   const handleTimeout = () => {
     // all pipelined requests have timed out here
     this.resData.forEach(() => this.emit('timeout'))
@@ -75,19 +69,6 @@ function Client (opts) {
     this.timeoutTicker.reschedule(this.timeout)
   }
 
-  opts.path = opts.path || '/'
-  opts.hostname = opts.hostname || 'localhost'
-
-  if (!opts.host && opts.hostname && opts.port) {
-    opts.host = opts.hostname + ':' + opts.port
-  }
-
-  this.baseReq = `${opts.method} ${opts.path} HTTP/1.1\r\nHost: ${opts.host}\r\nConnection: keep-alive\r\n`
-
-  this.setHeaders(opts.headers)
-
-  opts.customiseRequest(this)
-
   this._connect()
 }
 
@@ -122,10 +103,11 @@ Client.prototype._connect = function () {
 // rpi = request pipelining index
 Client.prototype._doRequest = function (rpi) {
   this.resData[rpi].startTime = process.hrtime()
-  this.conn.write(this._req)
+  this.conn.write(this.requestIterator.move())
 }
 
 Client.prototype._destroyConnection = function () {
+  // if (!this.conn) return
   this.conn.removeAllListeners('error')
   this.conn.removeAllListeners('end')
   this.conn.on('error', () => {})
@@ -138,47 +120,38 @@ Client.prototype.destroy = function () {
   this._destroyConnection()
 }
 
+Client.prototype.getRequestBuffer = function (newHeaders) {
+  return this.requestIterator.currentRequest.requestBuffer
+}
+
 Client.prototype.setHeaders = function (newHeaders) {
-  this.opts.headers = newHeaders || {}
-  this._rebuild()
+  this._okayToUpdateCheck()
+  this.requestIterator.setHeaders(newHeaders)
 }
 
 Client.prototype.setBody = function (newBody) {
-  this.opts.body = newBody
-
-  this._rebuild()
+  this._okayToUpdateCheck()
+  this.requestIterator.setBody(newBody)
 }
 
 Client.prototype.setHeadersAndBody = function (newHeaders, newBody) {
-  this.opts.headers = newHeaders || {}
-  this.opts.body = newBody
-
-  this._rebuild()
+  this._okayToUpdateCheck()
+  this.requestIterator.setHeadersAndBody(newHeaders, newBody)
 }
 
-Client.prototype._rebuild = function () {
-  let body
-
-  if (typeof this.opts.body === 'string') {
-    body = new Buffer(this.opts.body)
-  } else if (Buffer.isBuffer(this.opts.body)) {
-    body = this.opts.body
-  } else if (this.opts.body) {
-    throw new Error('body must be either a string or a buffer')
-  }
-
-  if (body) {
-    this.opts.headers['Content-Length'] = '' + body.length
-  }
-
-  this._req = Object.keys(this.opts.headers)
-    .map((key) => `${key}: ${this.opts.headers[key]}\r\n`)
-    .reduce((acc, str) => acc + str, this.baseReq)
+Client.prototype.setRequest = function (newRequest) {
+  this._okayToUpdateCheck()
+  this.requestIterator.setRequest(newRequest)
+}
 
-  this._req = new Buffer(this._req + '\r\n', 'utf8')
+Client.prototype.setRequests = function (newRequests) {
+  this._okayToUpdateCheck()
+  this.requestIterator.setRequests(newRequests)
+}
 
-  if (body) {
-    this._req = Buffer.concat([this._req, body, new Buffer('\r\n')])
+Client.prototype._okayToUpdateCheck = function () {
+  if (this.opts.pipelining > 1) {
+    throw new Error('cannot update requests when the piplining factor is greater than 1')
   }
 }
 
diff --git a/lib/httpRequestBuilder.js b/lib/httpRequestBuilder.js
new file mode 100644
index 0000000..e2b4713
--- /dev/null
+++ b/lib/httpRequestBuilder.js
@@ -0,0 +1,86 @@
+'use strict'
+
+const xtend = require('xtend')
+
+const methods = [
+  'GET',
+  'DELETE',
+  'POST',
+  'PUT'
+]
+
+// this is a build request factory, that curries the build request function
+// and sets the default for it
+function requestBuilder (defaults) {
+  // these need to be defined per request builder creation, because of the way
+  // headers don't get deep copied
+  const builderDefaults = {
+    method: 'GET',
+    path: '/',
+    headers: {},
+    body: new Buffer(0),
+    hostname: 'localhost',
+    port: 80
+  }
+
+  defaults = xtend(builderDefaults, defaults)
+
+  // buildRequest takes an object, and turns it into a buffer representing the
+  // http request
+  return function buildRequest (reqData) {
+    // below is a hack to enable deep extending of the headers so the default
+    // headers object isn't overwritten by accident
+    reqData = reqData || {}
+    reqData.headers = xtend(defaults.headers, reqData.headers)
+
+    reqData = xtend(defaults, reqData)
+    // for some reason some tests fail with method === undefined
+    // the reqData.method should be set to SOMETHING in this case
+    // cannot find reason for failure if `|| 'GET'` is taken out
+    const method = reqData.method
+    const path = reqData.path
+    const headers = reqData.headers
+    const body = reqData.body
+
+    let host = reqData.host
+    if (!host) {
+      const hostname = reqData.hostname
+      const port = reqData.port
+      host = hostname + ':' + port
+    }
+
+    if (methods.indexOf(method) < 0) {
+      throw new Error(`${method} HTTP method is not supported`)
+    }
+
+    const baseReq = `${method} ${path} HTTP/1.1\r\nHost: ${host}\r\nConnection: keep-alive\r\n`
+
+    let bodyBuf
+
+    if (typeof body === 'string') {
+      bodyBuf = new Buffer(body)
+    } else if (Buffer.isBuffer(body)) {
+      bodyBuf = body
+    } else if (body) {
+      throw new Error('body must be either a string or a buffer')
+    }
+
+    if (bodyBuf && bodyBuf.length > 0) {
+      headers['Content-Length'] = '' + bodyBuf.length
+    }
+
+    let req = Object.keys(headers)
+      .map((key) => `${key}: ${headers[key]}\r\n`)
+      .reduce((acc, str) => acc + str, baseReq)
+
+    req = new Buffer(req + '\r\n', 'utf8')
+
+    if (bodyBuf && bodyBuf.length > 0) {
+      req = Buffer.concat([req, bodyBuf, new Buffer('\r\n')])
+    }
+
+    return req
+  }
+}
+
+module.exports = requestBuilder
diff --git a/lib/requestIterator.js b/lib/requestIterator.js
new file mode 100644
index 0000000..001140c
--- /dev/null
+++ b/lib/requestIterator.js
@@ -0,0 +1,75 @@
+const inherits = require('util').inherits
+const requestBuilder = require('./httpRequestBuilder')
+
+function RequestIterator (requests, defaults) {
+  if (!(this instanceof RequestIterator)) {
+    return new RequestIterator(requests, defaults)
+  }
+
+  this.reqDefaults = defaults
+  this.requestBuilder = requestBuilder(defaults)
+  this.setRequests(requests)
+}
+
+inherits(RequestIterator, Object)
+
+RequestIterator.prototype.nextRequest = function () {
+  ++this.currentRequestIndex
+  this.currentRequestIndex = this.currentRequestIndex < this.requests.length ? this.currentRequestIndex : 0
+  this.currentRequest = this.requests[this.currentRequestIndex]
+  return this.currentRequest
+}
+
+RequestIterator.prototype.nextRequestBuffer = function () {
+  // get next request
+  this.nextRequest()
+  return this.currentRequest.requestBuffer
+}
+
+RequestIterator.prototype.move = function () {
+  // get the current buffer and proceed to next request
+  const ret = this.currentRequest.requestBuffer
+  this.nextRequest()
+  return ret
+}
+
+RequestIterator.prototype.setRequests = function (newRequests) {
+  this.requests = newRequests || [{}]
+  this.currentRequestIndex = 0
+  this.currentRequest = this.requests[0]
+  this.rebuildRequests()
+}
+
+RequestIterator.prototype.rebuildRequests = function () {
+  this.requests.forEach((request) => {
+    request.requestBuffer = this.requestBuilder(request)
+  })
+}
+
+RequestIterator.prototype.setHeaders = function (newHeaders) {
+  this.currentRequest.headers = newHeaders || {}
+  this.rebuildRequest()
+}
+
+RequestIterator.prototype.setBody = function (newBody) {
+  this.currentRequest.body = newBody || new Buffer(0)
+  this.rebuildRequest()
+}
+
+RequestIterator.prototype.setHeadersAndBody = function (newHeaders, newBody) {
+  this.currentRequest.headers = newHeaders || {}
+  this.currentRequest.body = newBody || new Buffer(0)
+  this.rebuildRequest()
+}
+
+RequestIterator.prototype.setRequest = function (newRequest) {
+  this.currentRequest = newRequest || {}
+  this.rebuildRequest()
+}
+
+RequestIterator.prototype.rebuildRequest = function () {
+  this.currentRequest.requestBuffer = this.requestBuilder(this.currentRequest)
+  this.requests[this.currentRequestIndex] = this.currentRequest
+}
+
+module.exports = RequestIterator
diff --git a/lib/run.js b/lib/run.js
index 885c4ee..2d1503a 100644
--- a/lib/run.js
+++ b/lib/run.js
@@ -3,8 +3,20 @@
 const EE = require('events').EventEmitter
 const URL = require('url')
 const Histogram = require('native-hdr-histogram')
-const Client = require('./myhttp')
+const Client = require('./httpClient')
 const percentiles = require('./percentiles')
+const xtend = require('xtend')
+
+const defaultOptions = {
+  headers: {},
+  body: new Buffer(0),
+  method: 'GET',
+  duration: 10,
+  connections: 10,
+  pipelining: 1,
+  timeout: 10,
+  requests: [{}]
+}
 
 function run (opts, cb) {
   cb = cb || noop
@@ -23,10 +35,7 @@ function run (opts, cb) {
     0  // 5xx
   ]
 
-  opts.duration = opts.duration || 10
-  opts.connections = opts.connections || 10
-  opts.pipelining = opts.pipelining || 1
-  opts.timeout = opts.timeout || 10
+  opts = xtend(defaultOptions, opts)
 
   if (!opts.url) {
     cb(new Error('url option required'))
@@ -75,19 +84,20 @@ function run (opts, cb) {
   url.method = opts.method
   url.body = opts.body
   url.headers = opts.headers
-  url.customiseRequest = opts.customiseRequest
+  url.setupClient = opts.setupClient
   url.timeout = opts.timeout
+  url.requests = opts.requests
 
   let clients = []
   for (let i = 0; i < opts.connections; i++) {
     let client = new Client(url)
-    client.on('response', record)
+    client.on('response', onResponse)
     client.on('connError', onError)
     client.on('timeout', onTimeout)
     clients.push(client)
   }
 
-  function record (statusCode, resBytes, responseTime) {
+  function onResponse (statusCode, resBytes, responseTime) {
     tracker.emit('response', this, statusCode, resBytes, responseTime)
     statusCodes[(parseInt(statusCode) / 100) - 1] += 1
     latencies.record(responseTime)
diff --git a/package.json b/package.json
index 25b56eb..f7b2a7d 100644
--- a/package.json
+++ b/package.json
@@ -9,7 +9,9 @@
   "scripts": {
     "test": "standard && tap test/*.test.js"
   },
-  "pre-commit": ["test"],
+  "pre-commit": [
+    "test"
+  ],
   "repository": {
     "type": "git",
     "url": "git+https://github.com/mcollina/autocannon.git"
@@ -40,6 +42,7 @@
   },
   "dependencies": {
     "chalk": "^1.1.3",
+    "deep-extend": "^0.4.1",
     "http-parser-js": "^0.4.2",
     "minimist": "^1.2.0",
     "native-hdr-histogram": "^0.3.0",


Test patch:
diff --git a/test/myhttp.test.js b/test/httpClient.test.js
similarity index 75%
rename from test/myhttp.test.js
rename to test/httpClient.test.js
index 4b205b8..1095575 100644
--- a/test/myhttp.test.js
+++ b/test/httpClient.test.js
@@ -1,7 +1,7 @@
 'use strict'
 
 const test = require('tap').test
-const Client = require('../lib/myhttp')
+const Client = require('../lib/httpClient')
 const helper = require('./helper')
 const server = helper.startServer()
 const timeoutServer = helper.startTimeoutServer()
@@ -40,7 +40,7 @@ test('client calls a https server twice', (t) => {
   })
 })
 
-test('myhttp client automatically reconnects', (t) => {
+test('http client automatically reconnects', (t) => {
   t.plan(4)
 
   const client = new Client(server.address())
@@ -162,7 +162,7 @@ test('client supports sending a body which is a string', (t) => {
 })
 
 test('client supports changing the body', (t) => {
-  t.plan(4)
+  t.plan(2)
 
   const opts = server.address()
   opts.method = 'POST'
@@ -170,44 +170,39 @@ test('client supports changing the body', (t) => {
 
   const client = new Client(opts)
 
-  t.same(client._req,
+  t.same(client.getRequestBuffer(),
   new Buffer(`POST / HTTP/1.1\r\nHost: localhost:${server.address().port}\r\nConnection: keep-alive\r\nContent-Length: 11\r\n\r\nhello world\r\n`),
   'request is okay before modifying')
 
-  t.same(client.opts.body, 'hello world', 'body was as expected')
   client.setBody('modified')
-  t.same(client.opts.body, 'modified', 'body was changed')
 
-  t.same(client._req,
+  t.same(client.getRequestBuffer(),
   new Buffer(`POST / HTTP/1.1\r\nHost: localhost:${server.address().port}\r\nConnection: keep-alive\r\nContent-Length: 8\r\n\r\nmodified\r\n`),
   'body changes updated request')
   client.destroy()
 })
 
 test('client supports changing the headers', (t) => {
-  t.plan(4)
+  t.plan(2)
 
   const opts = server.address()
   opts.method = 'POST'
 
   const client = new Client(opts)
-
-  t.same(client._req,
+  t.same(client.getRequestBuffer(),
   new Buffer(`POST / HTTP/1.1\r\nHost: localhost:${server.address().port}\r\nConnection: keep-alive\r\n\r\n`),
   'request is okay before modifying')
 
-  t.same(client.opts.headers, {}, 'header was as expected')
   client.setHeaders({header: 'modified'})
-  t.same(client.opts.headers, {header: 'modified'}, 'header was changed')
 
-  t.same(client._req,
+  t.same(client.getRequestBuffer(),
   new Buffer(`POST / HTTP/1.1\r\nHost: localhost:${server.address().port}\r\nConnection: keep-alive\r\nheader: modified\r\n\r\n`),
   'header changes updated request')
   client.destroy()
 })
 
 test('client supports changing the headers and body', (t) => {
-  t.plan(6)
+  t.plan(2)
 
   const opts = server.address()
   opts.body = 'hello world'
@@ -215,27 +210,21 @@ test('client supports changing the headers and body', (t) => {
 
   const client = new Client(opts)
 
-  t.same(client._req,
+  t.same(client.getRequestBuffer(),
   new Buffer(`POST / HTTP/1.1\r\nHost: localhost:${server.address().port}\r\nConnection: keep-alive\r\nContent-Length: 11\r\n\r\nhello world\r\n`),
   'request is okay before modifying')
 
-  t.same(client.opts.body, 'hello world', 'body was as expected')
-  t.same(client.opts.headers, {'Content-Length': 11}, 'header was as expected')
-
   client.setBody('modified')
   client.setHeaders({header: 'modifiedHeader'})
 
-  t.same(client.opts.body, 'modified', 'body was changed')
-  t.same(client.opts.headers, {'Content-Length': 8, header: 'modifiedHeader'}, 'header was changed')
-
-  t.same(client._req,
+  t.same(client.getRequestBuffer(),
   new Buffer(`POST / HTTP/1.1\r\nHost: localhost:${server.address().port}\r\nConnection: keep-alive\r\nheader: modifiedHeader\r\nContent-Length: 8\r\n\r\nmodified\r\n`),
   'changes updated request')
   client.destroy()
 })
 
 test('client supports changing the headers and body together', (t) => {
-  t.plan(6)
+  t.plan(2)
 
   const opts = server.address()
   opts.body = 'hello world'
@@ -243,31 +232,46 @@ test('client supports changing the headers and body together', (t) => {
 
   const client = new Client(opts)
 
-  t.same(client._req,
+  t.same(client.getRequestBuffer(),
   new Buffer(`POST / HTTP/1.1\r\nHost: localhost:${server.address().port}\r\nConnection: keep-alive\r\nContent-Length: 11\r\n\r\nhello world\r\n`),
   'request is okay before modifying')
 
-  t.same(client.opts.body, 'hello world', 'body was as expected')
-  t.same(client.opts.headers, {'Content-Length': 11}, 'header was as expected')
-
   client.setHeadersAndBody({header: 'modifiedHeader'}, 'modified')
 
-  t.same(client.opts.body, 'modified', 'body was changed')
-  t.same(client.opts.headers, {'Content-Length': 8, header: 'modifiedHeader'}, 'header was changed')
-
-  t.same(client._req,
+  t.same(client.getRequestBuffer(),
   new Buffer(`POST / HTTP/1.1\r\nHost: localhost:${server.address().port}\r\nConnection: keep-alive\r\nheader: modifiedHeader\r\nContent-Length: 8\r\n\r\nmodified\r\n`),
   'changes updated request')
   client.destroy()
 })
 
+test('client supports updating the current request object', (t) => {
+  t.plan(2)
+
+  const opts = server.address()
+  opts.body = 'hello world'
+  opts.method = 'POST'
+
+  const client = new Client(opts)
+
+  t.same(client.getRequestBuffer(),
+    new Buffer(`POST / HTTP/1.1\r\nHost: localhost:${server.address().port}\r\nConnection: keep-alive\r\nContent-Length: 11\r\n\r\nhello world\r\n`),
+    'request is okay before modifying')
+
+  client.setRequest({ headers: { header: 'modifiedHeader' }, body: 'modified', method: 'GET' })
+
+  t.same(client.getRequestBuffer(),
+    new Buffer(`GET / HTTP/1.1\r\nHost: localhost:${server.address().port}\r\nConnection: keep-alive\r\nheader: modifiedHeader\r\nContent-Length: 8\r\n\r\nmodified\r\n`),
+    'changes updated request')
+  client.destroy()
+})
+
 test('client customiseRequest function overwrites the headers and body', (t) => {
-  t.plan(9)
+  t.plan(5)
 
   const opts = server.address()
   opts.body = 'hello world'
   opts.method = 'POST'
-  opts.customiseRequest = (client) => {
+  opts.setupClient = (client) => {
     t.ok(client.setHeadersAndBody, 'client had setHeadersAndBody method')
     t.ok(client.setHeaders, 'client had setHeaders method')
     t.ok(client.setBody, 'client had setBody method')
@@ -277,23 +281,29 @@ test('client customiseRequest function overwrites the headers and body', (t) =>
 
   const client = new Client(opts)
 
-  t.same(client.opts.body, 'modified', 'body was changed')
-  t.notSame(client.opts.body, 'hello world', 'body was changed')
-
-  t.same(client.opts.headers, {'Content-Length': 8, header: 'modifiedHeader'}, 'header was changed')
-  t.notSame(client.opts.body, {'Content-Length': 11}, 'header was changed')
-
-  t.same(client._req,
+  t.same(client.getRequestBuffer(),
   new Buffer(`POST / HTTP/1.1\r\nHost: localhost:${server.address().port}\r\nConnection: keep-alive\r\nheader: modifiedHeader\r\nContent-Length: 8\r\n\r\nmodified\r\n`),
   'changes updated request')
 
-  t.notSame(client._req,
+  t.notSame(client.getRequestBuffer(),
   new Buffer(`POST / HTTP/1.1\r\nHost: localhost:${server.address().port}\r\nConnection: keep-alive\r\nContent-Length: 11\r\n\r\nhello world\r\n`),
   'changes updated request')
 
   client.destroy()
 })
 
+test('client should throw when attempting to modify the request with a pipelining greater than 1', (t) => {
+  t.plan(1)
+
+  const opts = server.address()
+  opts.pipelining = 10
+  const client = new Client(opts)
+
+  t.throws(() => client.setHeaders({}))
+
+  client.destroy()
+})
+
 test('client should emit a timeout when no response is received', (t) => {
   t.plan(1)
 
@@ -321,3 +331,38 @@ test('client should emit 2 timeouts when no responses are received', (t) => {
 
   setTimeout(() => client.destroy(), 2500)
 })
+
+test('client should have 2 different requests it iterates over', (t) => {
+  t.plan(3)
+  const opts = server.address()
+  opts.method = 'POST'
+
+  opts.requests = [{
+    body: 'hello world'
+  },
+  {
+    method: 'GET',
+    body: 'modified'
+  }]
+
+  const client = new Client(opts)
+  let number = 0
+
+  client.on('response', (statusCode, length) => {
+    number++
+    if (number === 1 || number === 3) {
+      t.same(client.getRequestBuffer(),
+        new Buffer(`GET / HTTP/1.1\r\nHost: localhost:${server.address().port}\r\nConnection: keep-alive\r\nContent-Length: 8\r\n\r\nmodified\r\n`),
+        'body changes updated request')
+
+      if (number === 3) {
+        client.destroy()
+        t.end()
+      }
+    } else {
+      t.same(client.getRequestBuffer(),
+        new Buffer(`POST / HTTP/1.1\r\nHost: localhost:${server.address().port}\r\nConnection: keep-alive\r\nContent-Length: 11\r\n\r\nhello world\r\n`),
+        'request was okay')
+    }
+  })
+})
diff --git a/test/httpRequestBuilder.test.js b/test/httpRequestBuilder.test.js
new file mode 100644
index 0000000..777871d
--- /dev/null
+++ b/test/httpRequestBuilder.test.js
@@ -0,0 +1,58 @@
+'use strict'
+
+const test = require('tap').test
+const helper = require('./helper')
+const server = helper.startServer()
+const RequestBuilder = require('../lib/httpRequestBuilder')
+
+test('request builder should create a request with sensible defaults', (t) => {
+  t.plan(1)
+
+  const opts = server.address()
+
+  const build = RequestBuilder(opts)
+
+  const result = build()
+  t.same(result,
+    new Buffer(`GET / HTTP/1.1\r\nHost: localhost:${server.address().port}\r\nConnection: keep-alive\r\n\r\n`),
+    'request is okay')
+})
+
+test('request builder should allow default overwriting', (t) => {
+  t.plan(1)
+
+  const opts = server.address()
+  opts.method = 'POST'
+
+  const build = RequestBuilder(opts)
+
+  const result = build()
+  t.same(result,
+    new Buffer(`POST / HTTP/1.1\r\nHost: localhost:${server.address().port}\r\nConnection: keep-alive\r\n\r\n`),
+    'request is okay')
+})
+
+test('request builder should allow per build overwriting', (t) => {
+  t.plan(1)
+
+  const opts = server.address()
+  opts.method = 'POST'
+
+  const build = RequestBuilder(opts)
+
+  const result = build({method: 'GET'})
+
+  t.same(result,
+    new Buffer(`GET / HTTP/1.1\r\nHost: localhost:${server.address().port}\r\nConnection: keep-alive\r\n\r\n`),
+    'request is okay')
+})
+
+test('request builder should throw on unknown http method', (t) => {
+  t.plan(1)
+
+  const opts = server.address()
+
+  const build = RequestBuilder(opts)
+
+  t.throws(() => build({method: 'HEAD'}))
+})
diff --git a/test/requestIterator.test.js b/test/requestIterator.test.js
new file mode 100644
index 0000000..323674f
--- /dev/null
+++ b/test/requestIterator.test.js
@@ -0,0 +1,145 @@
+'use strict'
+
+const test = require('tap').test
+const helper = require('./helper')
+const server = helper.startServer()
+const RequestIterator = require('../lib/requestIterator')
+
+test('request iterator should create requests with sensible defaults', (t) => {
+  t.plan(3)
+
+  const opts = server.address()
+
+  let iterator = new RequestIterator(undefined, opts)
+
+  t.same(iterator.currentRequest.requestBuffer,
+    new Buffer(`GET / HTTP/1.1\r\nHost: localhost:${server.address().port}\r\nConnection: keep-alive\r\n\r\n`),
+    'request is okay')
+
+  iterator = new RequestIterator([{}], opts)
+
+  t.same(iterator.currentRequest.requestBuffer,
+    new Buffer(`GET / HTTP/1.1\r\nHost: localhost:${server.address().port}\r\nConnection: keep-alive\r\n\r\n`),
+    'request is okay')
+
+  iterator = new RequestIterator([], opts)
+
+  t.notOk(iterator.currentRequest, 'request doesn\'t exist')
+})
+
+test('request iterator should create requests with overwritten defaults', (t) => {
+  t.plan(1)
+
+  const opts = server.address()
+  opts.method = 'POST'
+
+  let iterator = new RequestIterator(undefined, opts)
+
+  t.same(iterator.currentRequest.requestBuffer,
+    new Buffer(`POST / HTTP/1.1\r\nHost: localhost:${server.address().port}\r\nConnection: keep-alive\r\n\r\n`),
+    'request is okay')
+})
+
+test('request iterator should create requests with overwritten defaults', (t) => {
+  t.plan(3)
+
+  const opts = server.address()
+  opts.method = 'POST'
+
+  const requests = [
+    {
+      body: 'hello world'
+    },
+    {
+      method: 'GET',
+      body: 'modified'
+    }
+  ]
+
+  const request1Res = new Buffer(`POST / HTTP/1.1\r\nHost: localhost:${server.address().port}\r\nConnection: keep-alive\r\nContent-Length: 11\r\n\r\nhello world\r\n`)
+  const request2Res = new Buffer(`GET / HTTP/1.1\r\nHost: localhost:${server.address().port}\r\nConnection: keep-alive\r\nContent-Length: 8\r\n\r\nmodified\r\n`)
+
+  let iterator = new RequestIterator(requests, opts)
+
+  t.same(iterator.currentRequest.requestBuffer, request1Res, 'request was okay')
+  iterator.nextRequest()
+  t.same(iterator.currentRequest.requestBuffer, request2Res, 'request was okay')
+  iterator.nextRequest()
+  t.same(iterator.currentRequest.requestBuffer, request1Res, 'request was okay')
+})
+
+test('request iterator should allow for overwriting the requests passed in, but still use overwritten defaults', (t) => {
+  t.plan(5)
+
+  const opts = server.address()
+  opts.method = 'POST'
+
+  const requests1 = [
+    {
+      body: 'hello world'
+    },
+    {
+      method: 'GET',
+      body: 'modified'
+    }
+  ]
+
+  const requests2 = [
+    {
+      body: 'hell0 w0rld'
+    }
+  ]
+
+  const request1Res = new Buffer(`POST / HTTP/1.1\r\nHost: localhost:${server.address().port}\r\nConnection: keep-alive\r\nContent-Length: 11\r\n\r\nhello world\r\n`)
+  const request2Res = new Buffer(`GET / HTTP/1.1\r\nHost: localhost:${server.address().port}\r\nConnection: keep-alive\r\nContent-Length: 8\r\n\r\nmodified\r\n`)
+  const request3Res = new Buffer(`POST / HTTP/1.1\r\nHost: localhost:${server.address().port}\r\nConnection: keep-alive\r\nContent-Length: 11\r\n\r\nhell0 w0rld\r\n`)
+
+  let iterator = new RequestIterator(requests1, opts)
+
+  t.same(iterator.currentRequest.requestBuffer, request1Res, 'request was okay')
+  iterator.nextRequest()
+  t.same(iterator.currentRequest.requestBuffer, request2Res, 'request was okay')
+  iterator.nextRequest()
+  t.same(iterator.currentRequest.requestBuffer, request1Res, 'request was okay')
+
+  iterator.setRequests(requests2)
+  t.same(iterator.currentRequest.requestBuffer, request3Res, 'request was okay')
+  iterator.nextRequest()
+  t.same(iterator.currentRequest.requestBuffer, request3Res, 'request was okay')
+})
+
+test('request iterator should allow for rebuilding the current request', (t) => {
+  t.plan(6)
+
+  const opts = server.address()
+  opts.method = 'POST'
+
+  const requests1 = [
+    {
+      body: 'hello world'
+    },
+    {
+      method: 'GET',
+      body: 'modified'
+    }
+  ]
+
+  const request1Res = new Buffer(`POST / HTTP/1.1\r\nHost: localhost:${server.address().port}\r\nConnection: keep-alive\r\nContent-Length: 11\r\n\r\nhello world\r\n`)
+  const request2Res = new Buffer(`POST / HTTP/1.1\r\nHost: localhost:${server.address().port}\r\nConnection: keep-alive\r\nContent-Length: 8\r\n\r\nmodified\r\n`)
+  const request3Res = new Buffer(`GET / HTTP/1.1\r\nHost: localhost:${server.address().port}\r\nConnection: keep-alive\r\nContent-Length: 8\r\n\r\nmodified\r\n`)
+  const request4Res = new Buffer(`POST / HTTP/1.1\r\nHost: localhost:${server.address().port}\r\nConnection: keep-alive\r\nheader: modifiedHeader\r\nContent-Length: 8\r\n\r\nmodified\r\n`)
+  const request5Res = new Buffer(`POST / HTTP/1.1\r\nHost: localhost:${server.address().port}\r\nConnection: keep-alive\r\n\r\n`)
+
+  let iterator = new RequestIterator(requests1, opts)
+  t.same(iterator.currentRequest.requestBuffer, request1Res, 'request was okay')
+  iterator.setBody('modified')
+  t.same(iterator.currentRequest.requestBuffer, request2Res, 'request was okay')
+  iterator.nextRequest() // verify it didn't affect the other request
+  t.same(iterator.currentRequest.requestBuffer, request3Res, 'request was okay')
+  iterator.nextRequest()
+  t.same(iterator.currentRequest.requestBuffer, request2Res, 'request was okay')
+  iterator.setHeaders({header: 'modifiedHeader'})
+  t.same(iterator.currentRequest.requestBuffer, request4Res, 'request was okay')
+  iterator.setRequest() // this should build default request
+  t.same(iterator.currentRequest.requestBuffer, request5Res, 'request was okay')
+})