2 * jQuery QUnit CompletenessTest 0.4
4 * Tests the completeness of test suites for object oriented javascript
5 * libraries. Written to be used in environments with jQuery and QUnit.
6 * Requires jQuery 1.7.2 or higher.
8 * Built for and tested with:
13 * @author Timo Tijhof, 2011-2012
19 hasOwn
= Object
.prototype.hasOwnProperty
,
20 log
= (window
.console
&& window
.console
.log
)
21 ? function () { return window
.console
.log
.apply(window
.console
, arguments
); }
24 // Simplified version of a few jQuery methods, except that they don't
25 // call other jQuery methods. Required to be able to run the CompletenessTest
26 // on jQuery itself as well.
28 keys
: Object
.keys
|| function ( object
) {
30 for ( key
in object
) {
31 if ( hasOwn
.call( object
, key
) ) {
37 each: function ( object
, callback
) {
39 for ( name
in object
) {
40 if ( callback
.call( object
[ name
], name
, object
[ name
] ) === false ) {
45 // $.type and $.isEmptyObject are safe as is, they don't call
46 // other $.* methods. Still need to be derefenced into `util`
47 // since the CompletenessTest will overload them with spies.
49 isEmptyObject
: $.isEmptyObject
57 * var myTester = new CompletenessTest( myLib );
58 * @param masterVariable {Object} The root variable that contains all object
59 * members. CompletenessTest will recursively traverse objects and keep track
61 * @param ignoreFn {Function} Optionally pass a function to filter out certain
62 * methods. Example: You may want to filter out instances of jQuery or some
63 * other constructor. Otherwise "missingTests" will include all methods that
64 * were not called from that instance.
66 function CompletenessTest( masterVariable
, ignoreFn
) {
68 // Keep track in these objects. Keyed by strings with the
69 // method names (ie. 'my.foo', 'my.bar', etc.) values are boolean true.
70 this.injectionTracker
= {};
71 this.methodCallTracker
= {};
72 this.missingTests
= {};
74 this.ignoreFn
= ignoreFn
=== undefined ? function () { return false; } : ignoreFn
;
76 // Lazy limit in case something weird happends (like recurse (part of) ourself).
77 this.lazyLimit
= 2000;
82 // Bind begin and end to QUnit.
83 QUnit
.begin( function () {
84 that
.walkTheObject( null, masterVariable
, masterVariable
, [], CompletenessTest
.ACTION_INJECT
);
85 log( 'CompletenessTest/walkTheObject/ACTION_INJECT', that
);
88 QUnit
.done( function () {
89 that
.populateMissingTests();
90 log( 'CompletenessTest/populateMissingTests', that
);
92 var toolbar
, testResults
, cntTotal
, cntCalled
, cntMissing
;
94 cntTotal
= util
.keys( that
.injectionTracker
).length
;
95 cntCalled
= util
.keys( that
.methodCallTracker
).length
;
96 cntMissing
= util
.keys( that
.missingTests
).length
;
98 function makeTestResults( blob
, title
, style
) {
99 var elOutputWrapper
, elTitle
, elContainer
, elList
, elFoot
;
101 elTitle
= document
.createElement( 'strong' );
102 elTitle
.textContent
= title
|| 'Values';
104 elList
= document
.createElement( 'ul' );
105 util
.each( blob
, function ( key
) {
106 var elItem
= document
.createElement( 'li' );
107 elItem
.textContent
= key
;
108 elList
.appendChild( elItem
);
111 elFoot
= document
.createElement( 'p' );
112 elFoot
.innerHTML
= '<em>— CompletenessTest</em>';
114 elContainer
= document
.createElement( 'div' );
115 elContainer
.appendChild( elTitle
);
116 elContainer
.appendChild( elList
);
117 elContainer
.appendChild( elFoot
);
119 elOutputWrapper
= document
.getElementById( 'qunit-completenesstest' );
120 if ( !elOutputWrapper
) {
121 elOutputWrapper
= document
.createElement( 'div' );
122 elOutputWrapper
.id
= 'qunit-completenesstest';
124 elOutputWrapper
.appendChild( elContainer
);
126 util
.each( style
, function ( key
, value
) {
127 elOutputWrapper
.style
[key
] = value
;
129 return elOutputWrapper
;
132 if ( cntMissing
=== 0 ) {
134 testResults
= makeTestResults(
136 'Detected calls to ' + cntCalled
+ '/' + cntTotal
+ ' methods. No missing tests!',
138 backgroundColor
: '#D2E0E6',
142 paddingBottom
: '1em',
148 testResults
= makeTestResults(
150 'Detected calls to ' + cntCalled
+ '/' + cntTotal
+ ' methods. ' + cntMissing
+ ' methods not covered:',
152 backgroundColor
: '#EE5757',
156 paddingBottom
: '1em',
162 toolbar
= document
.getElementById( 'qunit-testrunner-toolbar' );
164 toolbar
.insertBefore( testResults
, toolbar
.firstChild
);
172 CompletenessTest
.ACTION_INJECT
= 500;
173 CompletenessTest
.ACTION_CHECK
= 501;
176 CompletenessTest
.fn
= CompletenessTest
.prototype = {
179 * CompletenessTest.fn.walkTheObject
181 * This function recursively walks through the given object, calling itself as it goes.
182 * Depending on the action it either injects our listener into the methods, or
183 * reads from our tracker and records which methods have not been called by the test suite.
185 * @param currName {String|Null} Name of the given object member (Initially this is null).
186 * @param currVar {mixed} The variable to check (initially an object,
187 * further down it could be anything).
188 * @param masterVariable {Object} Throughout our interation, always keep track of the master/root.
189 * Initially this is the same as currVar.
190 * @param parentPathArray {Array} Array of names that indicate our breadcrumb path starting at
191 * masterVariable. Not including currName.
192 * @param action {Number} What is this function supposed to do (ACTION_INJECT or ACTION_CHECK)
194 walkTheObject: function ( currName
, currVar
, masterVariable
, parentPathArray
, action
) {
195 var key
, value
, currPathArray
,
196 type
= util
.type( currVar
),
199 currPathArray
= parentPathArray
;
201 currPathArray
.push( currName
);
205 if ( this.ignoreFn( currVar
, that
, currPathArray
) ) {
209 // Handle the lazy limit
211 if ( this.lazyCounter
> this.lazyLimit
) {
212 log( 'CompletenessTest.fn.walkTheObject> Limit reached: ' + this.lazyCounter
, currPathArray
);
217 if ( type
=== 'function' ) {
219 if ( !currVar
.prototype || util
.isEmptyObject( currVar
.prototype ) ) {
221 if ( action
=== CompletenessTest
.ACTION_INJECT
) {
223 that
.injectionTracker
[ currPathArray
.join( '.' ) ] = true;
224 that
.injectCheck( masterVariable
, currPathArray
, function () {
225 that
.methodCallTracker
[ currPathArray
.join( '.' ) ] = true;
229 // We don't support checking object constructors yet...
230 // ...we can check the prototypes fine, though.
232 if ( action
=== CompletenessTest
.ACTION_INJECT
) {
234 for ( key
in currVar
.prototype ) {
235 if ( hasOwn
.call( currVar
.prototype, key
) ) {
236 value
= currVar
.prototype[key
];
237 if ( key
=== 'constructor' ) {
241 that
.walkTheObject( key
, value
, masterVariable
, currPathArray
.concat( 'prototype' ), action
);
250 // Recursively. After all, this is the *completeness* test
251 if ( type
=== 'function' || type
=== 'object' ) {
252 for ( key
in currVar
) {
253 if ( hasOwn
.call( currVar
, key
) ) {
254 value
= currVar
[key
];
256 that
.walkTheObject( key
, value
, masterVariable
, currPathArray
.slice(), action
);
262 populateMissingTests: function () {
264 util
.each( ct
.injectionTracker
, function ( key
) {
270 * CompletenessTest.fn.hasTest
272 * Checks if the given method name (ie. 'my.foo.bar')
273 * was called during the test suite (as far as the tracker knows).
274 * If not it adds it to missingTests.
276 * @param fnName {String}
279 hasTest: function ( fnName
) {
280 if ( !( fnName
in this.methodCallTracker
) ) {
281 this.missingTests
[fnName
] = true;
288 * CompletenessTest.fn.injectCheck
290 * Injects a function (such as a spy that updates methodCallTracker when
291 * it's called) inside another function.
293 * @param masterVariable {Object}
294 * @param objectPathArray {Array}
295 * @param injectFn {Function}
297 injectCheck: function ( masterVariable
, objectPathArray
, injectFn
) {
298 var i
, len
, prev
, memberName
, lastMember
,
299 curr
= masterVariable
;
301 // Get the object in question through the path from the master variable,
302 // We can't pass the value directly because we need to re-define the object
303 // member and keep references to the parent object, member name and member
304 // value at all times.
305 for ( i
= 0, len
= objectPathArray
.length
; i
< len
; i
++ ) {
306 memberName
= objectPathArray
[i
];
309 curr
= prev
[memberName
];
310 lastMember
= memberName
;
313 // Objects are by reference, members (unless objects) are not.
314 prev
[lastMember
] = function () {
316 return curr
.apply( this, arguments
);
322 window
.CompletenessTest
= CompletenessTest
;