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 ) ) {
38 var options, name, src, copy,
39 target = arguments[0] || {},
41 length = arguments.length;
43 for ( ; i < length; i++ ) {
44 options = arguments[ i ];
45 // Only deal with non-null/undefined values
46 if ( options !== null && options !== undefined ) {
47 // Extend the base object
48 for ( name in options ) {
50 copy = options[ name ];
52 // Prevent never-ending loop
53 if ( target === copy ) {
57 if ( copy !== undefined ) {
58 target[ name ] = copy;
64 // Return the modified object
67 each: function ( object, callback ) {
69 for ( name in object ) {
70 if ( callback.call( object[ name ], name, object[ name ] ) === false ) {
75 // $.type and $.isEmptyObject are safe as is, they don't call
76 // other $.* methods. Still need to be derefenced into `util`
77 // since the CompletenessTest will overload them with spies.
79 isEmptyObject: $.isEmptyObject
88 * var myTester = new CompletenessTest( myLib );
89 * @param masterVariable {Object} The root variable that contains all object
90 * members. CompletenessTest will recursively traverse objects and keep track
92 * @param ignoreFn {Function} Optionally pass a function to filter out certain
93 * methods. Example: You may want to filter out instances of jQuery or some
94 * other constructor. Otherwise "missingTests" will include all methods that
95 * were not called from that instance.
97 function CompletenessTest( masterVariable, ignoreFn ) {
99 // Keep track in these objects. Keyed by strings with the
100 // method names (ie. 'my.foo', 'my.bar', etc.) values are boolean true.
101 this.injectionTracker = {};
102 this.methodCallTracker = {};
103 this.missingTests = {};
105 this.ignoreFn = undefined === ignoreFn ? function () { return false; } : ignoreFn;
107 // Lazy limit in case something weird happends (like recurse (part of) ourself).
108 this.lazyLimit = 2000;
109 this.lazyCounter = 0;
113 // Bind begin and end to QUnit.
114 QUnit.begin( function () {
115 that.walkTheObject( null, masterVariable, masterVariable, [], CompletenessTest.ACTION_INJECT );
116 log( 'CompletenessTest/walkTheObject/ACTION_INJECT', that );
119 QUnit.done( function () {
120 that.populateMissingTests();
121 log( 'CompletenessTest/populateMissingTests', that );
123 var toolbar, testResults, cntTotal, cntCalled, cntMissing;
125 cntTotal = util.keys( that.injectionTracker ).length;
126 cntCalled = util.keys( that.methodCallTracker ).length;
127 cntMissing = util.keys( that.missingTests ).length;
129 function makeTestResults( blob, title, style ) {
130 var elOutputWrapper, elTitle, elContainer, elList, elFoot;
132 elTitle = document.createElement( 'strong' );
133 elTitle.textContent = title || 'Values';
135 elList = document.createElement( 'ul' );
136 util.each( blob, function ( key ) {
137 var elItem = document.createElement( 'li' );
138 elItem.textContent = key;
139 elList.appendChild( elItem );
142 elFoot = document.createElement( 'p' );
143 elFoot.innerHTML = '<em>— CompletenessTest</em>';
145 elContainer = document.createElement( 'div' );
146 elContainer.appendChild( elTitle );
147 elContainer.appendChild( elList );
148 elContainer.appendChild( elFoot );
150 elOutputWrapper = document.getElementById( 'qunit-completenesstest' );
151 if ( !elOutputWrapper ) {
152 elOutputWrapper = document.createElement( 'div' );
153 elOutputWrapper.id = 'qunit-completenesstest';
155 elOutputWrapper.appendChild( elContainer );
157 util.each( style, function ( key, value ) {
158 elOutputWrapper.style[key] = value;
160 return elOutputWrapper;
163 if ( cntMissing === 0 ) {
165 testResults = makeTestResults(
167 'Detected calls to ' + cntCalled + '/' + cntTotal + ' methods. No missing tests!',
169 backgroundColor: '#D2E0E6',
173 paddingBottom: '1em',
179 testResults = makeTestResults(
181 'Detected calls to ' + cntCalled + '/' + cntTotal + ' methods. ' + cntMissing + ' methods not covered:',
183 backgroundColor: '#EE5757',
187 paddingBottom: '1em',
193 toolbar = document.getElementById( 'qunit-testrunner-toolbar' );
195 toolbar.insertBefore( testResults, toolbar.firstChild );
203 CompletenessTest.ACTION_INJECT = 500;
204 CompletenessTest.ACTION_CHECK = 501;
207 CompletenessTest.fn = CompletenessTest.prototype = {
210 * CompletenessTest.fn.walkTheObject
212 * This function recursively walks through the given object, calling itself as it goes.
213 * Depending on the action it either injects our listener into the methods, or
214 * reads from our tracker and records which methods have not been called by the test suite.
216 * @param currName {String|Null} Name of the given object member (Initially this is null).
217 * @param currVar {mixed} The variable to check (initially an object,
218 * further down it could be anything).
219 * @param masterVariable {Object} Throughout our interation, always keep track of the master/root.
220 * Initially this is the same as currVar.
221 * @param parentPathArray {Array} Array of names that indicate our breadcrumb path starting at
222 * masterVariable. Not including currName.
223 * @param action {Number} What is this function supposed to do (ACTION_INJECT or ACTION_CHECK)
225 walkTheObject: function ( currName, currVar, masterVariable, parentPathArray, action ) {
227 var key, value, tmpPathArray,
228 type = util.type( currVar ),
232 if ( this.ignoreFn( currVar, that, parentPathArray ) ) {
236 // Handle the lazy limit
238 if ( this.lazyCounter > this.lazyLimit ) {
239 log( 'CompletenessTest.fn.walkTheObject> Limit reached: ' + this.lazyCounter, parentPathArray );
244 if ( type === 'function' ) {
246 if ( !currVar.prototype || util.isEmptyObject( currVar.prototype ) ) {
248 if ( action === CompletenessTest.ACTION_INJECT ) {
250 that.injectionTracker[ parentPathArray.join( '.' ) ] = true;
251 that.injectCheck( masterVariable, parentPathArray, function () {
252 that.methodCallTracker[ parentPathArray.join( '.' ) ] = true;
256 // We don't support checking object constructors yet...
257 // ...we can check the prototypes fine, though.
259 if ( action === CompletenessTest.ACTION_INJECT ) {
261 for ( key in currVar.prototype ) {
262 if ( hasOwn.call( currVar.prototype, key ) ) {
263 value = currVar.prototype[key];
264 if ( key === 'constructor' ) {
268 // Clone and break reference to parentPathArray
269 tmpPathArray = util.extend( [], parentPathArray );
270 tmpPathArray.push( 'prototype' );
271 tmpPathArray.push( key );
273 that.walkTheObject( key, value, masterVariable, tmpPathArray, action );
282 // Recursively. After all, this is the *completeness* test
283 if ( type === 'function' || type === 'object' ) {
284 for ( key in currVar ) {
285 if ( hasOwn.call( currVar, key ) ) {
286 value = currVar[key];
288 // Clone and break reference to parentPathArray
289 tmpPathArray = util.extend( [], parentPathArray );
290 tmpPathArray.push( key );
292 that.walkTheObject( key, value, masterVariable, tmpPathArray, action );
298 populateMissingTests: function () {
300 util.each( ct.injectionTracker, function ( key ) {
306 * CompletenessTest.fn.hasTest
308 * Checks if the given method name (ie. 'my.foo.bar')
309 * was called during the test suite (as far as the tracker knows).
310 * If not it adds it to missingTests.
312 * @param fnName {String}
315 hasTest: function ( fnName ) {
316 if ( !( fnName in this.methodCallTracker ) ) {
317 this.missingTests[fnName] = true;
324 * CompletenessTest.fn.injectCheck
326 * Injects a function (such as a spy that updates methodCallTracker when
327 * it's called) inside another function.
329 * @param masterVariable {Object}
330 * @param objectPathArray {Array}
331 * @param injectFn {Function}
333 injectCheck: function ( masterVariable, objectPathArray, injectFn ) {
334 var i, len, prev, memberName, lastMember,
335 curr = masterVariable;
337 // Get the object in question through the path from the master variable,
338 // We can't pass the value directly because we need to re-define the object
339 // member and keep references to the parent object, member name and member
340 // value at all times.
341 for ( i = 0, len = objectPathArray.length; i < len; i++ ) {
342 memberName = objectPathArray[i];
345 curr = prev[memberName];
346 lastMember = memberName;
349 // Objects are by reference, members (unless objects) are not.
350 prev[lastMember] = function () {
352 return curr.apply( this, arguments );
358 window.CompletenessTest = CompletenessTest;