Test-Tube

test-tube is the testing component of the Carbon.io framework. It is used to test all components of Carbon.io (including test-tube itself) and can be used independently to test any JavaScript based project.

Test Environment Setup

The suggested way to structure your test environment is to create a directory named test that exists in your application’s root directory:

<path-to-your-app>/
  test/
    index.js

To use test-tube, you can access it via carbon-io as follows:

var testtube = require('carbon-io').testtube

Additionally, to run tests via npm, you should add the following scripts property to your package.json file:

{
  ...,
  "scripts": {
    "test": "node test"
  }
  ...
}

Test Suite Structure

Defining your test suite is pretty straight forward and for those familiar with traditional unit testing frameworks like Java’s JUnit or Python’s unittest module, things should feel reasonably comfortable.

With test-tube there is no difference between a “test” and a “test suite”, as the Test class acts as both. A basic test suite then might look something like the following:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
var carbon = require('carbon-io')

var __ = carbon.fibers.__(module)
var _o = carbon.bond._o(module)
var o = carbon.atom.o(module)
var testtube = carbon.testtube

__(function() {
  module.exports = o.main({
    _type: testtube.Test,
    name: 'FooBarBazTests',
    descriptions: 'Test all the foos, bars, and bazes.',
    tests: [
      _o('./foo'),
      _o('./bar'),
      _o('./baz')
    ]
  })
})

In the context of our “hello-world” application (see <test-tube-root>/docs/code-frags/hello-world) it looks as follows:

var carbon = require('carbon-io')

var __ = carbon.fibers.__(module)
var o = carbon.atom.o(module)
var _o = carbon.bond._o(module)
var testtube = carbon.testtube

 __(function() {
  module.exports = o.main({
    _type: testtube.Test,
    name: 'HelloWorldTestSuite',
    description: 'A test suite demonstrating Test-Tube\'s various features.',
    tests: [
      _o('./SimpleBareBonesTest'),
      _o('./SimpleTest'),
      _o('./SimpleAsyncTest'),
      _o('./SimpleTests'),
      _o('./SimpleNestedTests'),
      _o('./CmdlineTests'),
      _o('./ContextTests'),
      _o('./HttpTests'),
      _o('./SkipTests')
    ]
  })
})

With the corresponding directory hierarchy looking like:

$> tree test
test
├── CmdlineTests.js
├── ContextTests.js
├── HttpTests.js
├── SimpleAsyncTest.js
├── SimpleBareBonesTest.js
├── SimpleNestedTests.js
├── SimpleTest.js
├── SimpleTests.js
├── SkipTests.js
└── index.js

You may have noticed a bit a boiler plate in the previous two examples:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
var carbon = require('carbon-io')

var __ = carbon.fibers.__(module)
var _o = carbon.bond._o(module)
var o = carbon.atom.o(module)
var testtube = carbon.testtube

__(function() {
  module.exports = o.main({
    _type: testtube.Test,
    /*
     * Test implementation
     */
  })
})

This boiler plate code should wrap every test exported by a module in the tree, not just index.js (see the various tests in hello-world for examples). This accomplishes two things. First, it ensures that all setup within the wrapped test and its children run inside a Fiber. Second, it allows you to easily run any subtree of tests by passing a particular test module to node as the main module (e.g., if you just want to run hello-world’s command line tests, you would issue the command node test/cmdline-tests.js). Additionally, it should be noted that the main variant of both the __ and o operators are required (see documentation for @carbon-io/fibers and @carbon-io/atom for a more in-depth explanation).

Basic Tests

Synchronous

In its simplest form, a test in test-tube looks as follows:

o({
  _type: testtube.Test,
  doTest: function() {
    assert(<expression>)
  }
})

It consists of a instance of Test with doTest overriden to perform the actual test. To indicate a failure, simply throw an error. To indicate success, don’t.

Asynchronous

If a test is asynchronous, use the done errback to indicate the test has completed:

o({
  _type: testtube.Test,
  doTest: function(_, done) {
    asyncFunc(..., function(err, result) {
      if (!err) {
        try {
          assert(<expression>)
        } catch (e) {
          err = e
        }
      }
      done(err)
    })
  }
})

Fixtures

In addition to doTest, test classes have setup and teardown methods that can be used to create fixtures expected by the test and perform any cleanup required after the test has completed:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
var assert = require('assert')

var carbon = require('carbon-io')

var __ = carbon.fibers.__(module)
var o = carbon.atom.o(module)
var testtube = carbon.testtube

__(function() {
  module.exports = o.main({
    _type: testtube.Test,
    name: 'SimpleTest',
    description: 'A simple test',
    setup: function() {
      // setup the environment for the test
      process.env.foo = 1
    },
    teardown: function() {
      // clean up the environment for subsequent tests
      delete process.env.foo
    },
    doTest: function() {
      // test that foo is in the environment and that it is set to 1
      assert.equal(process.env.foo, 1)
    }
  })
})

Note, setup and teardown have the same signature as doTest, so, if something needs to be done asynchronously, simply use the supplied errback:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
var assert = require('assert')

var carbon = require('carbon-io')

var __ = carbon.fibers.__(module)
var o = carbon.atom.o(module)
var testtube = carbon.testtube

__(function() {
  module.exports = o.main({
    _type: testtube.Test,
    name: 'SimpleAsyncTest',
    description: 'A simple async test',
    setup: function(_, done) {
      setImmediate(function() {
        // setup the environment for the test
        process.env.foo = 1
        done()
      })
    },
    teardown: function(_, done) {
      setImmediate(function() {
        // clean up the environment for subsequent tests
        delete process.env.foo
        done()
      })
    },
    doTest: function(_, done) {
      setImmediate(function() {
        var err = undefined
        try {
          // test that foo is in the environment and that it is set to 1
          assert.equal(process.env.foo, 1)
        } catch (e) {
          err = e
        }
        done(err)
      })
    }
  })
})

Test Suites

To implement a test suite, simply override the tests property with an array of tests to execute:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
var assert = require('assert')

var carbon = require('carbon-io')

var __ = carbon.fibers.__(module)
var o = carbon.atom.o(module)
var testtube = carbon.testtube

__(function() {
  module.exports = o.main({
    _type: testtube.Test,
    name: 'SimpleTests',
    description: 'A simple set of tests',
    tests: [
      o({
        _type: testtube.Test,
        name: 'SimpleWithSetupAndTeardownTest',
        setup: function() {
          this.x = 1
        },
        doTest: function() {
          assert.equal(this.x, 1)
        },
        teardown: function() {
          delete this.x
        }
      }),
      o({
        _type: testtube.Test,
        name: 'SimpleAsyncTest',
        doTest: function(_, done) {
          setImmediate(function() {
            var err = undefined
            try {
              assert.equal(1, 1)
            } catch (e) {
              err = e
            }
            done(err)
          })
        }
      }),
      o({
        _type: testtube.Test,
        name: 'SimpleAsyncWithSetupAndTeardownTest',
        setup: function(_, done) {
          var self = this
          setImmediate(function() {
            self.x = 1
            done()
          })
        },
        doTest: function(_, done) {
          var self = this
          setImmediate(function() {
            var err = undefined
            try {
              assert.equal(self.x, 1)
            } catch (e) {
              err = e
            }
            done(err)
          })
        },
        teardown: function(_, done) {
          var self = this
          setImmediate(function() {
            delete self.x
            done()
          })
        }
      })
    ]
  })
})

Note, as mentioned previously, a test can act both as a test suite and a test. To do this, simply override doTest in addition to tests.

Back References

If access to the test suite is required by a test, the parent property can be used:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
__(function() {
  module.exports = o.main({
    _type: testtube.Test,
    name: 'SimpleNestedTests',
    description: 'A simple set of tests',
    setup: function() {
      assert(typeof this.level === 'undefined')
      this.level = 0
    },
    doTest: function() {
      assert.equal(this.level, 0)
    },
    teardown: function() {
      assert.equal(this.level, 0)
      delete this.level
    },
    tests: [
      o({
        _type: testtube.Test,
        name: 'SimpleNestedTest',
        setup: function() {
          this._oldLevel = this.parent.level
          this.parent.level += 1
        },
        doTest: function() {
          assert.equal(this.parent.level, this._oldLevel + 1)
        },
        teardown: function() {
          try {
            assert.equal(this.parent.level, this._oldLevel + 1)
          } finally {
            this.parent.level = this._oldLevel
          }
        },
        tests: [
          o({
            _type: testtube.Test,
            name: 'SimpleDoubleNestedAsyncTest',
            setup: function(_, done) {
              var self = this
              setImmediate(function() {
                self._oldLevel = self.parent.parent.level
                self.parent.parent.level += 1
                done()
              })
            },
            doTest: function(_, done) {
              var self = this
              setImmediate(function() {
                var err = undefined
                try {
                  assert.equal(self.parent.parent.level, self._oldLevel + 1)
                } catch (e) {
                  err = e
                }
                done(err)
              })
            },
            teardown: function(_, done) {
              var self = this
              setImmediate(function() {
                var err = undefined
                try {
                  assert.equal(self.parent.parent.level, self._oldLevel + 1)
                } catch (e) {
                  err = e
                } finally {
                  self.parent.parent.level = self._oldLevel
                }
                done(err)
              })
            },
            tests: [
              o({
                _type: testtube.Test,
                name: 'SimpleTripleNestedTest',
                setup: function() {
                  this._oldLevel = this.parent.parent.parent.level
                  this.parent.parent.parent.level += 1
                },
                doTest: function() {
                  assert.equal(
                    this.parent.parent.parent.level, this._oldLevel + 1)
                },
                teardown: function() {
                  try {
                    assert.equal(
                      this.parent.parent.parent.level, this._oldLevel + 1)
                  } finally {
                    this.parent.parent.parent.level = this._oldLevel
                  }
                }
              })
            ]
          })
        ]
      })
    ]
  })
})

Expecting an Error

If an error is expected to be thrown by a test, the errorExpected can be used to catch and verify an error that may be thrown anywhere in the defined test. If it is set to a boolean, the test class will simply verify that an error was thrown or not. If it is set to a class or function, it will behave like throws. This is mostly useful for meta-tests.

Reporting

Tests and their outcomes are reported as the test suite executes, with a final hierarchical test report generated at completion. For example, the output of running the “hello-world” test suite looks as follows:

$> npm test

  Running HelloWorldTestSuite...
    [*] Test (0ms)
    [*] SimpleTest (0ms)
    [*] SimpleAsyncTest (0ms)
    [*] SimpleWithSetupAndTeardownTest (0ms)
    [*] SimpleAsyncTest (1ms)
    [*] SimpleAsyncWithSetupAndTeardownTest (1ms)
    [*] SimpleTests (2ms)
    [*] SimpleTripleNestedTest (0ms)
    [*] SimpleDoubleNestedAsyncTest (0ms)
    [*] SimpleNestedTest (0ms)
    [*] SimpleNestedTests (0ms)
    [*] CmdlineTests (10ms)
    [*] SimpleContextTest (0ms)
    [*] SimpleNestedTestWithContextTest1 (1ms)
    [*] SimpleNestedTestWithContextTest2 (0ms)
    [*] SimpleNestedTestsWithContextTest (1ms)
    [*] SimpleAsyncContextTest (1ms)
    [*] SimpleContextTests (2ms)
    [*] GET /say (86ms)
    [*] NamedHttpTestWithSetupAndTeardown (16ms)
    [*] ReqResSpecFunctionTests (7ms)
    [*] ReqResSpecFunctionTests (6ms)
    [*] GET /say (5ms)
    [*] SimpleReverseHttpHistoryTest (9ms)
    [*] SimpleForwardHttpHistoryTest (4ms)
    [*] SimpleNamedHttpHistoryTest (10ms)
    [*] NestedNamedHttpTest (4ms)
    [*] undefined undefined (11ms)
    [*] NestedHttpTest (15ms)
    [*] SimpleNamedHttpHistoryTest2 (9ms)
    [*] GET undefined (60ms)
    [*] GET http://127.0.0.1:8888/say (8ms)
    [*] GET /say (8ms)
    [*] HttpTests (243ms)
    [*] Test SKIPPED (0ms)
    [*] SkipTest SKIPPED (0ms)
    [*] Test NOT IMPLEMENTED (1ms)
    [*] NotImplementedTest NOT IMPLEMENTED (0ms)
    [*] SkipTests (1ms)
    [*] HelloWorldTestSuite (258ms)

  Test Report
    [*] Test: HelloWorldTestSuite (A test suite demonstrating Test-Tube's various features.) (258ms)
    [*] Test: Test (0ms)
    [*] Test: SimpleTest (A simple test) (0ms)
    [*] Test: SimpleAsyncTest (A simple async test) (0ms)
    [*] Test: SimpleTests (A simple set of tests) (2ms)
      [*] Test: SimpleWithSetupAndTeardownTest (0ms)
      [*] Test: SimpleAsyncTest (1ms)
      [*] Test: SimpleAsyncWithSetupAndTeardownTest (1ms)
    [*] Test: SimpleNestedTests (A simple set of tests) (0ms)
      [*] Test: SimpleNestedTest (0ms)
        [*] Test: SimpleDoubleNestedAsyncTest (0ms)
          [*] Test: SimpleTripleNestedTest (0ms)
    [*] Test: CmdlineTests (10ms)
    [*] Test: SimpleContextTests (A simple set of tests using context) (2ms)
      [*] Test: SimpleContextTest (0ms)
      [*] Test: SimpleNestedTestsWithContextTest (1ms)
        [*] Test: SimpleNestedTestWithContextTest1 (1ms)
        [*] Test: SimpleNestedTestWithContextTest2 (0ms)
      [*] Test: SimpleAsyncContextTest (1ms)
    [*] Test: HttpTests (Http tests.) (243ms)
      [*] Test: GET /say (86ms)
      [*] Test: NamedHttpTestWithSetupAndTeardown (16ms)
      [*] Test: ReqResSpecFunctionTests (7ms)
      [*] Test: ReqResSpecFunctionTests (6ms)
      [*] Test: GET /say (5ms)
      [*] Test: SimpleReverseHttpHistoryTest (9ms)
      [*] Test: SimpleForwardHttpHistoryTest (4ms)
      [*] Test: SimpleNamedHttpHistoryTest (10ms)
      [*] Test: NestedHttpTest (15ms)
        [*] Test: NestedNamedHttpTest (4ms)
        [*] Test: undefined undefined (11ms)
      [*] Test: SimpleNamedHttpHistoryTest2 (9ms)
      [*] Test: GET undefined (60ms)
      [*] Test: GET http://127.0.0.1:8888/say (8ms)
      [*] Test: GET /say (8ms)
    [*] Test: SkipTests (Demonstrate how to skip tests.) (1ms)
      [*] Test: Test SKIPPED (0ms)
        Skipping test because of foo
      [*] Test: SkipTest SKIPPED (Skipping test because of foo) (0ms)
        Skipping test because of foo
      [*] Test: Test NOT IMPLEMENTED (1ms)
        Implement foo
      [*] Test: NotImplementedTest NOT IMPLEMENTED (Foo test not implemented) (0ms)
        Foo test not implemented

In order to make this report more descriptive, there are two other properties of Test that can be overridden: name and description. If name is not overriden, the test will be given the default name of Test (e.g., [*] Test: Test (0ms)). If description is overridden, it will be appended to the report output for that test in parentheses (e.g., [*] Test: SimpleTest (A simple test) (0ms)).

Additionally, the exit code will indicate success (0) or failure (>0) of the test suite as a whole.

Context

In order to make test suites reentrant, test-tube provides a context object that is passed down through the execution tree as the first argument to each test’s setup, teardown, and doTest mthods.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
      o({
        _type: testtube.Test,
        name: 'SimpleContextTest',
        setup: function(context) {
          assert(typeof context.local.testName === 'undefined')
          context.local.testName = this.name
        },
        teardown: function(context) {
          assert.equal(context.local.testName, this.name)
          assert.deepEqual(context.global.testNames, [
            'SimpleContextTest',
          ])
        },
        doTest: function(context) {
          assert.equal(context.local.testName, this.name)
          context.global.testNames.push(this.name)
        }
      }),

The test context class (see: TestContext) has three important/reserved properties: global, local, and httpHistory. local can be used to record any state relevant to the current test. If a test contains a tests property with more tests, the current state will be saved when execution passes to its children and restored after their completion. Alternatively, the test itself can be used (i.e., this.foo = bar) at the cost of the test suite being reentrant. httpHistory records all previously executed request/response pairs in a testtube.HttpTest (see: HttpTest).

In addition to maintaining state for the current test, TestContext can be used to communicate or collect other state throughout the test suite. Simply attach data to the context object’s global property and it will be passed down through the tree untouched by test-tube.

The following example demonstrates the use of local by saving the current test’s name on local in setup and verifying that it persists through teardown despite the presence of child tests. It also demonstrates the sharing of state using the context object by recording the name of each test that has executed on the global.testNames property and verifying that this persists all the way back up to the root.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
__(function() {
  // NOTE: if tests are nested, ensure _main is only invoked on the top
  //       level test if this is run as the main module
  module.exports = o.main({
    _type: testtube.Test,
    name: 'SimpleContextTests',
    description: 'A simple set of tests using context',
    setup: function(context) {
      assert(typeof context.local.testName === 'undefined')
      context.local.testName = this.name
      context.global.testNames = []
    },
    teardown: function(context) {
      assert.equal(context.local.testName, this.name)
      assert.deepEqual(context.global.testNames, [
        'SimpleContextTest',
        'SimpleNestedTestWithContextTest1',
        'SimpleNestedTestWithContextTest2',
        'SimpleAsyncContextTest',
        'SimpleContextTests'
      ])
    },
    doTest: function(context) {
      assert.equal(context.local.testName, this.name)
      context.global.testNames.push(this.name)
    },
    tests: [
      o({
        _type: testtube.Test,
        name: 'SimpleContextTest',
        setup: function(context) {
          assert(typeof context.local.testName === 'undefined')
          context.local.testName = this.name
        },
        teardown: function(context) {
          assert.equal(context.local.testName, this.name)
          assert.deepEqual(context.global.testNames, [
            'SimpleContextTest',
          ])
        },
        doTest: function(context) {
          assert.equal(context.local.testName, this.name)
          context.global.testNames.push(this.name)
        }
      }),
      o({
        _type: testtube.Test,
        name: 'SimpleNestedTestsWithContextTest',
        setup: function(context) {
          assert(typeof context.local.testName === 'undefined')
          context.local.testName = this.name
        },
        teardown: function(context) {
          assert.equal(context.local.testName, this.name)
          assert.deepEqual(context.global.testNames, [
            'SimpleContextTest',
            'SimpleNestedTestWithContextTest1',
            'SimpleNestedTestWithContextTest2',
          ])
        },
        tests: [
          o({
            _type: testtube.Test,
            name: 'SimpleNestedTestWithContextTest1',
            setup: function(context) {
              assert(typeof context.local.testName === 'undefined')
              context.local.testName = this.name
            },
            teardown: function(context) {
              assert.equal(context.local.testName, this.name)
              assert.deepEqual(context.global.testNames, [
                'SimpleContextTest',
                'SimpleNestedTestWithContextTest1',
              ])
            },
            doTest: function(context) {
              assert.equal(context.local.testName, this.name)
              context.global.testNames.push(this.name)
            }
          }),
          o({
            _type: testtube.Test,
            name: 'SimpleNestedTestWithContextTest2',
            setup: function(context) {
              assert(typeof context.local.testName === 'undefined')
              context.local.testName = this.name
            },
            teardown: function(context) {
              assert.equal(context.local.testName, this.name)
              assert.deepEqual(context.global.testNames, [
                'SimpleContextTest',
                'SimpleNestedTestWithContextTest1',
                'SimpleNestedTestWithContextTest2',
              ])
            },
            doTest: function(context) {
              assert.equal(context.local.testName, this.name)
              context.global.testNames.push(this.name)
            }
          })
        ]
      }),
      o({
        _type: testtube.Test,
        name: 'SimpleAsyncContextTest',
        setup: function(context, done) {
          assert(typeof context.local.testName === 'undefined')
          var self = this
          setImmediate(function() {
            context.local.testName = self.name
            done()
          })
        },
        teardown: function(context, done) {
          var self = this
          setImmediate(function() {
            var err = undefined
            try {
              assert.equal(context.local.testName, self.name)
              assert.deepEqual(context.global.testNames, [
                'SimpleContextTest',
                'SimpleNestedTestWithContextTest1',
                'SimpleNestedTestWithContextTest2',
                'SimpleAsyncContextTest',
              ])
            } catch (e) {
              err = e
            }
            done(err)
          })
        },
        doTest: function(context, done) {
          var self = this
          setImmediate(function() {
            var err = undefined
            try {
              assert.equal(context.local.testName, self.name)
              context.global.testNames.push(self.name)
            } catch (e) {
              err = e
            }
            done(err)
          })
        },
      })
    ]
  })
})

HttpTest

To simplify testing Carbon.io based services, test-tube provides a second test class called HttpTest. This class extends Test, adding the top-level property baseUrl which may or may not be overriden by a test instance. Additionally, it provides a shorthand for issuing requests to a service as well as for validating the responses to those requests.

Instead of instantiating tests in the tests property directly, the requests to send and the responses that are expected can be specified with the following shorthand:

...
tests: [
  ...
  {
    reqSpec: {
      <request specification>
    },
    resSpec: {
      <response specification>
    }
  }
],
...

Of course, you can also intermingle regular tests if you should choose.

reqSpec

A reqSpec has one required property, method, which should be an HTTP verb (e.g., GET, PUT, POST, etc.). Beyond this, the URL to probe can be specified using the url property as an absolute URL (containing the scheme, host, path, etc.) or as a relative URL (to be appended to the aforementioned baseUrl property of the parent test).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
DEFAULT_NAME = 'foo'
BASE_URL = 'http://127.0.0.1:8888'

__(function() {
  module.exports = o.main({
    _type: testtube.HttpTest,
    name: 'HttpTests',
    description: 'Http tests.',
    baseUrl: BASE_URL,
    setup: function(_, done) {
      var parsedUrl = urlParse(this.baseUrl)
      this.server = new HelloWorld(
        parsedUrl.hostname,
        parsedUrl.port,
        DEFAULT_NAME)
      this.server.serve({}, done)
    },
    teardown: function(_, done) {
      this.server.close(done)
    },
    tests: [
      {
        reqSpec: {
          method: 'GET'
        },
        resSpec: {
          statusCode: 200,
          body: `Hello ${DEFAULT_NAME}.`
        }
      },
      {
        reqSpec: {
          url: BASE_URL + '/say',
          method: 'GET'
        },
        resSpec: {
          statusCode: 200,
          body: `Hello ${DEFAULT_NAME}.`
        }
      },
      {
        reqSpec: {
          url: '/say',
          method: 'GET'
        },
        resSpec: {
          statusCode: 200,
          body: `Hello ${DEFAULT_NAME}.`
        }
      },
    ]
  })
})

Additionally, you can specify query parameters with parameters property, headers with the headers property, and a body with the body property.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
DEFAULT_NAME = 'foo'
BASE_URL = 'http://127.0.0.1:8888'

__(function() {
  module.exports = o.main({
    _type: testtube.HttpTest,
    name: 'HttpTests',
    description: 'Http tests.',
    baseUrl: BASE_URL,
    setup: function(_, done) {
      var parsedUrl = urlParse(this.baseUrl)
      this.server = new HelloWorld(
        parsedUrl.hostname,
        parsedUrl.port,
        DEFAULT_NAME)
      this.server.serve({}, done)
    },
    teardown: function(_, done) {
      this.server.close(done)
    },
    tests: [
      {
        reqSpec: {
          url: '/say',
          method: 'GET',
          parameters: {
            name: 'foo'
          }
        },
        resSpec: {
          statusCode: 200,
          request: function(val) {
            assert.deepEqual(querystring.parse(val.url.query), {name: 'foo'})
          },
          body: 'Hello foo.'
        }
      },
      {
        reqSpec: {
          url: '/say',
          method: 'GET',
          headers: {
            foo: 'bar'
          }
        },
        resSpec: {
          statusCode: 200,
          request: function(val) {
            assert('foo' in val.headers)
            assert.equal(val.headers.foo, 'bar')
          },
          body: `Hello ${DEFAULT_NAME}.`
        }
      },
      {
        reqSpec: {
          url: '/scream',
          method: 'POST',
          body: {
            name: 'bar'
          }
        },
        resSpec: {
          statusCode: 200,
          request: function(val) {
            assert.deepEqual(val.body, JSON.stringify({name: 'bar'}))
          },
          body: 'HELLO BAR!!!'
        }
      },
    ]
  })
})

Finally, name, setup, and teardown are also valid and perform the same functions described in previous sections.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
DEFAULT_NAME = 'foo'
BASE_URL = 'http://127.0.0.1:8888'

__(function() {
  module.exports = o.main({
    _type: testtube.HttpTest,
    name: 'HttpTests',
    description: 'Http tests.',
    baseUrl: BASE_URL,
    setup: function(_, done) {
      var parsedUrl = urlParse(this.baseUrl)
      this.server = new HelloWorld(
        parsedUrl.hostname,
        parsedUrl.port,
        DEFAULT_NAME)
      this.server.serve({}, done)
    },
    teardown: function(_, done) {
      this.server.close(done)
    },
    tests: [
      {
        name: 'NamedHttpTestWithSetupAndTeardown',
        setup: function() {
          process.env[this.name] = 1
        },
        teardown: function() {
          try {
            assert.equal(process.env[this.name], 1)
          } finally {
            delete process.env[this.name]
          }
        },
        reqSpec: {
          url: '/say',
          method: 'GET'
        },
        resSpec: {
          statusCode: 200,
          body: `Hello ${DEFAULT_NAME}.`
        }
      },
    ]
  })
})

If reqSpec is a function, it will be bound to the test instance, called with the context object as the first argument, and should return a refSpec as described above.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
      {
        name: 'ReqResSpecFunctionTests',
        reqSpec: function() {
          return {
            url: '/say',
            method: 'GET'
          }
        },
        resSpec: function(res) {
          assert.equal(res.statusCode, 200)
          assert.equal(res.body, `Hello ${DEFAULT_NAME}.`)
        }

resSpec

Much like a reqSpec, a resSpec can be an Object or a Function. However, it can also be an Object whose properties are Functions. If it is a plain old object, the value of each property will be compared (using assert.deepEquals) to the corresponding value on the response object (as seen above), failing if any of these values do not match. If the value is a function (as seen in the previous example), it will be bound to the test and called with the response object as the first argument and the context object as the second. If the resSpec falls into the third category, each function in the resSpec will bound to the test instance and called with the corresponding value of the response object as the first argument and the context object as the second.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
      },
      {
        name: 'ReqResSpecFunctionTests',
        reqSpec: function() {
          return {
            url: '/say',
            method: 'GET'
          }
        },
        resSpec: {
          statusCode: function(val) {
            assert.equal(val, 200)
          },
          body: function(val) {
            assert.equal(val, `Hello ${DEFAULT_NAME}.`)
          }
        }

httpHistory

Finally, if you need to look back at the request/response history in a test, you can use the httpHistory property of the context object to do this. This is useful if you need to base the current request of a previous response or if you simply want to replay a previous request. The httpHistory object is an instance of HttpTestHistory and has five methods of interest: getReqSpec, getResSpec, getReq, getRes, and get (where get simply returns all history for a particular test). All of these methods take an integer or a string as an argument (the “index”). If the index is negative, the Nth previous history object is returned. If the index is postive it starts from the Nth history object starting from the beginning (e.g. 0 would return the history object for the first test). Finally, if the index is a string, it will return the history object for the test with that name.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
      },
      {
        name: 'SimpleReverseHttpHistoryTest',
        reqSpec: function(context) {
          return context.httpHistory.getReqSpec(-1)
        },
        resSpec: function(res, context) {
          var prevResSpec = context.httpHistory.getResSpec(-1)
          for (var k in prevResSpec) {
            assert.equal(res[k], prevResSpec[k])
          }
        }
      },
      {
        name: 'SimpleForwardHttpHistoryTest',
        reqSpec: function(context) {
          return context.httpHistory.getReqSpec(0)
        },
        resSpec: function(res, context) {
          var prevResSpec = context.httpHistory.getResSpec(0)
          for (var k in prevResSpec) {
            assert.equal(res[k], prevResSpec[k])
          }
        }
      },
      {
        name: 'SimpleNamedHttpHistoryTest',
        reqSpec: function(context) {
          return context.httpHistory.getReqSpec('NamedHttpTestWithSetupAndTeardown')
        },
        resSpec: function(res, context) {
          var prevResSpec =
            context.httpHistory.getResSpec('NamedHttpTestWithSetupAndTeardown')
          for (var k in prevResSpec) {
            assert.equal(res[k], prevResSpec[k])
          }
        }

Skipping Tests

Sometimes a test needs to be skipped (e.g., when a certain language feature is not available in the version of node being run) or marked as unimplemented. There are a couple ways to do this.

To skip a test, either throw an instance of SkipTestError or instantiate an instance of SkipTest:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
      o({
        _type: testtube.Test,
        doTest: function() {
          throw new testtube.errors.SkipTestError('Skipping test because of foo')
        }
      }),
      o({
        _type: testtube.util.SkipTest,
        description: 'Skipping test because of foo'
      }),

Note, if SkipTest is instantiated, the description property will be used as the message argument to SkipTestError.

It is often common to think of tests that need to be implemented as one is designing a certain feature or in the midst of implementation. In these cases, a placeholder test can be added that indicates the test has not been implemented and that doesn’t fail the test suite. To do this, simply add the test skeleton and throw a testtube.errors.NotImplementedError

1
2
3
4
5
6
7
      o({
        _type: testtube.Test,
        doTest: function() {
          throw new testtube.errors.NotImplementedError('Implement foo')
        }
      }),
      o({

The resulting report for the above tests should look something like:

Running SkipTests...
  [*] Test SKIPPED (0ms)
  [*] SkipTest SKIPPED (1ms)
  [*] Test NOT IMPLEMENTED (0ms)
  [*] SkipTests (1ms)

Test Report
[*] Test: SkipTests (Demonstrate how to skip tests.) (1ms)
  [*] Test: Test SKIPPED (0ms)
    Skipping test because of foo
  [*] Test: SkipTest SKIPPED (Skipping test because of foo) (1ms)
    Skipping test because of foo
  [*] Test: Test NOT IMPLEMENTED (0ms)
    Implement foo

Command Line

$> node test -h

 Usage: node test [command] [options]

 command
   run     Run the test suite (default)

 Options:
    --path PATH      Process tests rooted at PATH (globs allowed)
    --include GLOB   Process tests matching GLOB (if --exclude is present, --include takes precedence, but will fall through if a test is not matched)
    --exclude GLOB   Process tests not matching GLOB (if --include is present, --exclude will be skipped if the test is matched by the former)

 Environment variables:
   <none>