Skip to content

Latest commit

 

History

History
751 lines (528 loc) · 28.5 KB

README.md

File metadata and controls

751 lines (528 loc) · 28.5 KB

qmock

Build Status Coverage Status

Light-weight test double library for easier testing of dependency injection style code. Patterned somewhat after phpunit, which looks like junit I believe.

Can stub, spy, and mock classes, objects, the system timers, require() and http calls.

qmock is testing framework agnostic; mocks can be used standalone. They are integrated into the qnit unit test runner.

Example

qmock = require('qmock');

// mock an existing object
var mock = qmock.getMock(console);

// instrument log() and ensure that it is called with 'hello'
// full phpunit syntax should also work, ie
// mock.expects(qmock.twice()).method('log').with('hello');
mock.expects(2).method('log').with('hello');
mock.log('hello');
qmock.check(mock);      // assertion error, called 1 times, expected 2

// with() is sticky, all subsequent calls must also match
mock.log('world');
qmock.check(mock);     // assertion error, 'world' !== 'hello'

// methods don't have to already exist.  create and call a stub method
// by specifying what it will return:
mock.expects(1).method('boom').will(qmock.throwError(new Error("BOOM!")));
mock.boom();            // error throw, BOOM!

Mock Objects API

qmock.getMock(master) returns a mock object that is instanceof master. The master can be a class (constructor function) or an existing object. The mock is a fully functional object, with some methods possibly stubbed out.

qmock.getMock( master, methodNames [,constructorArgs] )

If master is a class (a constructor function), returns a mock object built with the given constructorArgs.

If master is an object, an identical object is cloned. The clone will be instanceof the same class as master, and will have the same own and inherited properties as master.

In both cases, some or all of the named methods are replaced by no-op stubs. If methodNames is given as falsy, no methods are stubbed; if an Array, the named methods are stubbed; if not passed, all methods are stubbed.

Example:

var fakeLogger = qmock.getMock({}, ['log']);
fakeLogger.log("test message");

qmock.getMockSkipConstructor( master, methodNames )

Build a mock object like getMock but do not initialize the object. The returned object will still have the instance, own and inherited properties of master, but will be completely un-initialized.

If master is an existing object, this call is identical to getMock.

Mock Objects

Stub and Spy API

Stubs are stand-in methods that track and instrument calls. Spies are fully functional methods annotated with instrumentation. Their functionality overlaps.

A spy instruments an existing method (or function) and tracks calls made to it. The original method contiues to work exactly as before.

A stub temporarily replaces an existing method (or function), or creates an anonymous function and spies on the replacement. The original method will not be accessible until after it's restore-d.

qmock.spy( [func] )

Spy on calls to the given function. Returns an instrumented function that tracks calls to func. If no func is given, an anonymous function is created to be spied on, which can then be passed as eg a callback. Returns the spy function. The stats are accessible as properties on the spyFunc, or in its property spyFunc.stub.

Example

var qmock = require('qmock');
var computeFunc = function(a, b) { return a + b };
var spyFunc = qmock.spy(computeFunc);

var c = spyFunc(1, 2);
computeFunc = spyFunc.restore();

// c => 3
// spyFunc.callCount => 1
// spyFunc.callArguments => [1, 2]
// spyFunc.callResult = 3

qmock.spy( object, methodName [,override] )

Spy on calls to the named method of the object. If the override function is given, the method will be replaced with the override. Returns a spy function that holds information about the calls made. The object method can be restored to the original with spy.restore().

The returned spy contains the call stats like qmock.stub(), with additional methods (see Stub and Spy API, below).

Example

var qmock = require('qmock');
var originalWrite = process.stderr.write;
process.stderr.write = qmock.spy(function(str, cb) {
    console.log("would have written %d bytes", str.length);
    if (cb) cb();
});
process.stderr.write("test message\n");
// => would have written 13 bytes

process.stderr.write = originalWrite;
process.stderr.write("another message\n");
// => another message

qmock.stub( )

With no arguments, returns an instrumented anonymous function like qmock.spy().

qmock.stub( func )

Return an instrumented anonymous function to replace func. restore() returns the original func.

Example:

process.exit = qmock.stub(process.exit);
process.exit();
// did not exit!
process.exit = process.exit.restore();

qmock.stub( object, methodName [,overrideFunction] )

Replace the named method of object with an anonymous noop function or the specified override, and return a stub method that will contain information about calls to the method. This form can be used to suppress or rewrite method calls.

If an override function is not specified, a noop function is used (changed in 0.4.0). The noop function does not call its callback, if any.

If object does not have a method methodName, one will be created for it. The overridden object property will be restored to its original value (or deleted if it did not exist) upon stub.restore().

Use spy to passively examine calls to an existing method or function.

Returns a stub object that is updated with information about the last call's arguments, return value, exception thrown, and callback arguments:

qmock.stubOnce( object, methodName [,overrideFunction] )

One-shot stub: stub the method like qmock.stub(), but restore the original method after the first call.

qmock.spyOnce( object, methodName [,override] )

One-shot spy: spy on the function or method like qmock.spy(), but restore the original after the first call.

Stub and Spy Methods

stub.returns( value )

Make the stub return value when called. Returns the stub for call chaining. Calling returns on a spy converts it into a stub.

stub.yields( [val1, [val2, ...]] )

Make the stub call its callback with the provided values [val1, val2, ...]. The callback is assumed to be the first function in the stub argument list. Returns the stub for chaining. Calling yields on a spy converts it into a stub.

The callback is invoked synchronously, before the stub returns. Use yieldsAsync to call back after a small pause.

stub.yieldsAsync( [val1, [val2, ...]] )

Like stub.yields, but invoke the callback asynchronously, after a short pause.

stub.resolves( val )

Make the stub return a Promise resolved to the value val.

stub.rejects( val )

Make the stub return a Promise that rejects with the value val.

stub.throws( error )

Make the stub throw the given error value. Returns the stub for chaining. If a stub both yields and throws, it will throw first and call the callback on the next event loop tick. Calling throws on a spy converts it into a stub.

stub.onCall( n )

Make all subsequent returns, yields and throws calls configure the n-th (0-based) use-once retval. Call onCall(-1) to restore the default behavior of configuring the permanent retval.

stub.returnsOnce( value )

Like stub.returns, but only returns the value once. Creates a new use-once retval and configures it to return value. returnsOnce actions are performed in sequence, so stub.returnsOnce(1).returnsOnce(2) will return first 1 then 2. Returns the stub for call chanining.

After all stub.returnsOnce values have been returned, all subsequent calls will return the stub.returns value.

stub.yieldsOnce( [val1, [val2, ...]] )

Like stub.yields, but calls back with these values only once. See also returnsOnce.

The callback is invoked synchronously, before the stub returns. Use yieldsAsyncOnce to call back after a small pause.

stub.yieldsAsyncOnce( [val1, [val2, ...]] )

Like stub.yieldsOnce but invoke the callback asynchronously, after a short pause.

stub.throwsOnce( error )

Like stub.throws, but throws only once. See also returnsOnce.

stub.restore( ), spy.restore( )

Unhook the spy or stub, and restore the original method back onto the object. Returns the original spied-on/stubbed method, function, or property.

stub.getCall( n )

Return the n-th (0-based) call details. The details include the the call args, its returnValue, and any exception thrown.

Stub and Spy Properties

stub.callCount

Number of calls made to the stub.

stub.callArguments

Array with the arguments from the last call to the stub.

stub.callResult

Last value returned from the stub function, undefined if it threw an Error.

stub.callError

The error generated by the last call to the stub function, null if none. A successful call following a call that threw replaces the error with null.

stub.error

Like stub.callError, but error is sticky, and contains the last error thrown by any call to the stub.

stub.callCallbackArguments

If a spied-on method is passed an argument of type "function", it will be assumed to be the method callback and will be instrumented to track what it returns. A copy of the callback arguments with will be placed into spy.callCallbackArguments. Only the first function-type argument is instrumented.

Note: callbacks are not synchronized with calls to the stub, so the callback arguments may not be from the most recent call.

Note: qmock versions 0.10.2 and earlier looked for a callback only in the last argument position; qmock 0.11.0 and up look for the first argument of type "function".

Example:

var qmock = require('qmock');
var assert = require('assert');

var stub = qmock.stub(process, 'exit', function(){});
process.exit(1);
process.exit(2, 3);
console.log("did not exit");
// => did not exit

assert.equal(stub.callCount, 2);
assert.deepEqual(stub.callArguments, [2, 3]);

stub.restore();
process.exit();
// process exited, program stopped

console.log("this line will not appear");
// no output, line not reached

spy.args

spy.getAllArguments( )

Return the argument vectors passed to the first 10 calls of the spied function. For convenience, this information is also available in the spy.args array.

spy.returnValues

spy.getAllResults( )

Return the values returned by the first 10 calls to the spied function. Also available as spy.returnValues.

spy.exceptions

spy.getAllErrors( )

Return the errors thrown by the first 10 calls to the spied function. If no error was thrown by a call, the array contains a null entry for it. Also available as spy.exceptions.

spy.getAllCallbackArguments( )

Return the argument vectors passed to the stub callback. The callback is recognized as a function passed as the last value in the stub arguments list. Note that callbacks may be called out of order, so the returned argument may not match 1-to-1 the stub arguments passed in getAllArguments.

Example

var qmock = require('./');
var spy = qmock.spy(process.stderr, 'write', function(str, cb) {
    console.log("would have written %d bytes", str.length);
    if (cb) cb();
});
process.stderr.write("test message\n");
// => would have written 13 bytes

spy.restore();
process.stderr.write("another message\n");
// => another message

Mock Timers API

mockTimers overrides the system setImmediate, setTimeout, etc calls with mock work-alikes that trigger under user control. unmockTimers restores the system timers.

clock = qmock.mockTimers( )

Replace the nodejs timers functions setImmediate, clearImmediate, setTimeout et al with mocked versions whose time is not linear and is not limited by real time. Returns a clock object. To restore the timers back to their original unmodified versions, use qmock.unmockTimers().

This function can be called any number of times, each call replaces the previous timers calls in effect with a new set. Note that any pending immediates and timeouts in the system timers will still trigger, but with follow-up timeouts queued into the mock.

Returns a mock timeouts clock that controls the passage of events time:

clock.tick( [n] )

Advances mock timers time by n milliseconds (default 1). Immediates and timeouts are run as they come due, immediates before timeouts. 0 milliseconds runs only the immediates.

clock.immediates

The array of immediate tasks that will execute on the next event loop tick.

clock.timeouts

A hash indexed by the expiration timestamp of arrays of timeouts.

clock.timestamp

The current mock timers timestamp that is advanced by tick.

Example:

var qmock = require('qmock');
var clock = qmock.mockTimers();
setTimeout(function() {
    console.log("timeout");
    setImmediate(function() {
        console.log("immediate");
        qmock.unmockTimers();
    });
}, 10);
clock.tick(9);
// => (nothing)
clock.tick(1);
// => "timeout"
clock.tick(0);
// => "immediate"

qmock.unmockTimers( )

Restore the global setImmediate, setTimeout etc functions back to their inital original nodejs versions. Can be called any time. Note that any pending timeouts in the mock timers can still be triggered with clock.tick().

Mock Require API

mockRequire overrides require everywhere to return the mock value for the named modules. unmockRequire restores the system require.

qmock.mockRequire( moduleName, replacement )

Arrange for require of moduleName to return replacement in all sources, even if moduleName had been loaded previously. Calls to mockRequire are cumulative, each one defines one more module that will be mocked.

It is an error for moduleName to be falsy.

qmock.mockRequireStub( moduleName, handler )

Stub require of moduleName with the provided handler function, and return the value it computes. Handler is invoked as handler(moduleName).

qmock.unmockRequire( [moduleName] )

If moduleName is provided, arrange for require of moduleName to load the actual module and not the mock replacement value.

Without a module name, uninstall the mock require hooks and restore the original unmodified system require functionality. All previously defined module mocks are cleared.

qmock.unrequire( moduleName )

Helper function to restore the system to the state it was in before the named module was ever require-d. It deletes all cached copies of the module from require.cache, module.children, module.parent.children etc.

Mock Http API

mockHttp overrides http.request and https.request with mocks that return user supplied values. unmockHttp restores the system http functions.

qmock.mockHttp( handler(req, res) )

Override http.request and https.request to redirect all web requests to the provided handler. Each new request will make a call to handler. Request and response behavior and emulation is up to the handler. The handler is invoked immediately after the caller receives the req return object. This function can be called at any time, each replaces the previous override. Restore the default system request functionality with unmockHttp.

The request callback is passed the mock res object or the res supplied by the handler as soon as mockResponse event is emitted req.emit('mockResponse', [res]).

Note that the handler gets a client-side http.ClientRequest (what the client sends to the server) and http.IncomingMessage (what the client receives back), not the usual server-side IncomingMessage and ServerResponse objects.

Example

qmock.mockHttp(function(req, res) {
    req.emit('mockResponse');
    res.emit('data', "mock data");
    res.emit('end');
})
var req = http.request("http://localhost", function(res) {
    res.on('data', function(chunk) {
        console.log("got:", chunk);
    })
    res.on('end', function() {
        qmock.unmockHttp();
    })
})
// => got: mock data

qmock.mockHttp( )

Without arguments, mockHttp mocks an http server, and returns the mock server object. The mock server has methods to recognize and respond to calls to mocked routes.

server = qmock.mockHttp();
server.when('http://localhost/test')
  .end(200, 'Hello, test.');

server.when( condition )

Match the route against the condition. If the route matches condition, the actions that follow will be run to generate the response for the route. Only the first matching condition will have its actions run.

The http.request callback is called before the first matching action is run, and the 'end' event is emitted when no more actions are left to run. Note that because the actions that build the respones have not been run yet, the res.statusCode and other response fields may remain undefined until the res 'end' event has been received.

Conditions:

  • string - match the full url or the request pathname against the string
  • METHOD:string - match the full annotated url or annotated request pathname against the string, The annotated url would look something like "POST:http://localhost:80/pathname".
  • RegExp - match the url or pathname against the regular expression
  • function(req, res) - use the given function to test whether the route matches

Examples:

.when('http://localhost:80/')       - match any http request to localhost port 80
.when(/\/test\//)                   - match any request with "/test/" in its pathname
.when(/^POST:/)                     - match any POST request
.when(/^/)                          - match any request
.when(function(req, res) {          - match any request with Basic user:pass authorization
    return (req._headers['authorization'].indexOf('Basic ') === 0);
})

server.on( condition )

An alias for server.when.

server.once( condition )

Like server.when, but the condition will be matched only once. After the first match, it will not match any other request. This allows the mock to return different responses to the same request. The matching actions are run in the order defined.

qmock.mockHttp()
    .once('http://host/getNext')
      .send(200, 'data1')
    .once('http://host/getNext')
      .send(200, 'data2')
    .once('http://host/getNext')
      .send(500, 'no more data');

// http.request('http://host/getNext') => 'data1', statusCode 200
// http.request('http://host/getNext') => 'data2', statusCode 200
// http.request('http://host/getNext') => 'no more data', statusCode 500

server.default( )

An alias for a condition that always matches the route, equivalent to .when(/.*/). Define a default as the very last condition, because conditions are tested in the order defined and the default always matches all routes so no other conditions will be tested.

server.before( )

The before actions are run for all matched routes before their condition actions are run.

server.after( )

The after actions are run for all matched routes after their condition actions are run.

Actions:

server.send( [statusCode], [responseBody], [responseHeaders] )

Set the response statusCode and responseHeaders, write the responseBody, and finish the response. No more data should be written after the response has been finished.

server.send( responseFunction(req, res, next) )

Call the provided responseFunction to generate the response.

server.write( responseBodyChunk )

Cause res to emit a 'data' event with the given chunk.

server.writeHead( [statusCode], [responseHeaders] )

Set the response statusCode and headers.

server.end( [statusCode], [responseBody] )

Same as send: set the statusCode, write the reply and finish the response.

server.compute( callback(req, res, next) )

Invoke the provided callout, let it adjust res.

server.delay( ms )

Pause for ms milliseconds before continuing with the rest of the condition actions.

server.emit( event, [arg1, arg2, ...] )

Emit an event on the res object.

server.throw( err )

Emit the error event on the req object.

server.makeRequest( [url [,body [,headers]]] )

Without arguments, replays the mock request: makes a real http request with the same arguments as the mock, and relays the real http response to the mock response.

With arguments, makes an http request to the specified url string or object, optionally with the given request body and request headers, and relays the real http response back to the mock response. Body, if specified, must be a string or Buffer.

The default request method is GET, use a uri object to override.

Here's an example that mocks just the third call, making actual web requests for the first, second, fourth and all subsequent calls:

// mock just the third call to 'localhost:80'
mock = qmock.mockHttp()
    .once("http://localhost:80").makeRequest()      // first call
    .once("http://localhost:80").makeRequest()      // second call
    .once("http://localhost:80").send(404, 'override with Not Found')
    .when("http://localhost:80").makeRequest()      // all other calls

Example

var mockServer = qmock.mockHttp()
    .when("http://localhost:1337/test/call")
      .send(204)
    .when("http://localhost:1337/test/error")
      .emit('error', new Error("error 409"))
      .send(409, "test error 409", { 'test-header-1': 'test-value-1' })

var req = http.request("http://localhost:1337/test/error", function(res) {
    var response = "";
    res.on('data', function(chunk) {
        response += chunk;
    })
    res.on('end', function() {
        assert.equal(res.statusCode, 409);
        assert.equal(response, "test error 409");
        assert.equal(res.headers['test-header-1'], 'test-value-1');
    })
    res.on('error', function(err) {
        console.log("got err '%s'", err.message)
    })
})
req.end("test request body");

// => got err 'error 409'

qmock.unmockHttp( )

Restore the original system implementations for http.request and https.request. This function can be called any time.

Change Log

  • 0.17.2 - bump disrequire to fix stack trace capture for node-v0.1x and node-v0.6, remove unused dependency on qassert, remove dev dependencies since not publishing tests
  • 0.17.1 - fix disrequire under node-v0.6, fix mockHttp under node-v16 and up that call socket.destroy
  • 0.17.0 - new stub.resolves and stub.rejects
  • 0.16.5 - avoid Buffer deprecation warnings too
  • 0.16.4 - mockHttp fix req.socket for node-v0.8, avoid node v12 req._headers deprecation warning, fix tests that check req._headers
  • 0.16.3 - bump disrequire to use with source maps
  • 0.16.2 - always record the first 10 calls by default, the previous 3 was too low
  • 0.16.1 - upgrade disrequire to fix global name leak stack
  • 0.16.0 - breaking: fix mockHttp semantics to not auto-install on load and to uninstall completely on unmock, ensure req.abort and req.socket.destroy are available on mock requests
  • 0.15.0 - expose disrequire under the package name as well (alias of unrequire), upgrade disrequire to hugely improve worst case speed
  • 0.14.3 - make mockHttpServer match paths by full url, path+query, or just pathname
  • 0.14.2 - patch mockHttp to handle incomplete mockRequest uri, fix mockHttp makeRequest to return with response
  • 0.14.1 - upgrade disrequire for fix to resolveOrSelf of caller of anonymous function
  • 0.14.0 - use the unrequire code moved into the disrequire package, fix makeRequest call method handling
  • 0.13.1 - improve mockRequire code coverage (qmock now at 100%), also test with node-v9
  • 0.13.0 - new server.makeRequest mock http server call, fix onConsecutiveCalls to be able to return plain functions
  • 0.12.0 - new stub.onCall and stub.getCall methods, document yieldsAsync and yieldsAsyncOnce.
  • 0.11.3 - remove dependency on mongoid, make stub work with node-v0.8 without setImmediate
  • 0.11.2 - fix filepath resolution for _require, mockRequire and mockRequireStub, and throw if cannot find file
  • 0.11.1 - stub internal reorg, implement getCall/yieldsAsync/yieldsAsyncOnce, cleanups
  • 0.11.0 - treat as callback the first function (not the last arg), new: stub/spy methods yields(), returns(), throws(), yieldsOnce(), returnsOnce(), throwsOnce() new: stub/spy methods calledBefore(), calledAfter() fix: record actual callback arg not the internal callback spy
  • 0.10.2 - fix unrequire() of ./ and ../ relative filepaths
  • 0.10.1 - fix stubbing a method on a function
  • 0.10.0 - save 3 results in stub(), always return a function from spy() and stub(), make restore() return the original func, make spied stats accessible on the returned spy itself in addition to spy.stub
  • 0.9.3 - fix unrequire to tolerate corrupted require.cache vs module.children
  • 0.9.2 - add mockRequireStub
  • 0.9.0 - new mockRequire() functionality
  • 0.8.0 - new .on, .once and .default mockHttpServer commands, make spy(func).restore() return func (not throw), upgrade to mongoid-1.1.3
  • 0.7.0 - breaking: fix mockHttpServer buildUrl and .when to build and test the same url nodejs does. This means uri.pathmame is now ignored, which might break tests that depended on it.
  • 0.6.6 - allow falsy timers to clearTimeout et al
  • 0.6.5 - stub req.setTimeout, intercept request even when called as a bare function
  • 0.6.4 - also test with node-v8, experimental server.throw action, match POST:, DEL: etc qualified urls or pathnames, stub req.sock.setTimeout, propagate uri.headers to req._headers
  • 0.6.3 - set stub.called for sinon compat, fix getMock(Constructor), fix extendWithMocks().getMockSkipConstructor, fix mocks when have expects/method/check methods, fix QMock.expects() when mocked has expects() method
  • 0.6.2 - fix extendWithMocks to export all mock methods
  • 0.6.1 - readme updates
  • 0.6.0 - new mockHttp() methods write, writeHead, end and emit, document mockHttp()
  • 0.5.5 - fix code and tests to make unit tests pass under node-v0.10
  • 0.5.2 - make stub() without args return an anonymous stub function like spy()
  • 0.5.1 - fix, test and export stubOnce / spyOnce, fix coverage script
  • 0.5.0 - stubOnce and spyOnce
  • 0.4.0 - breaking change: stub() with a noop function if no override method is given
  • 0.3.1 - fix mockHttpServer typos and parser errors (experimental)
  • 0.3.0 - extendWithMocks adds stub/spy/mockTimers/mockHttp, mockHttpServer (experimental)
  • 0.2.0 - also track stub callbacks, new anonymous spy functions, simple http mocking, test with qnit
  • 0.1.0 - stub() and mockTimers(), initial spy()
  • 0.0.8 - Jan 2015 version

Todo

  • the nodejs property getter/setter methods should make it possible for data properties to be mocked too, eg getMockValue(name). with() could map to set, will() to get.
  • introduce a havingReturned() method to be able to inspect not just the called with arguments but the method return value as well
  • add returnCallback() method to return err, for callbacks not just direct returns
  • clone un-enumerable properties as well, retaining their original definitions
  • inherit() and disinherit() calls: annotate the prototype (inherited properties) of the object, for e.g. x = 3; inherit(x, 'a', 1); assert(x.a === 1)
  • .when().timeout(ms) action to emulate a req timeout
  • todo: publish req._mockWrites, useful for debugging
  • todo: error out on write after end, to catch errors
  • fix: mockHttp should match default ports 80 and 443 whether or not explicitly included in url
  • fix: mockHttp should not end the call if no actions have been specified
  • maybe: mockHttpServer should match only path, not query string params
  • fix: http.request should emit a 'response' event, and the callback should listen for it
  • fix: support res.setEncoding (filter events? and convert as needed??)
  • support node-v0.8.28 (mockHttp)
  • maybe: qmock.restoreAll? qmock.resetHistory?