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
15 /*global jQuery, QUnit */
16 /*jshint eqeqeq:false, eqnull:false, forin:false */
21 hasOwn
= Object
.prototype.hasOwnProperty
,
22 log
= (window
.console
&& window
.console
.log
)
23 ? function () { return window
.console
.log
.apply(window
.console
, arguments
); }
26 // Simplified version of a few jQuery methods, except that they don't
27 // call other jQuery methods. Required to be able to run the CompletenessTest
28 // on jQuery itself as well.
30 keys
: Object
.keys
|| function ( object
) {
32 for ( key
in object
) {
33 if ( hasOwn
.call( object
, key
) ) {
40 var options
, name
, src
, copy
,
41 target
= arguments
[0] || {},
43 length
= arguments
.length
;
45 for ( ; i
< length
; i
++ ) {
46 options
= arguments
[ i
];
47 // Only deal with non-null/undefined values
48 if ( options
!== null && options
!== undefined ) {
49 // Extend the base object
50 for ( name
in options
) {
52 copy
= options
[ name
];
54 // Prevent never-ending loop
55 if ( target
=== copy
) {
59 if ( copy
!== undefined ) {
60 target
[ name
] = copy
;
66 // Return the modified object
69 each: function ( object
, callback
) {
71 for ( name
in object
) {
72 if ( callback
.call( object
[ name
], name
, object
[ name
] ) === false ) {
77 // $.type and $.isEmptyObject are safe as is, they don't call
78 // other $.* methods. Still need to be derefenced into `util`
79 // since the CompletenessTest will overload them with spies.
81 isEmptyObject
: $.isEmptyObject
90 * var myTester = new CompletenessTest( myLib );
91 * @param masterVariable {Object} The root variable that contains all object
92 * members. CompletenessTest will recursively traverse objects and keep track
94 * @param ignoreFn {Function} Optionally pass a function to filter out certain
95 * methods. Example: You may want to filter out instances of jQuery or some
96 * other constructor. Otherwise "missingTests" will include all methods that
97 * were not called from that instance.
99 function CompletenessTest( masterVariable
, ignoreFn
) {
101 // Keep track in these objects. Keyed by strings with the
102 // method names (ie. 'my.foo', 'my.bar', etc.) values are boolean true.
103 this.injectionTracker
= {};
104 this.methodCallTracker
= {};
105 this.missingTests
= {};
107 this.ignoreFn
= undefined === ignoreFn
? function () { return false; } : ignoreFn
;
109 // Lazy limit in case something weird happends (like recurse (part of) ourself).
110 this.lazyLimit
= 2000;
111 this.lazyCounter
= 0;
115 // Bind begin and end to QUnit.
116 QUnit
.begin( function () {
117 that
.walkTheObject( null, masterVariable
, masterVariable
, [], CompletenessTest
.ACTION_INJECT
);
118 log( 'CompletenessTest/walkTheObject/ACTION_INJECT', that
);
121 QUnit
.done( function () {
122 that
.populateMissingTests();
123 log( 'CompletenessTest/populateMissingTests', that
);
125 var toolbar
, testResults
, cntTotal
, cntCalled
, cntMissing
;
127 cntTotal
= util
.keys( that
.injectionTracker
).length
;
128 cntCalled
= util
.keys( that
.methodCallTracker
).length
;
129 cntMissing
= util
.keys( that
.missingTests
).length
;
131 function makeTestResults( blob
, title
, style
) {
132 var elOutputWrapper
, elTitle
, elContainer
, elList
, elFoot
;
134 elTitle
= document
.createElement( 'strong' );
135 elTitle
.textContent
= title
|| 'Values';
137 elList
= document
.createElement( 'ul' );
138 util
.each( blob
, function ( key
) {
139 var elItem
= document
.createElement( 'li' );
140 elItem
.textContent
= key
;
141 elList
.appendChild( elItem
);
144 elFoot
= document
.createElement( 'p' );
145 elFoot
.innerHTML
= '<em>— CompletenessTest</em>';
147 elContainer
= document
.createElement( 'div' );
148 elContainer
.appendChild( elTitle
);
149 elContainer
.appendChild( elList
);
150 elContainer
.appendChild( elFoot
);
152 elOutputWrapper
= document
.getElementById( 'qunit-completenesstest' );
153 if ( !elOutputWrapper
) {
154 elOutputWrapper
= document
.createElement( 'div' );
155 elOutputWrapper
.id
= 'qunit-completenesstest';
157 elOutputWrapper
.appendChild( elContainer
);
159 util
.each( style
, function ( key
, value
) {
160 elOutputWrapper
.style
[key
] = value
;
162 return elOutputWrapper
;
165 if ( cntMissing
=== 0 ) {
167 testResults
= makeTestResults(
169 'Detected calls to ' + cntCalled
+ '/' + cntTotal
+ ' methods. No missing tests!',
171 backgroundColor
: '#D2E0E6',
175 paddingBottom
: '1em',
181 testResults
= makeTestResults(
183 'Detected calls to ' + cntCalled
+ '/' + cntTotal
+ ' methods. ' + cntMissing
+ ' methods not covered:',
185 backgroundColor
: '#EE5757',
189 paddingBottom
: '1em',
195 toolbar
= document
.getElementById( 'qunit-testrunner-toolbar' );
197 toolbar
.insertBefore( testResults
, toolbar
.firstChild
);
205 CompletenessTest
.ACTION_INJECT
= 500;
206 CompletenessTest
.ACTION_CHECK
= 501;
209 CompletenessTest
.fn
= CompletenessTest
.prototype = {
212 * CompletenessTest.fn.walkTheObject
214 * This function recursively walks through the given object, calling itself as it goes.
215 * Depending on the action it either injects our listener into the methods, or
216 * reads from our tracker and records which methods have not been called by the test suite.
218 * @param currName {String|Null} Name of the given object member (Initially this is null).
219 * @param currVar {mixed} The variable to check (initially an object,
220 * further down it could be anything).
221 * @param masterVariable {Object} Throughout our interation, always keep track of the master/root.
222 * Initially this is the same as currVar.
223 * @param parentPathArray {Array} Array of names that indicate our breadcrumb path starting at
224 * masterVariable. Not including currName.
225 * @param action {Number} What is this function supposed to do (ACTION_INJECT or ACTION_CHECK)
227 walkTheObject: function ( currName
, currVar
, masterVariable
, parentPathArray
, action
) {
229 var key
, value
, tmpPathArray
,
230 type
= util
.type( currVar
),
234 if ( this.ignoreFn( currVar
, that
, parentPathArray
) ) {
238 // Handle the lazy limit
240 if ( this.lazyCounter
> this.lazyLimit
) {
241 log( 'CompletenessTest.fn.walkTheObject> Limit reached: ' + this.lazyCounter
, parentPathArray
);
246 if ( type
=== 'function' ) {
248 if ( !currVar
.prototype || util
.isEmptyObject( currVar
.prototype ) ) {
250 if ( action
=== CompletenessTest
.ACTION_INJECT
) {
252 that
.injectionTracker
[ parentPathArray
.join( '.' ) ] = true;
253 that
.injectCheck( masterVariable
, parentPathArray
, function () {
254 that
.methodCallTracker
[ parentPathArray
.join( '.' ) ] = true;
258 // We don't support checking object constructors yet...
259 // ...we can check the prototypes fine, though.
261 if ( action
=== CompletenessTest
.ACTION_INJECT
) {
263 for ( key
in currVar
.prototype ) {
264 if ( hasOwn
.call( currVar
.prototype, key
) ) {
265 value
= currVar
.prototype[key
];
266 if ( key
=== 'constructor' ) {
270 // Clone and break reference to parentPathArray
271 tmpPathArray
= util
.extend( [], parentPathArray
);
272 tmpPathArray
.push( 'prototype' );
273 tmpPathArray
.push( key
);
275 that
.walkTheObject( key
, value
, masterVariable
, tmpPathArray
, action
);
284 // Recursively. After all, this is the *completeness* test
285 if ( type
=== 'function' || type
=== 'object' ) {
286 for ( key
in currVar
) {
287 if ( hasOwn
.call( currVar
, key
) ) {
288 value
= currVar
[key
];
290 // Clone and break reference to parentPathArray
291 tmpPathArray
= util
.extend( [], parentPathArray
);
292 tmpPathArray
.push( key
);
294 that
.walkTheObject( key
, value
, masterVariable
, tmpPathArray
, action
);
300 populateMissingTests: function () {
302 util
.each( ct
.injectionTracker
, function ( key
) {
308 * CompletenessTest.fn.hasTest
310 * Checks if the given method name (ie. 'my.foo.bar')
311 * was called during the test suite (as far as the tracker knows).
312 * If not it adds it to missingTests.
314 * @param fnName {String}
317 hasTest: function ( fnName
) {
318 if ( !( fnName
in this.methodCallTracker
) ) {
319 this.missingTests
[fnName
] = true;
326 * CompletenessTest.fn.injectCheck
328 * Injects a function (such as a spy that updates methodCallTracker when
329 * it's called) inside another function.
331 * @param masterVariable {Object}
332 * @param objectPathArray {Array}
333 * @param injectFn {Function}
335 injectCheck: function ( masterVariable
, objectPathArray
, injectFn
) {
336 var i
, len
, prev
, memberName
, lastMember
,
337 curr
= masterVariable
;
339 // Get the object in question through the path from the master variable,
340 // We can't pass the value directly because we need to re-define the object
341 // member and keep references to the parent object, member name and member
342 // value at all times.
343 for ( i
= 0, len
= objectPathArray
.length
; i
< len
; i
++ ) {
344 memberName
= objectPathArray
[i
];
347 curr
= prev
[memberName
];
348 lastMember
= memberName
;
351 // Objects are by reference, members (unless objects) are not.
352 prev
[lastMember
] = function () {
354 return curr
.apply( this, arguments
);
360 window
.CompletenessTest
= CompletenessTest
;