using-stubs
Stubbing and verification for node.js tests. Enables you to validate and override behaviour of nested pieces of code such as methods, require() and npm modules or even instances of classes.
This library is inspired on node-gently, MockJS and mock-require.
Getting started
var using = require('using-stubs')(); //an instance of using
Matching, following and stubbing in-depth code
using(object, 'method').like( foo.method("example") );
using(object, 'method').stub( function(originalFn, args, context){ /*...*/ } );
using(object, 'method').follow( function(resultingObject){ /*...*/ } );
Consider the object:
//class Bazfunction ClassExample(){ this.test = function(){ return ""; }} //object foovar foo = { bar: function(){ return { Baz: ClassExample } }, getBaz: function(){ return ClassExample }}
Basics: Dive deep into Baz class, intercept and modify returned instance:
//setup using()using(foo, 'bar').follow(function(obj){ return using(obj, 'Baz'); //chaining with .follow() }).follow(function(bazInstance){ // modify bazInstance however you'd like using(bazInstance, 'test').stub(function(){ return "OK!"; }) }); //actual test casevar obj = foo.bar(); var instance = new obj.Baz(); console.log(instance.test()); //prints "OK!"
Class constructor: Actually replace bazInstance with your mock entirely when adquired via foo.bar("test-match").getBaz() and constructed via new Baz(callback):
//setup using()using(foo, 'bar').like( foo.bar("test-match") ).follow(function(obj){ return using(obj, 'getBaz'); }).follow(function(Baz){ //Notice that because we intercept Baz itself in .follow() we can modify its entire behaviour. //This wouldn't be possible if Baz reference hadn't been piped through using.follow(), //in that case we would only be able to intercept methods of Baz, but not its instances using(Baz).like( new Baz(using.aCallback) ).stub(function(originalFn, args, context){ var callback = args[0]; callback("YEAH!"); }); }); //actual test casevar obj = foo.bar("test-match"); //anything else won't applyvar Baz = new obj.getBaz(); new Baz(function(str){ console.log(str);}) //prints "YEAH!"
Node.js require()
Intercepting require() in Node.js:
Consider the example object at the top of this documentation is encapsulated in node module example.js
//example.js // foo code// ... module.exports = foo;
Let's intercept and modify it
//using.require('./example').first().follow(function(foo){ using(foo, 'bar') .follow(function(){ console.log("HI!") }); }) //running the testvar foo = require('./example') foo.bar(); //prints "HI!"
Notes:
- The using.require() interface only has a forceful .first() API available (not a direct .follow()) - this happens because a require() in node always returns the same instance and thus .first() avoids triggering a .follow() every time a require() is called per file and avoids overriding behaviour on that instance over and over
- using will automatically pin the rule (eg. using.require(path)) to the target file given by the relative path to the current module path, meaning it will override the target module even if you require it from a different module later located on any other relative path (subfolders, etc)
- using.require(module) also works for any module (eg. 'http' - not necessarily a path) and the match in this case is pinned down to all calls to the given module string exactly (independent of module location or dependecy resolution)
Cleaning rules
Examples on removing all or part of set rules :
//clean all rules for a given methodusing(obj, 'prop').clean(); //clean all rules for classes / functionsusing(obj).clean(); //clean all rules for moduleusing.require('module').clean(); //clean everything - all rules for all objects and methods, modules, classes, etcusing.clean(); //Note: .clean() only cleans rules that have been set via the given using instance (see first item on documentation)
matchers
parameter matchers
The simplest way to exactly match a parameter is by specifying it directly.
using(foo, 'bar').like( foo.bar(5) ); //foo.bar(5); //matchesfoo.bar("5"); //does not match
Or, you can use any callback as a matcher (returning true matches)
function divisibleBy3(param){ return typeof(param)==='number' && (param % 3) === 0;} using(foo, 'bar').like( foo.bar(divisibleBy3) ); //foo.bar(6); //matchesfoo.bar(7); //does not match
Context matchers
Parameter matching even works on context
using(foo, 'bar').like( foo.bar.call(using.anObjectLike({'a':'a'}), 5) ); //foo.bar.apply({'a':'a'}, [5]); //matchesfoo.bar(5); //does not match
using-stubs provides you a few common matchers for ease of use
using.aString //matches any stringusing.aStringLike(regex) //matches the regular expressionusing.anInt //matches any integerusing.aNumber //matches any numberusing.anObject //matches any object (not null)using.aFunction //matches any functionusing.typeOf(type) //tests typeOf(parameter)===typeusing.instanceOf(Class) //tests parameter instanceOf Classusing.something //matches parameter!==undefinedusing.anything //matches any param as long as it is set in the argument list (even undefined) using.atLeast(x) //a number > xusing.atMost(x) //a number < xusing.between(x, y) //a number > x && < yusing.oneOf(a1, [[a2], ...[aN]]) //matches one of the given parametersusing.otherThan(a1, [[a2], ...[aN]]) //matches if none of the given parameters match using.anObjectLike(obj, [boolean strict]) //deep compare to given object //strict - defaults to non-strict (==) comparison (false) using.everything //special matcher - all arguments from this point onward //will be matched, even if not set in the argument list. //Eg. foo("a", using.everything) will match foo("a"), foo("a", "one") //or even foo("a", 1, 2, 3, 4, 5, 6);
Putting it all together
An example using the most relevant API methods all at once:
var scope = {}; //using most API methods in one chainusing.require('./example').first().follow(function(foo){ using(foo, 'bar') .like( foo.bar.call(scope, using.aString) ) //only when called like this .stub(function(fn, args, context){ //eg. replace a calling argument but still return the original function result return fn.call(context, "my-override-string") }) .the(2) //stalker-pattern API: only follow the 2nd match .follow(function(obj){ console.log("the(2) foo.bar()"); return using(obj, 'Baz'); //chaining with .follow() }) .first() //stalker-pattern API: only follow the first instance of obj.Baz .follow(function(bazInstance){ console.log("first() instance of obj.Baz"); return using(bazInstance, 'test'); //chaining with .follow() }) .from(2).to(5) //stalker-pattern API: only follow 2nd to 5th call to bazInstance.test() .follow(function(res){ console.log("+1"); }) }) //running the testfoo.bar("-"); //does nothing (does not match on context) var a = foo.bar.call(scope, "a"); //does nothing (1st match)var b = foo.bar.call(scope, "b"); //prints "the(2) foo.bar()" var instance = new b.Baz(); //prints "first() instance of obj.Baz" instance.test(); //does nothing instance.test(); //prints "+1"instance.test(); //prints "+1"instance.test(); //prints "+1"instance.test(); //prints "+1" instance.test(); //does nothing
Note: The interfaces .first(), .the(), .from(), .to(), and .follow() are inherited from the stalker-pattern API Take a look at stalker-pattern reference in order to further understand the pattern and its chaining API