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 two
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>