mocha-ui-exports-request
v2.2.0
Published
Unit-test helper for network request assertions, based on Mikael's request and should.js of visionmedia, designed for mocha-ui-exports
Downloads
1,887
Maintainers
Readme
mocha-ui-exports-request
Brief
Use a DECLEARATIVE language for your HTTP request assertions, and get DESCRIPTIVE spec output from your mocha tests.
This is a unit-test helper for network request assertions, based on Mikael's request and should.js of visionmedia, designed for the mocha-ui-exports plugin for mocha test framework.
Content
Use Case
Assume you have a cool http server that you want covered in BDD unit-tests, and you want:
- high resolution DESCRIPTIVE test results
- generated by a DECLARATIVE language
Prequisites - you're programming in node, and using mocha as test framework.
Example Lets just take for example - a notes application, that uses couch-db. To make the example short, lets just assume 2 APIs:
- homepage - an HTML page
- list-notes - an AJAX call
- post-note - an AJAX call
You want DESCRIPTIVE test results that follow the BDD principal that makes it as readable as specifications. One that looks, for example, like that:
./lib/server.js
homepage - /
with no parameters
√ should return status 200
√ should emit http-header: 'content-type' as text/html
√ body should match : /<h1>Your notes<\/h1>/
ajax - /ajax/listnotes
with no parameters - should return the 5 latest notes
√ should return status 200
√ should emit http-header: 'content-type' as text/json
√ body should be : '{"notes":[{"date":1396257...'
with 'to' - should return the next page in fixture
√ should return status 200
√ should emit http-header: 'content-type' as text/json
√ body should be : '{"notes":[{"date":1396257...'
with 'to' and 'from' - should return the cut
√ should return status 200
√ should emit http-header: 'content-type' as text/json
√ body should be : '{"notes":[{"date":1396257...'
ajax - /ajax/postnote
with valid form - should accept the note
√ should return status 200
all expected couch-db hits
√ should have been called
14 passing (81ms)
But you want to express your mocha tests in a DECLERATIVE way. Declarative - means:
- avoid flow-control
- avoid logic
- stick with declaration
- stick with descriptions No ifs, no elses, no loops, and as few add-hock custom callbacks as possible.
Ah, assume you want to cover the back-end requests too, and use for that purpose, for example nock.
How would you feel if I tould you that you can do it this way:
// the test target
var svr = require('../server')
//the helper
, request = require('mocha-ui-exports-request')
//plugs the recorded scenario of http requests to couch
, nock = require('fixtures/nock')
//System Under Test
, SUT = "http://127.0.0.1:1234"
;
svr.listen(1234);
module.exports =
{ "./lib/server.js" :
{ "homepage - /" :
{ "with no parameters" :
request( SUT + "/")
.responds(
{ status: 200
, headers:
{ "content-type" : "text/html"
}
, body: /<h1>Your notes<\/h1>/
}
)
}
, "ajax - /ajax/listnotes" :
{ "with no parameters - should return the 5 latest notes in fixture" :
request(SUT + "/ajax/listnotes")
.responds(
{ status : 200
, headers:
{ "content-type" : "text/json"
}
, json:
{ notes :
[ { date: new Date('2014-04-02T15:35:55.754').getTime()
, note: "note 1"
}
, { date: new Date('2014-04-02T05:22:41.832').getTime()
, note: "note 2"
}
, { date: new Date('2014-04-01T21:03:43.004').getTime()
, note: "note 3"
}
, { date: new Date('2014-04-01T13:55:48.123').getTime()
, note: "note 4"
}
, { date: new Date('2014-04-01T08:14:31.631').getTime()
, note: "note 5"
}
]
}
}
)
, "with 'to' - should return the next page in fixture" :
request(SUT + "/ajax/listnotes/to/2014-03-31/")
.responds(
{ status: 200
, headers:
{ "content-type" : "text/json"
}
, json:
{ notes :
[ { date: new Date('2014-03-31T09:21:55.331').getTime()
, note: "note 6"
}
, { date: new Date('2014-03-30T06:21:55.784').getTime()
, note: "note 7"
}
, { date: new Date('2014-03-29T18:13:41.575').getTime()
, note: "note 8"
}
]
}
}
)
, "with 'to' and 'from' - should return the cut" :
request(SUT + "/ajax/listnotes/from/2014-03-30/to/2014-03-30/")
.responds(
{ status: 200
, headers:
{ "content-type" : "text/json"
}
, json:
{ notes :
[ { date: new Date('2014-03-30T06:21:55.784').getTime()
, note: "note 7"
}
]
}
}
)
}
, "ajax - /ajax/postnote" :
{ "with valid form - should accept the note" :
request(
{ uri : SUT + "/ajax/postnote"
, form:
{ note : "note 9"
}
}
).responds(
{ status: 200
}
)
}
, "all expected couch-db hits" :
{ "should have been called" :
function(){
nock.done()
}
}
}
}
I want to use the standard BDD mocha UI
Ok. here: (since 1.1.0)
If you set to --ui bdd
, or if your --ui
switch is not provided
(not in your CLI command, nor in your test/mocha.opts
file)
then .responds({..})
will use the describe(..)
and it(..)
APIs for you,
loading the generated tests with the generated descriptive titles to the tests tree.
In this case, the .responds({..})
will return a context object instead of the
suite that the mocha-ui-exports plugin expects.
The context object is described right after the snippet, and can be used to writing tests using it(..)
instead of providing headers:
, responseBody:
, or and:
blocks.
var request = require('mocha-ui-exports-request')
, SUT = 'http://localhost:4321'
;
describe('/my-path', function() {
describe('called with no parameters', function() {
var ctx = request(SUT + '/my-path')
.responds({
status: 200,
});
it('should foo', function() {
ctx.res.should.be...
})
});
})
This will registger all the test handlers using describe
, before
, it
and after
, using the same titles and structure.
If for some reason you're using hybrid UI and your --ui
switch is set to
any value that is not bdd
this behavior will not trigger automatically, however, you can
still trigger this behavior manually by calling .bddCtx()
.
suite('/my-path', function() {
suite('called with bad parameter value', function() {
var ctx = request(SUT + '/my-path?param=bad')
.responds({
status: 400
}).bddCtx()
test('should ...' )
})
})
The returned ctx
object has:
ctx.err
- when the http-request fails abruptly for networking issues (dns, network). mind that if you have ANY statusCode - it means there was no error, and a response object is passed.ctx.res
- the response object (http.IncomingMessage
)ctx.body
- the body on the passed response object, as extracted by therequest/request
package.
Install
npm install mocha-ui-exports-request
ok, long name. I will accept better offers. But until then... :P
Test
the published package is slim: it does not contain the test suite.
You'll have to clone this repo, to npm install
from within the cloned folder, and then to npm test
.
API
request
request(reqOptions) : RequestTester
It builds a requet-tester for the provided reqOptions
.
reqOptions
- any options setting valid for mikael's request module, including form, post-data, multiplart, and whatever you want.
Starting from 1.0.1 - it could be a handler that returns such options settings.
RequestTester
- implements one method: RequestTester#responds
, that returns a mocha-ui-exports suite, who's setup function will fire the request, and hold it in context closure for all the asserts that follow.
RequestTester#Responds
reqTester#responds(options) : suite
It builds an asserter for every one of the options provided.
Supported options:
options.status
assert a status code. generates "should return status ..." asserts.
Example:
module.exports = {
'/my/resource' :
request( 'http://my-sut-server.com/my/resource' )
.responds( {
status: 200
})
}
becomes:
/my/resource
√ should return status 200
options.headers
asserts against headers in the headers collection. generates "should emit http-header: 'xxx' as yyy" asserts keys are expected HTTP-headers, values can be strings to compare to, or RegExps to match with.
Example:
module.exports = module.exports = {
'/my path' :
request( 'http://my-sut-server.com/my-path' )
.responds( {
headers: {
'x-powered-by' : 'my cool server'
etag : /.*/
}
})
}
becomes:
/my-path
√ should emit http-header: 'x-powered-by' as 'my cool server'
√ should emit http-header: 'etag' as /.*/
options.responseHeaders
For add-hock assertions that should be performed against the entire headers collection,
and cannot be expressed as descrete assertions against a single http header.
The test titles in this case are your responsibility. Your tests will be grouped under 'response headers' section
The value of this entry should be an object where every key is a test-tile, and every value
is a function that accepts the response.headers
collection and performs add-hock assertions against it.
Example:
module.exports = module.exports = {
'/my path' :
request( 'http://my-sut-server.com/my-path' )
.responds( {
responseHeaders: {
'must contain either x-powered-by or x-server-type' : function(headers) {
Should( headers['x-powered-by'] || headers['x-server-type']).be.ok
}
}
})
}
becomes:
/my-path
response headers
√ must contain either x-powered-by or x-server-type
options.body
an array of body asserts. Assertions may be:
string
- asserts that the body is equal to the givenstring
value, under title like : "body should be : {your assertion value}". When the given value is longer than 20 chars the remnant is cut and appended with ellipis (...).RegExp
- asserts that the body matches the givenRegExp
value, under title like "body should match: ..."object
- this is a suite object, where every key is a test tile, and every value is a test-funciton that expects the body, and lets you write whatever add-hock tests you need. In this case, the test titles are your responsibility.Array
- a combination of any of the above.
Example:
module.exports = module.exports = {
'/my path' :
request( 'http://my-sut-server.com/index.html' )
.responds( {
body: [
/<h3>Hello Anonymous</h3>/,
/<h2>Our Catalog</h2>/,
{ 'should contain 3 promoted products' : function(body) {
var n = 0;
body.replace(/class="promotedProduct"/, function() { n++ } );
n.should.eql(3)
}
}
]
})
}
becomes:
/my-path
√ body should match: /<h3>Hello Anonymous</h3>/
√ body should match: /<h2>Our Catalog</h2>/
response body
√ sould contain 3 promoted products
options.json
When provided - it is stringified and treated a a string body asserter.
Example:
module.exports = {
'/api/search' :
'search for non existing product' :
request( 'http://my-sut-server.com/search?no-such-product' )
.responds( {
json: { products: [ ] }
})
},
'search for specific existing product' :
request( 'http://my-sut-server.com/search?yellow%20t%20shirt' )
.responds( {
json: { products: [ { name: 'XL yellow T-Shirt', description: 'a very cool shirt, organic materials, very comfortable, loved by our customers' } ] }
})
}
}
}
becomes:
/api/search
search for non existing product
√ body should be '{ products: [ ] }'
search for specific existing product
√ body should be "{"products":[{"name"..."
options.err
Assert an expected error for your request.
- Mind that it's a network error, and not HTTP error. HTTP errors are checked with
options.status
. - it's useful in wierd edge-cases, for example, when you want to test that the server instance terminates in response to a shutdown request by an authenticated administrator.
options.and
a subsuite of add-hock custom assertions, performed against the entire response object.
The value should be an object where every key is a test-tile, and every value is a function that accepts the response
object and performs add-hock assertions against it.
Example:
module.exports = {
'/api/wierd' : {
request('http://my-sut-server.com/search?no-such-product')
.responds({
headers: {
'x-powered-by' : 'my cool server'
etag : /.*/
},
and: {
'should redirect header, or the resource' : function(res) {
Should.be.ok(
res.status == 302 ||
res.status == 200 && res.headers['content-type'] == 'application/json'
)
}
}
})
}
}
becomes:
/api/wierd
√ should emit http-header: 'x-powered-by' as 'my cool server'
√ should emit http-header: 'etag' as /.*/
and
√ should give a redirect header, or the resource
options.skip
marks the suite as skipped
Example:
module.exports = module.exports = {
'/my path' :
request({
url: 'http://my-sut-server.com/my-path',
skip: true
})
.responds({
status: 200
})
}
becomes:
/my-path
- should return status 200
request.skip
same as passing options.skip
Example:
module.exports = module.exports = {
'/my path' :
request.skip( 'http://my-sut-server.com/my-path' )
.responds( {
status: 200
})
}
becomes:
/my-path
- should return status 200
For more detailed API spec - don't dig for docs - run the test, just like the BDD lore sais... Have fun ;)
Contribute
Sure, why not. That's why it's here ;)
Future
- negated assertions (status is not..., body does not contain...)
- handle timeouts
Lisence
- MIT