Repository: mcollina/autocannon
Pull request #360 (2021-04-13 14:07:23)

Fix: forever on command line
This PR started out to fix the forever bug on the command line. fixes https://github.com/mcollina/autocannon/issues/358

However, it was difficult to unit test the fix using the installed version of tap which required upgrading to tap 15.

tap 15 introduced a new problem, in that the coverage test is stricter than it used to be and as a result total coverage has fallen from the previous 100% to 96% (lines). I have examined all the uncovered code and added additional tests for all the low lying fruit. What remains is difficult to test without refactoring. Much of the current coverage is achieved through functional tests that happen to cover imported functions rather than unit tests and the odd lines not being tested really require unit tests being created for the whole function rather than just the odd fix to create coverage. ~I've added minimum coverage values to `.taprc` to maintain at least the coverage that we currently have.~ I've had to lower the coverage further as node 10 skips some tests and this isn't taken into account in the coverage report
 

Upgrading tap to v 15 also produced a bunch of deprecation warnings for  `t.tearDown`, `t.deepEqual`, `t.is` and `t.false`. These have all been updated to their current variants.

Finally a couple of bugs were found in `validate.js` whilst writing tests for it. I've noted them in the code below.

![Screenshot 2021-04-13 at 15 05 20](https://user-images.githubusercontent.com/4930551/114565960-b2313300-9c69-11eb-94d8-ef0fab71c1d1.png)


Patch:
diff --git a/.taprc b/.taprc
index a4b6f3f..d943d34 100644
--- a/.taprc
+++ b/.taprc
@@ -1,6 +1,8 @@
-esm: false
 ts: false
 jsx: false
 flow: false
 timeout: 900
-coverage: true
+branches: 86
+functions: 84
+lines: 87
+statements: 87
\ No newline at end of file
diff --git a/autocannon.js b/autocannon.js
index 0c534ae..e3ed8e3 100755
--- a/autocannon.js
+++ b/autocannon.js
@@ -239,9 +239,17 @@ function start (argv) {
       })
     })
   } else {
-    initJob(argv).catch((err) => {
+    // if forever is true then a promise is not returned and we need to try ... catch errors
+    try {
+      const tracker = initJob(argv)
+      if (tracker.then) {
+        tracker.catch((err) => {
+          console.error(err.message)
+        })
+      }
+    } catch (err) {
       console.error(err.message)
-    })
+    }
   }
 }
 
diff --git a/lib/validate.js b/lib/validate.js
index 2a69fd1..2cdfec1 100644
--- a/lib/validate.js
+++ b/lib/validate.js
@@ -11,7 +11,7 @@ const isValidFn = (opt) => (!opt || typeof opt === 'function' || typeof opt ===
 
 const lessThanOneError = (label) => new Error(`${label} can not be less than 1`)
 const greaterThanZeroError = (label) => new Error(`${label} must be greater than 0`)
-const minIfPresent = (val, min) => val && val < min
+const minIfPresent = (val, min) => val !== null && val < min
 
 function safeRequire (path) {
   if (typeof path === 'string') {
@@ -83,8 +83,15 @@ module.exports = function validateOpts (opts, cbPassedIn) {
   }
 
   if (typeof opts.duration === 'string') {
-    if (/[a-zA-Z]/.exec(opts.duration)) opts.duration = timestring(opts.duration)
-    else opts.duration = Number(opts.duration.trim())
+    if (/[a-zA-Z]/.exec(opts.duration)) {
+      try {
+        opts.duration = timestring(opts.duration)
+      } catch (error) {
+        return error
+      }
+    } else {
+      opts.duration = Number(opts.duration.trim())
+    }
   }
 
   if (typeof opts.duration !== 'number') {
diff --git a/package.json b/package.json
index 448c1c8..4fdec5b 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "autocannon",
-  "version": "7.0.5",
+  "version": "7.1.0",
   "description": "Fast HTTP benchmarking tool written in Node.js",
   "main": "autocannon.js",
   "bin": {
@@ -45,7 +45,7 @@
     "sinon": "^10.0.1",
     "split2": "^3.2.2",
     "standard": "^16.0.3",
-    "tap": "^14.10.8"
+    "tap": "^15.0.2"
   },
   "dependencies": {
     "chalk": "^4.1.0",


Test patch:
diff --git a/test/basic-auth.test.js b/test/basic-auth.test.js
index ad61c81..249445f 100644
--- a/test/basic-auth.test.js
+++ b/test/basic-auth.test.js
@@ -40,7 +40,7 @@ const child = childProcess.spawn(process.execPath, [path.join(__dirname, '..'),
   detached: false
 })
 
-t.tearDown(() => {
+t.teardown(() => {
   child.kill()
 })
 
diff --git a/test/cli-ipc.test.js b/test/cli-ipc.test.js
index 3080e53..f6a1226 100644
--- a/test/cli-ipc.test.js
+++ b/test/cli-ipc.test.js
@@ -38,7 +38,7 @@ if (!win) {
 }
 
 t.autoend(false)
-t.tearDown(function () {
+t.teardown(function () {
   child.kill()
 })
 
diff --git a/test/cli.test.js b/test/cli.test.js
index ac2c5c2..4e6f562 100644
--- a/test/cli.test.js
+++ b/test/cli.test.js
@@ -44,7 +44,7 @@ test('should run benchmark against server', (t) => {
     detached: false
   })
 
-  t.tearDown(() => {
+  t.teardown(() => {
     child.kill()
   })
 
@@ -97,7 +97,7 @@ test('should parse HAR file and run requests', (t) => {
     detached: false
   })
 
-  t.tearDown(() => {
+  t.teardown(() => {
     child.kill()
   })
 
@@ -121,7 +121,7 @@ test('should throw on unknown HAR file', (t) => {
     detached: false
   })
 
-  t.tearDown(() => {
+  t.teardown(() => {
     child.kill()
   })
 
@@ -150,7 +150,7 @@ test('should throw on invalid HAR file', (t) => {
     detached: false
   })
 
-  t.tearDown(() => {
+  t.teardown(() => {
     child.kill()
   })
 
@@ -182,7 +182,7 @@ test('should write warning about unused HAR requests', (t) => {
     detached: false
   })
 
-  t.tearDown(() => {
+  t.teardown(() => {
     child.kill()
   })
 
@@ -234,7 +234,7 @@ test('run with workers', { skip: !hasWorkerSupport }, (t) => {
     detached: false
   })
 
-  t.tearDown(() => {
+  t.teardown(() => {
     child.kill()
   })
 
diff --git a/test/envPort.test.js b/test/envPort.test.js
index b315408..93da904 100644
--- a/test/envPort.test.js
+++ b/test/envPort.test.js
@@ -43,7 +43,7 @@ const child = childProcess.spawn(process.execPath, [path.join(__dirname, '..'),
   detached: false
 })
 
-t.tearDown(() => {
+t.teardown(() => {
   child.kill()
 })
 
diff --git a/test/httpClient.test.js b/test/httpClient.test.js
index 9c12ffd..5f33eac 100644
--- a/test/httpClient.test.js
+++ b/test/httpClient.test.js
@@ -98,7 +98,7 @@ test('client calls a tls server without SNI servername twice', (t) => {
 
   client.on('headers', (response) => {
     t.equal(response.statusCode, 200, 'status code matches')
-    t.deepEqual(response.headers, ['X-servername', '', 'Content-Length', '0'])
+    t.same(response.headers, ['X-servername', '', 'Content-Length', '0'])
     if (count++ > 0) {
       client.destroy()
     }
@@ -116,7 +116,7 @@ test('client calls a tls server with SNI servername twice', (t) => {
 
   client.on('headers', (response) => {
     t.equal(response.statusCode, 200, 'status code matches')
-    t.deepEqual(response.headers, ['X-servername', opts.servername, 'Content-Length', '0'])
+    t.same(response.headers, ['X-servername', opts.servername, 'Content-Length', '0'])
     if (count++ > 0) {
       client.destroy()
     }
@@ -134,7 +134,7 @@ test('client uses SNI servername from URL hostname by default', (t) => {
 
   client.on('headers', (response) => {
     t.equal(response.statusCode, 200, 'status code matches')
-    t.deepEqual(response.headers, ['X-servername', opts.hostname, 'Content-Length', '0'])
+    t.same(response.headers, ['X-servername', opts.hostname, 'Content-Length', '0'])
     if (count++ > 0) {
       client.destroy()
     }
@@ -153,7 +153,7 @@ test('client prefers SNI servername from opts over URL hostname', (t) => {
 
   client.on('headers', (response) => {
     t.equal(response.statusCode, 200, 'status code matches')
-    t.deepEqual(response.headers, ['X-servername', opts.servername, 'Content-Length', '0'])
+    t.same(response.headers, ['X-servername', opts.servername, 'Content-Length', '0'])
     if (count++ > 0) {
       client.destroy()
     }
@@ -171,7 +171,7 @@ test('client ignores IP address in hostname-derived SNI servername', (t) => {
 
   client.on('headers', (response) => {
     t.equal(response.statusCode, 200, 'status code matches')
-    t.deepEqual(response.headers, ['X-servername', '', 'Content-Length', '0'])
+    t.same(response.headers, ['X-servername', '', 'Content-Length', '0'])
     if (count++ > 0) {
       client.destroy()
     }
@@ -189,7 +189,7 @@ test('client ignores falsy SNI servername', (t) => {
 
   client.on('headers', (response) => {
     t.equal(response.statusCode, 200, 'status code matches')
-    t.deepEqual(response.headers, ['X-servername', '', 'Content-Length', '0'])
+    t.same(response.headers, ['X-servername', '', 'Content-Length', '0'])
     if (count++ > 0) {
       client.destroy()
     }
@@ -211,7 +211,7 @@ test('client passes through tlsOptions to connect', (t) => {
 
   client.on('headers', (response) => {
     t.equal(response.statusCode, 200, 'status code matches')
-    t.deepEqual(response.headers, ['X-servername', '', 'X-email', 'tes@test.com', 'Content-Length', '0'])
+    t.same(response.headers, ['X-servername', '', 'X-email', 'tes@test.com', 'Content-Length', '0'])
     if (count++ > 0) {
       client.destroy()
     }
@@ -362,7 +362,7 @@ test('client supports sending a body', (t) => {
   server.once('request', (req, res) => {
     req.pipe(bl((err, body) => {
       t.error(err)
-      t.deepEqual(body.toString(), opts.body.toString(), 'body matches')
+      t.same(body.toString(), opts.body.toString(), 'body matches')
     }))
   })
 
@@ -385,7 +385,7 @@ test('client supports sending a body which is a string', (t) => {
   server.once('request', (req, res) => {
     req.pipe(bl((err, body) => {
       t.error(err)
-      t.deepEqual(body.toString(), opts.body, 'body matches')
+      t.same(body.toString(), opts.body, 'body matches')
     }))
   })
 
@@ -718,21 +718,21 @@ test('client exposes response bodies and statuses', (t) => {
     switch (number) {
       case 1:
         t.same(client.getRequestBuffer().toString(), makeResponseFromBody({ server, ...opts.requests[0] }), 'first request')
-        t.deepEqual(responses, [{
+        t.same(responses, [{
           status: 200,
           body: 'hello!'
         }])
         break
       case 2:
         t.same(client.getRequestBuffer().toString(), makeResponseFromBody({ server, ...opts.requests[1] }), 'second request')
-        t.deepEqual(responses, [{
+        t.same(responses, [{
           status: 200,
           body: 'hello!'
         }])
         break
       case 3:
         t.same(client.getRequestBuffer().toString(), makeResponseFromBody({ server, ...opts.requests[2] }), 'third request')
-        t.deepEqual(responses, [{
+        t.same(responses, [{
           status: 200,
           body: 'hello!'
         }, {
@@ -742,7 +742,7 @@ test('client exposes response bodies and statuses', (t) => {
         break
       case 4:
         t.same(client.getRequestBuffer().toString(), makeResponseFromBody({ server, ...opts.requests[0] }), 'first request')
-        t.deepEqual(responses, [{
+        t.same(responses, [{
           status: 200,
           body: 'hello!'
         }, {
@@ -771,7 +771,7 @@ test('client keeps context and reset it when looping on requests', (t) => {
       body: 'hello world again',
       onResponse: (status, body, context) => {
         if (number < 3) {
-          t.deepEqual(context, {}, 'context was supposed to be null')
+          t.same(context, {}, 'context was supposed to be null')
           context.previousRes = body
         }
       }
@@ -780,7 +780,7 @@ test('client keeps context and reset it when looping on requests', (t) => {
       method: 'PUT',
       setupRequest: (req, context) => {
         if (number < 3) {
-          t.deepEqual(context, { previousRes: expectedResponse }, 'context was supposed to contain previous response')
+          t.same(context, { previousRes: expectedResponse }, 'context was supposed to contain previous response')
         }
         return Object.assign({}, req, { body: context.previousRes })
       }
@@ -913,22 +913,22 @@ test('client invokes appropriate onResponse when using pipelining', (t) => {
       case 1:
         // 1st & 2nd were sent, receiving 1st
         t.same(client.getRequestBuffer().toString(), makeResponseFromBody({ server, method: 'GET' }), 'current should be second request')
-        t.deepEqual(responses, ['POST'])
+        t.same(responses, ['POST'])
         break
       case 2:
         // 3rd was sent as 1st is finished, receiving 2nd
         t.same(client.getRequestBuffer().toString(), makeResponseFromBody({ server, method: 'PUT' }), 'current should be third request')
-        t.deepEqual(responses, ['POST', 'GET'])
+        t.same(responses, ['POST', 'GET'])
         break
       case 3:
         // 1st was resent, receiving 3rd
         t.same(client.getRequestBuffer().toString(), makeResponseFromBody({ server, method: 'POST' }), 'current should be first request')
-        t.deepEqual(responses, ['POST', 'GET', 'PUT'])
+        t.same(responses, ['POST', 'GET', 'PUT'])
         break
       case 4:
         // 2nd was resent, receiving 1st
         t.same(client.getRequestBuffer().toString(), makeResponseFromBody({ server, method: 'GET' }), 'current should be second request')
-        t.deepEqual(responses, ['POST', 'GET', 'PUT', 'POST'])
+        t.same(responses, ['POST', 'GET', 'PUT', 'POST'])
         client.destroy()
         t.end()
         break
diff --git a/test/httpRequestBuilder.test.js b/test/httpRequestBuilder.test.js
index ded68b3..76245fd 100644
--- a/test/httpRequestBuilder.test.js
+++ b/test/httpRequestBuilder.test.js
@@ -154,6 +154,6 @@ test('should throw error if body is not a string or a buffer', (t) => {
   try {
     build({ body: [] })
   } catch (error) {
-    t.is(error.message, 'body must be either a string or a buffer')
+    t.equal(error.message, 'body must be either a string or a buffer')
   }
 })
diff --git a/test/onPort.test.js b/test/onPort.test.js
index 60a82b6..17012c9 100644
--- a/test/onPort.test.js
+++ b/test/onPort.test.js
@@ -44,7 +44,7 @@ test('--on-port flag', { skip: !hasAsyncHooks() }, (t) => {
     detached: false
   })
 
-  t.tearDown(() => {
+  t.teardown(() => {
     child.kill()
   })
 
diff --git a/test/pipelinedRequestsQueue.test.js b/test/pipelinedRequestsQueue.test.js
index 7d20f00..3728ac4 100644
--- a/test/pipelinedRequestsQueue.test.js
+++ b/test/pipelinedRequestsQueue.test.js
@@ -68,7 +68,7 @@ test('PipelinedRequestsQueue methods set values to the request in first-in-last-
   const req1 = queue.terminateRequest()
   t.equal(req1.req, 1)
   t.equal(req1.body, '1')
-  t.deepEqual(req1.headers, { val: '1' })
+  t.same(req1.headers, { val: '1' })
 
   queue.addBody('2')
   queue.addByteCount(2)
@@ -77,5 +77,5 @@ test('PipelinedRequestsQueue methods set values to the request in first-in-last-
   const req2 = queue.terminateRequest()
   t.equal(req2.req, 2)
   t.equal(req2.body, '2')
-  t.deepEqual(req2.headers, { val: '2' })
+  t.same(req2.headers, { val: '2' })
 })
diff --git a/test/printResult-renderStatusCodes.test.js b/test/printResult-renderStatusCodes.test.js
index b830329..5dd0a3a 100644
--- a/test/printResult-renderStatusCodes.test.js
+++ b/test/printResult-renderStatusCodes.test.js
@@ -45,7 +45,7 @@ test('should stdout (print) the result', (t) => {
     detached: false
   })
 
-  t.tearDown(() => {
+  t.teardown(() => {
     child.kill()
   })
 
diff --git a/test/printResult.test.js b/test/printResult.test.js
index ff9c296..ccc81ec 100644
--- a/test/printResult.test.js
+++ b/test/printResult.test.js
@@ -34,7 +34,7 @@ test('should stdout (print) the result', (t) => {
     detached: false
   })
 
-  t.tearDown(() => {
+  t.teardown(() => {
     child.kill()
   })
 
diff --git a/test/progressTracker.test.js b/test/progressTracker.test.js
index 397a88a..38f4714 100644
--- a/test/progressTracker.test.js
+++ b/test/progressTracker.test.js
@@ -19,7 +19,7 @@ test(`should not emit warnings when using >= ${defaultMaxListeners} workers`, {
 
   setTimeout(() => {
     instance.stop()
-    t.false(emitWarningSpy.called)
+    t.notOk(emitWarningSpy.called)
     emitWarningSpy.restore()
     t.end()
   }, 2000)
diff --git a/test/requestIterator.test.js b/test/requestIterator.test.js
index 0b4bc4f..fac1860 100644
--- a/test/requestIterator.test.js
+++ b/test/requestIterator.test.js
@@ -203,7 +203,7 @@ test('request iterator should invoke onResponse callback when set', (t) => {
       onResponse: (status, body, context) => {
         t.same(status, 200)
         t.same(body, 'ok')
-        t.deepEqual(context, {})
+        t.same(context, {})
       }
     },
     {},
@@ -211,7 +211,7 @@ test('request iterator should invoke onResponse callback when set', (t) => {
       onResponse: (status, body, context) => {
         t.same(status, 201)
         t.same(body, '')
-        t.deepEqual(context, {})
+        t.same(context, {})
       }
     }
   ]
@@ -296,28 +296,28 @@ test('request iterator should reset when setupRequest returns nothing', (t) => {
   const requestPOST = `POST / HTTP/1.1\r\nHost: localhost:${server.address().port}\r\nConnection: keep-alive\r\nContent-Length: 11\r\n\r\nhello world`
 
   const iterator = new RequestIterator(opts)
-  t.is(iterator.resetted, false)
+  t.equal(iterator.resetted, false)
   // first GET, i is 0
   t.same(iterator.currentRequest.requestBuffer.toString(), requestGET, 'request 1 was okay')
   iterator.nextRequest()
   // first POST, i becomes 1
-  t.is(iterator.resetted, false)
+  t.equal(iterator.resetted, false)
   t.same(iterator.currentRequest.requestBuffer.toString(), requestPOST, 'request 2 was okay')
   iterator.nextRequest()
   // first PUT, i is 1
-  t.is(iterator.resetted, false)
+  t.equal(iterator.resetted, false)
   t.same(iterator.currentRequest.requestBuffer.toString(), requestPUT, 'request 3 was okay')
   iterator.nextRequest()
   // second GET, i is 1
-  t.is(iterator.resetted, false)
+  t.equal(iterator.resetted, false)
   t.same(iterator.currentRequest.requestBuffer.toString(), requestGET, 'request 4 was okay')
   iterator.nextRequest()
   // second POST, i becomes 2, pipeline is reset
-  t.is(iterator.resetted, true)
+  t.equal(iterator.resetted, true)
   t.same(iterator.currentRequest.requestBuffer.toString(), requestGET, 'request 5 was okay')
   iterator.nextRequest()
   // third POST, i becomes 3, pipeline is reset
-  t.is(iterator.resetted, true)
+  t.equal(iterator.resetted, true)
   t.same(iterator.currentRequest.requestBuffer.toString(), requestGET, 'request 6 was okay')
 })
 
@@ -332,11 +332,11 @@ test('request iterator should throw when first setupRequest returns nothing', (t
   const requestPOST = `POST / HTTP/1.1\r\nHost: localhost:${server.address().port}\r\nConnection: keep-alive\r\nContent-Length: 11\r\n\r\nhello world`
 
   const iterator = new RequestIterator(opts)
-  t.is(iterator.resetted, false)
+  t.equal(iterator.resetted, false)
   // first POST, i is 0
   t.same(iterator.currentRequest.requestBuffer.toString(), requestPOST, 'request 1 was okay')
   t.throws(() => iterator.nextRequest(), 'First setupRequest() failed did not returned valid request. Stopping')
-  t.is(iterator.resetted, false)
+  t.equal(iterator.resetted, false)
 })
 
 test('request iterator should maintain context while looping on requests', (t) => {
@@ -346,7 +346,7 @@ test('request iterator should maintain context while looping on requests', (t) =
   opts.requests = [
     {
       setupRequest: (req, context) => {
-        t.deepEqual(context, {}, 'context was supposed to be empty for first request')
+        t.same(context, {}, 'context was supposed to be empty for first request')
         context.num = 1
         context.init = true
         return req
@@ -354,14 +354,14 @@ test('request iterator should maintain context while looping on requests', (t) =
     },
     {
       setupRequest: (req, context) => {
-        t.deepEqual(context, { num: 1, init: true }, 'context was supposed to be initialized for second request')
+        t.same(context, { num: 1, init: true }, 'context was supposed to be initialized for second request')
         context.num++
         return req
       }
     },
     {
       setupRequest: (req, context) => {
-        t.deepEqual(context, { num: 2, init: true }, 'context was supposed to be initialized for third request')
+        t.same(context, { num: 2, init: true }, 'context was supposed to be initialized for third request')
         context.num++
         return req
       }
@@ -417,14 +417,14 @@ test('request iterator should initialize context from options', (t) => {
   opts.requests = [
     {
       setupRequest: (req, context) => {
-        t.deepEqual(context, { foo: 'bar' }, 'context should be initialized from opts')
+        t.same(context, { foo: 'bar' }, 'context should be initialized from opts')
         context.baz = 'qux'
         return req
       }
     },
     {
       setupRequest: (req, context) => {
-        t.deepEqual(context, { foo: 'bar', baz: 'qux' }, 'context should contain updated data')
+        t.same(context, { foo: 'bar', baz: 'qux' }, 'context should contain updated data')
         return req
       }
     }
diff --git a/test/run.test.js b/test/run.test.js
index 2d5a1ea..80a6d51 100644
--- a/test/run.test.js
+++ b/test/run.test.js
@@ -255,7 +255,7 @@ test('run should handle context correctly', (t) => {
     initialContext: { init: 'context' },
     requests: [{
       setupRequest: (req, context) => {
-        t.deepEqual(context, { init: 'context' }, 'context should be initialized from opts')
+        t.same(context, { init: 'context' }, 'context should be initialized from opts')
         return req
       }
     }]
@@ -414,7 +414,7 @@ test('run should not modify default options', (t) => {
     duration: 2
   }, function (err, result) {
     t.error(err)
-    t.deepEqual(defaultOptions, origin, 'calling run function does not modify default options')
+    t.same(defaultOptions, origin, 'calling run function does not modify default options')
     t.end()
   })
 })
@@ -524,7 +524,7 @@ test('tracker will emit reqMismatch when body does not match expectBody', (t) =>
 
   tracker.once('reqMismatch', (bodyStr) => {
     t.equal(bodyStr, responseBody)
-    t.notEqual(bodyStr, expectBody)
+    t.not(bodyStr, expectBody)
     tracker.stop()
   })
 })
@@ -637,7 +637,7 @@ test('should throw if duration is not a number nor a string', t => {
       t.fail()
     })
     .catch((err) => {
-      t.is(err.message, 'duration entered was in an invalid format')
+      t.equal(err.message, 'duration entered was in an invalid format')
     })
 })
 
@@ -658,7 +658,7 @@ test('should emit error', t => {
   })
 
   tracker.once('error', (error) => {
-    t.is(error.message, 'A \'type\' key with value \'text\' or \'file\' should be specified')
+    t.equal(error.message, 'A \'type\' key with value \'text\' or \'file\' should be specified')
     t.end()
   })
 })
@@ -676,7 +676,7 @@ test('should throw if timeout is less than zero', t => {
       t.fail()
     })
     .catch((err) => {
-      t.is(err.message, 'timeout must be greater than 0')
+      t.equal(err.message, 'timeout must be greater than 0')
     })
 })
 
@@ -710,7 +710,7 @@ test('should count resets', t => {
       }
     ]
   }).then((result) => {
-    t.is(result.resets, 4)
+    t.equal(result.resets, 4)
     t.end()
   })
 })
diff --git a/test/runMultipart.test.js b/test/runMultipart.test.js
index bb03028..f28dfb7 100644
--- a/test/runMultipart.test.js
+++ b/test/runMultipart.test.js
@@ -55,7 +55,7 @@ test('run should return an error with invalid form options', async t => {
   ]
 
   const server = helper.startMultipartServer()
-  t.tearDown(() => server.close())
+  t.teardown(() => server.close())
 
   for (const c of cases) {
     t.test(c.name, async t => {
@@ -70,7 +70,7 @@ test('run should return an error with invalid form options', async t => {
         })
       })
       await t.test('error', t => {
-        t.notEqual(null, err)
+        t.not(null, err)
         t.equal(c.message, err.message, `mismatching error message ${err.message}`)
         t.end()
       })
@@ -117,7 +117,7 @@ test('run should take form options as a JSON string or a JS Object', async t =>
         t.equal('j5', payload.name)
         t.equal('j5.jpeg', payload.image.filename)
       })
-      t.tearDown(() => server.close())
+      t.teardown(() => server.close())
       const [err, res] = await new Promise((resolve) => {
         initJob({
           url: 'http://localhost:' + server.address().port,
@@ -145,7 +145,7 @@ test('run should use a custom method if `options.method` is passed', t => {
     t.equal('j5', payload.name)
     t.equal('j5.jpeg', payload.image.filename)
   })
-  t.tearDown(() => server.close())
+  t.teardown(() => server.close())
 
   const form = {
     image: {
@@ -177,7 +177,7 @@ test('run should set filename', t => {
     t.equal('j5', payload.name)
     t.equal('j5.jpeg', payload.image.filename)
   })
-  t.tearDown(() => server.close())
+  t.teardown(() => server.close())
 
   const form = {
     image: {
@@ -209,7 +209,7 @@ test('run should allow overriding filename', t => {
     t.equal('j5', payload.name)
     t.equal('testfilename.jpeg', payload.image.filename)
   })
-  t.tearDown(() => server.close())
+  t.teardown(() => server.close())
 
   const form = {
     image: {
@@ -244,7 +244,7 @@ test('run should allow overriding filename with file path', t => {
     t.equal('j5', payload.name)
     t.equal('some/path/testfilename.jpeg', payload.image.filename)
   })
-  t.tearDown(() => server.close())
+  t.teardown(() => server.close())
 
   const form = {
     image: {
diff --git a/test/runRate.test.js b/test/runRate.test.js
index d8a3997..6f57c94 100644
--- a/test/runRate.test.js
+++ b/test/runRate.test.js
@@ -61,7 +61,7 @@ test('run should compensate for coordinated omission when the expected number of
     duration: 1
   }, (err, res) => {
     t.error(err)
-    t.notEqual(res.latency.totalCount, res.requests.total, 'should have recorded additionnal latencies')
+    t.not(res.latency.totalCount, res.requests.total, 'should have recorded additionnal latencies')
   })
 })
 
diff --git a/test/serial/autocannon.test.js b/test/serial/autocannon.test.js
index e02ea8d..d98d853 100644
--- a/test/serial/autocannon.test.js
+++ b/test/serial/autocannon.test.js
@@ -19,12 +19,12 @@ test('should print error if url.URL is not a function', t => {
   const _exit = process.exit
 
   process.exit = (code) => {
-    t.is(code, 1)
+    t.equal(code, 1)
     process.exit = _exit
     t.end()
   }
   console.error = (obj) => {
-    t.is(
+    t.equal(
       obj,
       'autocannon requires the WHATWG URL API, but it is not available. Please upgrade to Node 6.13+.'
     )
@@ -56,3 +56,158 @@ test('should print help if no url is provided', async t => {
   const res = await exec(`node ${baseDir}/autocannon.js`)
   t.same(res.stderr.trim(), help.trim()) // console.error adds \n at the end of print
 })
+
+test('start should console an error when a promise is caught', (t) => {
+  const Autocannon = t.mock('../../autocannon', {
+    '../../lib/init': () => new Promise((resolve, reject) => {
+      reject(new Error('Test Error'))
+    })
+  })
+
+  t.plan(1)
+
+  const _error = console.error
+  console.error = (obj) => {
+    t.equal(
+      obj,
+      'Test Error'
+    )
+    console.error = _error
+  }
+
+  Autocannon.start(
+    Autocannon.parseArguments([
+      '-d', '1',
+      '-c', '1',
+      'http://localhost/foo/bar'
+    ])
+  )
+})
+
+test('start should console an error when one is thrown without a promise', (t) => {
+  const Autocannon = t.mock('../../autocannon', {
+    '../../lib/init': () => { throw new Error('Test Error') }
+  })
+
+  t.plan(1)
+
+  const _error = console.error
+  console.error = (obj) => {
+    t.equal(
+      obj,
+      'Test Error'
+    )
+    console.error = _error
+  }
+
+  Autocannon.start(
+    Autocannon.parseArguments([
+      '-d', '1',
+      '-c', '1',
+      '--forever',
+      'http://localhost/foo/bar'
+    ])
+  )
+})
+
+test('start should console an error when --on-port is used without async hooks', (t) => {
+  const Autocannon = t.mock('../../autocannon', {
+    'has-async-hooks': () => false,
+    child_process: {
+      spawn: () => {}
+    },
+    '../../lib/init': () => {},
+    net: {
+      createServer: () => ({
+        listen: () => {},
+        on: () => {}
+      })
+    }
+  })
+
+  t.plan(2)
+
+  const _exit = process.exit
+  process.exit = (code) => {
+    t.equal(code, 1)
+    process.exit = _exit
+    t.end()
+  }
+  const _error = console.error
+  console.error = (obj) => {
+    t.equal(
+      obj,
+      'The --on-port flag requires the async_hooks builtin module, but it is not available. Please upgrade to Node 8.1+.'
+    )
+    console.error = _error
+  }
+
+  Autocannon.start(
+    Autocannon.parseArguments([
+      '-d', '1',
+      '-c', '1',
+      '--on-port',
+      'http://localhost/foo/bar'
+    ])
+  )
+})
+
+test('When there is a port, createChannel should try to unlink the socketPath', (t) => {
+  const Autocannon = t.mock('../../autocannon', {
+    'has-async-hooks': () => true,
+    child_process: {
+      spawn: () => {}
+    },
+    '../../lib/init': () => {},
+    net: {
+      createServer: () => ({
+        listen: () => {},
+        on: (eventName, cb) => {
+          t.equal(eventName, 'close')
+          cb()
+        }
+      })
+    }
+  })
+
+  t.plan(1)
+
+  Autocannon.start(
+    Autocannon.parseArguments([
+      '-d', '1',
+      '-c', '1',
+      '--on-port',
+      'http://localhost/foo/bar'
+    ])
+  )
+})
+
+test('createChannel should try to unlink the socketPath', (t) => {
+  const Autocannon = t.mock('../../autocannon', {
+    'has-async-hooks': () => true,
+    child_process: {
+      spawn: () => {}
+    },
+    '../../lib/init': () => {},
+    net: {
+      createServer: () => ({
+        listen: () => {},
+        on: (_, cb) => {
+          t.equal(_, 'close')
+          cb()
+        }
+      })
+    }
+  })
+
+  t.plan(1)
+
+  Autocannon.start(
+    Autocannon.parseArguments([
+      '-d', '1',
+      '-c', '1',
+      '--on-port',
+      'http://localhost/foo/bar'
+    ])
+  )
+})
diff --git a/test/url.test.js b/test/url.test.js
new file mode 100644
index 0000000..b70cb30
--- /dev/null
+++ b/test/url.test.js
@@ -0,0 +1,62 @@
+'use strict'
+
+const test = require('tap').test
+// const { ofUrl, checkUrl } = require('../lib/url')
+const { ofURL, checkURL } = require('../lib/url')
+
+test('checkURL should return true if a populated string is passed', (t) => {
+  t.plan(1)
+
+  const result = checkURL('foo')
+  t.ok(result)
+})
+
+test('checkURL should return false if an empty string is passed', (t) => {
+  t.plan(1)
+
+  const result = checkURL('')
+  t.notOk(result)
+})
+
+test('checkURL should return true if a populated array is passed', (t) => {
+  t.plan(1)
+
+  const result = checkURL(['foo'])
+  t.ok(result)
+})
+
+test('checkURL should return false if an empty array is passed', (t) => {
+  t.plan(1)
+
+  const result = checkURL([])
+  t.notOk(result)
+})
+
+test('ofUrl should return the array if the passed in url is an array', (t) => {
+  t.plan(1)
+
+  const result = ofURL(['foo', 'bar'])
+  t.same(result, ['foo', 'bar'])
+})
+
+test('When ofUrl is passed a string ofUrl should return an object containing a map function that accepts an url', (t) => {
+  t.plan(1)
+
+  const result = ofURL('foo', false)
+  const mappedResult = result.map((url) => url)
+  t.same(mappedResult, 'foo')
+})
+
+test('When ofUrl is passed a string and asArray=true ofUrl should return an object containing a map function that returns an array', (t) => {
+  t.plan(1)
+
+  const result = ofURL('foo', true)
+  const mappedResult = result.map((url) => url)
+  t.same(mappedResult, ['foo'])
+})
+
+test('ofUrl Should throw an error when passed an invalid url type', (t) => {
+  t.plan(1)
+
+  t.throws(() => ofURL(123), 'url should only be a string or an array of string')
+})
diff --git a/test/validate.test.js b/test/validate.test.js
new file mode 100644
index 0000000..eed3fd9
--- /dev/null
+++ b/test/validate.test.js
@@ -0,0 +1,230 @@
+'use strict'
+
+const test = require('tap').test
+const validateOpts = require('../lib/validate')
+const helper = require('./helper')
+const { hasWorkerSupport } = require('../lib/util')
+
+test('validateOpts should not return an error with only an url passed in', (t) => {
+  t.plan(1)
+
+  const result = validateOpts({ url: 'http://localhost' })
+  t.ok(!(result instanceof Error))
+})
+
+test('validateOpts should return an error when workers option is present and hasWorkerSupport is false', (t) => {
+  const validateOpts = t.mock('../lib/validate', {
+    '../lib/util': { hasWorkerSupport: false }
+  })
+  t.plan(2)
+
+  const result = validateOpts({ workers: 1 })
+  t.ok(result instanceof Error)
+  t.equal(result.message, 'Please use node >= 11.7.0 for workers support')
+})
+
+test('validateOpts should return an error when bailout is less than 1', (t) => {
+  const validateOpts = t.mock('../lib/validate', {
+    '../lib/util': { hasWorkerSupport: false }
+  })
+  t.plan(2)
+
+  const result = validateOpts({ bailout: 0 })
+  t.ok(result instanceof Error)
+  t.equal(result.message, 'bailout threshold can not be less than 1')
+})
+
+test('validateOpts should return an error when connectionRate is less than 1', (t) => {
+  t.plan(2)
+
+  const result = validateOpts({ connectionRate: 0 })
+  t.ok(result instanceof Error)
+  t.equal(result.message, 'connectionRate can not be less than 1')
+})
+
+test('validateOpts should return an error when overallRate is less than 1', (t) => {
+  t.plan(2)
+
+  const result = validateOpts({ overallRate: 0 })
+  t.ok(result instanceof Error)
+  t.equal(result.message, 'bailout overallRate can not be less than 1')
+})
+
+test('validateOpts should return an error when amount is less than 1', (t) => {
+  t.plan(2)
+
+  const result = validateOpts({ amount: 0 })
+  t.ok(result instanceof Error)
+  t.equal(result.message, 'amount can not be less than 1')
+})
+
+test('validateOpts should return an error when maxConnectionRequests is less than 1', (t) => {
+  t.plan(2)
+
+  const result = validateOpts({ url: 'http://localhost', maxConnectionRequests: 0 })
+  t.ok(result instanceof Error)
+  t.equal(result.message, 'maxConnectionRequests can not be less than 1')
+})
+
+test('validateOpts should return an error when maxOverallRequests is less than 1', (t) => {
+  t.plan(2)
+
+  const result = validateOpts({ maxOverallRequests: 0 })
+  t.ok(result instanceof Error)
+  t.equal(result.message, 'maxOverallRequests can not be less than 1')
+})
+
+test('validateOpts should return an error when requests does not contain a valid setupRequest function', (t) => {
+  t.plan(2)
+
+  const result = validateOpts({ url: 'http://localhost', requests: [{ setupRequest: 123 }] })
+  t.ok(result instanceof Error)
+  t.equal(result.message, 'Invalid option setupRequest, please provide a function (or file path when in workers mode)')
+})
+
+test('validateOpts should return an error when requests does not contain a valid onResponse function', (t) => {
+  t.plan(2)
+
+  const result = validateOpts({ url: 'http://localhost', requests: [{ onResponse: 123 }] })
+  t.ok(result instanceof Error)
+  t.equal(result.message, 'Invalid option onResponse, please provide a function (or file path when in workers mode)')
+})
+
+test('validateOpts should return an error when setupClient is not a valid function', (t) => {
+  t.plan(2)
+
+  const result = validateOpts({ url: 'http://localhost', setupClient: 123 })
+  t.ok(result instanceof Error)
+  t.equal(result.message, 'Invalid option setupClient, please provide a function (or file path when in workers mode)')
+})
+
+test('validateOpts should return an error if neither url or socket path are provided', (t) => {
+  t.plan(2)
+
+  const result = validateOpts({ })
+  t.ok(result instanceof Error)
+  t.equal(result.message, 'url or socketPath option required')
+})
+
+test('validateOpts should convert a duration that is a string representation of a number into a number', (t) => {
+  t.plan(1)
+
+  const result = validateOpts({ url: 'http://localhost', duration: '100' })
+  t.equal(result.duration, 100)
+})
+
+test('validateOpts should convert a duration that is a timestring into a number', (t) => {
+  t.plan(1)
+
+  const result = validateOpts({ url: 'http://localhost', duration: '2 weeks' })
+  t.equal(result.duration, 1209600)
+})
+
+test('validateOpts should return an error if duration is in an invalid format', (t) => {
+  t.plan(1)
+
+  const result = validateOpts({ url: 'http://localhost', duration: '2 dsweeks' })
+  t.ok(result instanceof Error)
+})
+
+test('validateOpts should return an error if duration less than 0', (t) => {
+  t.plan(2)
+
+  const result = validateOpts({ url: 'http://localhost', duration: -1 })
+  t.ok(result instanceof Error)
+  t.equal(result.message, 'duration can not be less than 0')
+})
+
+test('validateOpts should return an error if expectBody is used in conjunction with requests', (t) => {
+  t.plan(2)
+
+  const result = validateOpts({ url: 'http://localhost', expectBody: 'foo', requests: [] })
+  t.ok(result instanceof Error)
+  t.equal(result.message, 'expectBody cannot be used in conjunction with requests')
+})
+
+test('validateOpts should parse a multipart form correctly', (t) => {
+  t.plan(1)
+
+  const result = validateOpts({ url: 'http://localhost', form: '{ "field 1": { "type": "text", "value": "a text value"} }' })
+  t.ok(result.form)
+})
+
+test('validateOpts should return an error if a multipart form is incorrectly formatted', (t) => {
+  t.plan(1)
+
+  const result = validateOpts({ url: 'http://localhost', form: 'invalid form' })
+  t.ok(result instanceof Error)
+})
+
+test('validateOpts should parse a HAR request successfully', (t) => {
+  t.plan(1)
+
+  const har = helper.customizeHAR('./fixtures/httpbin-get.json', 'https://httpbin.org', 'http://localhost')
+  const result = validateOpts({ url: 'http://localhost', har })
+  t.ok(result.har)
+})
+
+test('validateOpts should return an error if a HAR request is unsuccessful', (t) => {
+  t.plan(1)
+
+  const result = validateOpts({ url: 'http://localhost', har: 'invalid har' })
+  t.ok(result instanceof Error)
+})
+
+test('validateOpts should return an error when connections is less than 1', (t) => {
+  t.plan(2)
+
+  const result = validateOpts({ url: 'http://localhost', connections: 0 })
+  t.ok(result instanceof Error)
+  t.equal(result.message, 'connections can not be less than 1')
+})
+
+test('validateOpts should return an error when ignoreCoordinatedOmission used without connectionRate or overallRate', (t) => {
+  t.plan(2)
+
+  const result = validateOpts({
+    url: 'http://localhost',
+    ignoreCoordinatedOmission: true
+  })
+  t.ok(result instanceof Error)
+  t.equal(result.message, 'ignoreCoordinatedOmission makes no sense without connectionRate or overallRate')
+})
+
+test('validateOpts is successful when ignoreCoordinatedOmission is used with connectionRate', (t) => {
+  t.plan(1)
+
+  const result = validateOpts({
+    url: 'http://localhost',
+    ignoreCoordinatedOmission: true,
+    connectionRate: 1
+  })
+  t.ok(result.ignoreCoordinatedOmission)
+})
+
+test('validateOpts is successful when ignoreCoordinatedOmission is used with overallRate', (t) => {
+  t.plan(1)
+
+  const result = validateOpts({
+    url: 'http://localhost',
+    ignoreCoordinatedOmission: true,
+    overallRate: 1
+  })
+  t.ok(result.ignoreCoordinatedOmission)
+})
+
+test('validateOpts should return an error when forever is used with cbPassedIn', (t) => {
+  t.plan(2)
+
+  const result = validateOpts({ url: 'http://localhost', forever: true }, () => {})
+  t.ok(result instanceof Error)
+  t.equal(result.message, 'should not use the callback parameter when the `forever` option is set to true. Use the `done` event on this event emitter')
+})
+
+test('validateOpts should return an error when forever is used with workers', { skip: !hasWorkerSupport }, (t) => {
+  t.plan(2)
+
+  const result = validateOpts({ url: 'http://localhost', forever: true, workers: 2 })
+  t.ok(result instanceof Error)
+  t.equal(result.message, 'Using `forever` option isn\'t currently supported with workers')
+})