lib/goog/testing/mock.js

1// Copyright 2008 The Closure Library Authors. All Rights Reserved.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS-IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15/**
16 * @fileoverview This file defines base classes used for creating mocks in
17 * JavaScript. The API was inspired by EasyMock.
18 *
19 * The basic API is:
20 * <ul>
21 * <li>Create an object to be mocked
22 * <li>Create a mock object, passing in the above object to the constructor
23 * <li>Set expectations by calling methods on the mock object
24 * <li>Call $replay() on the mock object
25 * <li>Pass the mock to code that will make real calls on it
26 * <li>Call $verify() to make sure that expectations were met
27 * </ul>
28 *
29 * For examples, please see the unit tests for LooseMock and StrictMock.
30 *
31 * Still TODO
32 * implement better (and pluggable) argument matching
33 * Have the exceptions for LooseMock show the number of expected/actual calls
34 * loose and strict mocks share a lot of code - move it to the base class
35 *
36 */
37
38goog.provide('goog.testing.Mock');
39goog.provide('goog.testing.MockExpectation');
40
41goog.require('goog.array');
42goog.require('goog.object');
43goog.require('goog.testing.JsUnitException');
44goog.require('goog.testing.MockInterface');
45goog.require('goog.testing.mockmatchers');
46
47
48
49/**
50 * This is a class that represents an expectation.
51 * @param {string} name The name of the method for this expectation.
52 * @constructor
53 * @final
54 */
55goog.testing.MockExpectation = function(name) {
56 /**
57 * The name of the method that is expected to be called.
58 * @type {string}
59 */
60 this.name = name;
61
62 /**
63 * An array of error messages for expectations not met.
64 * @type {Array<string>}
65 */
66 this.errorMessages = [];
67};
68
69
70/**
71 * The minimum number of times this method should be called.
72 * @type {number}
73 */
74goog.testing.MockExpectation.prototype.minCalls = 1;
75
76
77/**
78 * The maximum number of times this method should be called.
79 * @type {number}
80 */
81goog.testing.MockExpectation.prototype.maxCalls = 1;
82
83
84/**
85 * The value that this method should return.
86 * @type {*}
87 */
88goog.testing.MockExpectation.prototype.returnValue;
89
90
91/**
92 * The value that will be thrown when the method is called
93 * @type {*}
94 */
95goog.testing.MockExpectation.prototype.exceptionToThrow;
96
97
98/**
99 * The arguments that are expected to be passed to this function
100 * @type {Array<*>}
101 */
102goog.testing.MockExpectation.prototype.argumentList;
103
104
105/**
106 * The number of times this method is called by real code.
107 * @type {number}
108 */
109goog.testing.MockExpectation.prototype.actualCalls = 0;
110
111
112/**
113 * The number of times this method is called during the verification phase.
114 * @type {number}
115 */
116goog.testing.MockExpectation.prototype.verificationCalls = 0;
117
118
119/**
120 * The function which will be executed when this method is called.
121 * Method arguments will be passed to this function, and return value
122 * of this function will be returned by the method.
123 * @type {Function}
124 */
125goog.testing.MockExpectation.prototype.toDo;
126
127
128/**
129 * Allow expectation failures to include messages.
130 * @param {string} message The failure message.
131 */
132goog.testing.MockExpectation.prototype.addErrorMessage = function(message) {
133 this.errorMessages.push(message);
134};
135
136
137/**
138 * Get the error messages seen so far.
139 * @return {string} Error messages separated by \n.
140 */
141goog.testing.MockExpectation.prototype.getErrorMessage = function() {
142 return this.errorMessages.join('\n');
143};
144
145
146/**
147 * Get how many error messages have been seen so far.
148 * @return {number} Count of error messages.
149 */
150goog.testing.MockExpectation.prototype.getErrorMessageCount = function() {
151 return this.errorMessages.length;
152};
153
154
155
156/**
157 * The base class for a mock object.
158 * @param {Object|Function} objectToMock The object that should be mocked, or
159 * the constructor of an object to mock.
160 * @param {boolean=} opt_mockStaticMethods An optional argument denoting that
161 * a mock should be constructed from the static functions of a class.
162 * @param {boolean=} opt_createProxy An optional argument denoting that
163 * a proxy for the target mock should be created.
164 * @constructor
165 * @implements {goog.testing.MockInterface}
166 */
167goog.testing.Mock = function(objectToMock, opt_mockStaticMethods,
168 opt_createProxy) {
169 if (!goog.isObject(objectToMock) && !goog.isFunction(objectToMock)) {
170 throw new Error('objectToMock must be an object or constructor.');
171 }
172 if (opt_createProxy && !opt_mockStaticMethods &&
173 goog.isFunction(objectToMock)) {
174 /**
175 * @constructor
176 * @final
177 */
178 var tempCtor = function() {};
179 goog.inherits(tempCtor, objectToMock);
180 this.$proxy = new tempCtor();
181 } else if (opt_createProxy && opt_mockStaticMethods &&
182 goog.isFunction(objectToMock)) {
183 throw Error('Cannot create a proxy when opt_mockStaticMethods is true');
184 } else if (opt_createProxy && !goog.isFunction(objectToMock)) {
185 throw Error('Must have a constructor to create a proxy');
186 }
187
188 if (goog.isFunction(objectToMock) && !opt_mockStaticMethods) {
189 this.$initializeFunctions_(objectToMock.prototype);
190 } else {
191 this.$initializeFunctions_(objectToMock);
192 }
193 this.$argumentListVerifiers_ = {};
194};
195
196
197/**
198 * Option that may be passed when constructing function, method, and
199 * constructor mocks. Indicates that the expected calls should be accepted in
200 * any order.
201 * @const
202 * @type {number}
203 */
204goog.testing.Mock.LOOSE = 1;
205
206
207/**
208 * Option that may be passed when constructing function, method, and
209 * constructor mocks. Indicates that the expected calls should be accepted in
210 * the recorded order only.
211 * @const
212 * @type {number}
213 */
214goog.testing.Mock.STRICT = 0;
215
216
217/**
218 * This array contains the name of the functions that are part of the base
219 * Object prototype.
220 * Basically a copy of goog.object.PROTOTYPE_FIELDS_.
221 * @const
222 * @type {!Array<string>}
223 * @private
224 */
225goog.testing.Mock.PROTOTYPE_FIELDS_ = [
226 'constructor',
227 'hasOwnProperty',
228 'isPrototypeOf',
229 'propertyIsEnumerable',
230 'toLocaleString',
231 'toString',
232 'valueOf'
233];
234
235
236/**
237 * A proxy for the mock. This can be used for dependency injection in lieu of
238 * the mock if the test requires a strict instanceof check.
239 * @type {Object}
240 */
241goog.testing.Mock.prototype.$proxy = null;
242
243
244/**
245 * Map of argument name to optional argument list verifier function.
246 * @type {Object}
247 */
248goog.testing.Mock.prototype.$argumentListVerifiers_;
249
250
251/**
252 * Whether or not we are in recording mode.
253 * @type {boolean}
254 * @private
255 */
256goog.testing.Mock.prototype.$recording_ = true;
257
258
259/**
260 * The expectation currently being created. All methods that modify the
261 * current expectation return the Mock object for easy chaining, so this is
262 * where we keep track of the expectation that's currently being modified.
263 * @type {goog.testing.MockExpectation}
264 * @protected
265 */
266goog.testing.Mock.prototype.$pendingExpectation;
267
268
269/**
270 * First exception thrown by this mock; used in $verify.
271 * @type {Object}
272 * @private
273 */
274goog.testing.Mock.prototype.$threwException_ = null;
275
276
277/**
278 * Initializes the functions on the mock object.
279 * @param {Object} objectToMock The object being mocked.
280 * @private
281 */
282goog.testing.Mock.prototype.$initializeFunctions_ = function(objectToMock) {
283 // Gets the object properties.
284 var enumerableProperties = goog.object.getKeys(objectToMock);
285
286 // The non enumerable properties are added if they override the ones in the
287 // Object prototype. This is due to the fact that IE8 does not enumerate any
288 // of the prototype Object functions even when overriden and mocking these is
289 // sometimes needed.
290 for (var i = 0; i < goog.testing.Mock.PROTOTYPE_FIELDS_.length; i++) {
291 var prop = goog.testing.Mock.PROTOTYPE_FIELDS_[i];
292 // Look at b/6758711 if you're considering adding ALL properties to ALL
293 // mocks.
294 if (objectToMock[prop] !== Object.prototype[prop]) {
295 enumerableProperties.push(prop);
296 }
297 }
298
299 // Adds the properties to the mock.
300 for (var i = 0; i < enumerableProperties.length; i++) {
301 var prop = enumerableProperties[i];
302 if (typeof objectToMock[prop] == 'function') {
303 this[prop] = goog.bind(this.$mockMethod, this, prop);
304 if (this.$proxy) {
305 this.$proxy[prop] = goog.bind(this.$mockMethod, this, prop);
306 }
307 }
308 }
309};
310
311
312/**
313 * Registers a verfifier function to use when verifying method argument lists.
314 * @param {string} methodName The name of the method for which the verifierFn
315 * should be used.
316 * @param {Function} fn Argument list verifier function. Should take 2 argument
317 * arrays as arguments, and return true if they are considered equivalent.
318 * @return {!goog.testing.Mock} This mock object.
319 */
320goog.testing.Mock.prototype.$registerArgumentListVerifier = function(methodName,
321 fn) {
322 this.$argumentListVerifiers_[methodName] = fn;
323 return this;
324};
325
326
327/**
328 * The function that replaces all methods on the mock object.
329 * @param {string} name The name of the method being mocked.
330 * @return {*} In record mode, returns the mock object. In replay mode, returns
331 * whatever the creator of the mock set as the return value.
332 */
333goog.testing.Mock.prototype.$mockMethod = function(name) {
334 try {
335 // Shift off the name argument so that args contains the arguments to
336 // the mocked method.
337 var args = goog.array.slice(arguments, 1);
338 if (this.$recording_) {
339 this.$pendingExpectation = new goog.testing.MockExpectation(name);
340 this.$pendingExpectation.argumentList = args;
341 this.$recordExpectation();
342 return this;
343 } else {
344 return this.$recordCall(name, args);
345 }
346 } catch (ex) {
347 this.$recordAndThrow(ex);
348 }
349};
350
351
352/**
353 * Records the currently pending expectation, intended to be overridden by a
354 * subclass.
355 * @protected
356 */
357goog.testing.Mock.prototype.$recordExpectation = function() {};
358
359
360/**
361 * Records an actual method call, intended to be overridden by a
362 * subclass. The subclass must find the pending expectation and return the
363 * correct value.
364 * @param {string} name The name of the method being called.
365 * @param {Array<?>} args The arguments to the method.
366 * @return {*} The return expected by the mock.
367 * @protected
368 */
369goog.testing.Mock.prototype.$recordCall = function(name, args) {
370 return undefined;
371};
372
373
374/**
375 * If the expectation expects to throw, this method will throw.
376 * @param {goog.testing.MockExpectation} expectation The expectation.
377 */
378goog.testing.Mock.prototype.$maybeThrow = function(expectation) {
379 if (typeof expectation.exceptionToThrow != 'undefined') {
380 throw expectation.exceptionToThrow;
381 }
382};
383
384
385/**
386 * If this expectation defines a function to be called,
387 * it will be called and its result will be returned.
388 * Otherwise, if the expectation expects to throw, it will throw.
389 * Otherwise, this method will return defined value.
390 * @param {goog.testing.MockExpectation} expectation The expectation.
391 * @param {Array<?>} args The arguments to the method.
392 * @return {*} The return value expected by the mock.
393 */
394goog.testing.Mock.prototype.$do = function(expectation, args) {
395 if (typeof expectation.toDo == 'undefined') {
396 this.$maybeThrow(expectation);
397 return expectation.returnValue;
398 } else {
399 return expectation.toDo.apply(this, args);
400 }
401};
402
403
404/**
405 * Specifies a return value for the currently pending expectation.
406 * @param {*} val The return value.
407 * @return {!goog.testing.Mock} This mock object.
408 */
409goog.testing.Mock.prototype.$returns = function(val) {
410 this.$pendingExpectation.returnValue = val;
411 return this;
412};
413
414
415/**
416 * Specifies a value for the currently pending expectation to throw.
417 * @param {*} val The value to throw.
418 * @return {!goog.testing.Mock} This mock object.
419 */
420goog.testing.Mock.prototype.$throws = function(val) {
421 this.$pendingExpectation.exceptionToThrow = val;
422 return this;
423};
424
425
426/**
427 * Specifies a function to call for currently pending expectation.
428 * Note, that using this method overrides declarations made
429 * using $returns() and $throws() methods.
430 * @param {Function} func The function to call.
431 * @return {!goog.testing.Mock} This mock object.
432 */
433goog.testing.Mock.prototype.$does = function(func) {
434 this.$pendingExpectation.toDo = func;
435 return this;
436};
437
438
439/**
440 * Allows the expectation to be called 0 or 1 times.
441 * @return {!goog.testing.Mock} This mock object.
442 */
443goog.testing.Mock.prototype.$atMostOnce = function() {
444 this.$pendingExpectation.minCalls = 0;
445 this.$pendingExpectation.maxCalls = 1;
446 return this;
447};
448
449
450/**
451 * Allows the expectation to be called any number of times, as long as it's
452 * called once.
453 * @return {!goog.testing.Mock} This mock object.
454 */
455goog.testing.Mock.prototype.$atLeastOnce = function() {
456 this.$pendingExpectation.maxCalls = Infinity;
457 return this;
458};
459
460
461/**
462 * Allows the expectation to be called exactly once.
463 * @return {!goog.testing.Mock} This mock object.
464 */
465goog.testing.Mock.prototype.$once = function() {
466 this.$pendingExpectation.minCalls = 1;
467 this.$pendingExpectation.maxCalls = 1;
468 return this;
469};
470
471
472/**
473 * Disallows the expectation from being called.
474 * @return {!goog.testing.Mock} This mock object.
475 */
476goog.testing.Mock.prototype.$never = function() {
477 this.$pendingExpectation.minCalls = 0;
478 this.$pendingExpectation.maxCalls = 0;
479 return this;
480};
481
482
483/**
484 * Allows the expectation to be called any number of times.
485 * @return {!goog.testing.Mock} This mock object.
486 */
487goog.testing.Mock.prototype.$anyTimes = function() {
488 this.$pendingExpectation.minCalls = 0;
489 this.$pendingExpectation.maxCalls = Infinity;
490 return this;
491};
492
493
494/**
495 * Specifies the number of times the expectation should be called.
496 * @param {number} times The number of times this method will be called.
497 * @return {!goog.testing.Mock} This mock object.
498 */
499goog.testing.Mock.prototype.$times = function(times) {
500 this.$pendingExpectation.minCalls = times;
501 this.$pendingExpectation.maxCalls = times;
502 return this;
503};
504
505
506/**
507 * Switches from recording to replay mode.
508 * @override
509 */
510goog.testing.Mock.prototype.$replay = function() {
511 this.$recording_ = false;
512};
513
514
515/**
516 * Resets the state of this mock object. This clears all pending expectations
517 * without verifying, and puts the mock in recording mode.
518 * @override
519 */
520goog.testing.Mock.prototype.$reset = function() {
521 this.$recording_ = true;
522 this.$threwException_ = null;
523 delete this.$pendingExpectation;
524};
525
526
527/**
528 * Throws an exception and records that an exception was thrown.
529 * @param {string} comment A short comment about the exception.
530 * @param {?string=} opt_message A longer message about the exception.
531 * @throws {Object} JsUnitException object.
532 * @protected
533 */
534goog.testing.Mock.prototype.$throwException = function(comment, opt_message) {
535 this.$recordAndThrow(new goog.testing.JsUnitException(comment, opt_message));
536};
537
538
539/**
540 * Throws an exception and records that an exception was thrown.
541 * @param {Object} ex Exception.
542 * @throws {Object} #ex.
543 * @protected
544 */
545goog.testing.Mock.prototype.$recordAndThrow = function(ex) {
546 // If it's an assert exception, record it.
547 if (ex['isJsUnitException']) {
548 var testRunner = goog.global['G_testRunner'];
549 if (testRunner) {
550 var logTestFailureFunction = testRunner['logTestFailure'];
551 if (logTestFailureFunction) {
552 logTestFailureFunction.call(testRunner, ex);
553 }
554 }
555
556 if (!this.$threwException_) {
557 // Only remember first exception thrown.
558 this.$threwException_ = ex;
559 }
560 }
561 throw ex;
562};
563
564
565/**
566 * Verify that all of the expectations were met. Should be overridden by
567 * subclasses.
568 * @override
569 */
570goog.testing.Mock.prototype.$verify = function() {
571 if (this.$threwException_) {
572 throw this.$threwException_;
573 }
574};
575
576
577/**
578 * Verifies that a method call matches an expectation.
579 * @param {goog.testing.MockExpectation} expectation The expectation to check.
580 * @param {string} name The name of the called method.
581 * @param {Array<*>?} args The arguments passed to the mock.
582 * @return {boolean} Whether the call matches the expectation.
583 */
584goog.testing.Mock.prototype.$verifyCall = function(expectation, name, args) {
585 if (expectation.name != name) {
586 return false;
587 }
588 var verifierFn =
589 this.$argumentListVerifiers_.hasOwnProperty(expectation.name) ?
590 this.$argumentListVerifiers_[expectation.name] :
591 goog.testing.mockmatchers.flexibleArrayMatcher;
592
593 return verifierFn(expectation.argumentList, args, expectation);
594};
595
596
597/**
598 * Render the provided argument array to a string to help
599 * clients with debugging tests.
600 * @param {Array<*>?} args The arguments passed to the mock.
601 * @return {string} Human-readable string.
602 */
603goog.testing.Mock.prototype.$argumentsAsString = function(args) {
604 var retVal = [];
605 for (var i = 0; i < args.length; i++) {
606 try {
607 retVal.push(goog.typeOf(args[i]));
608 } catch (e) {
609 retVal.push('[unknown]');
610 }
611 }
612 return '(' + retVal.join(', ') + ')';
613};
614
615
616/**
617 * Throw an exception based on an incorrect method call.
618 * @param {string} name Name of method called.
619 * @param {Array<*>?} args Arguments passed to the mock.
620 * @param {goog.testing.MockExpectation=} opt_expectation Expected next call,
621 * if any.
622 */
623goog.testing.Mock.prototype.$throwCallException = function(name, args,
624 opt_expectation) {
625 var errorStringBuffer = [];
626 var actualArgsString = this.$argumentsAsString(args);
627 var expectedArgsString = opt_expectation ?
628 this.$argumentsAsString(opt_expectation.argumentList) : '';
629
630 if (opt_expectation && opt_expectation.name == name) {
631 errorStringBuffer.push('Bad arguments to ', name, '().\n',
632 'Actual: ', actualArgsString, '\n',
633 'Expected: ', expectedArgsString, '\n',
634 opt_expectation.getErrorMessage());
635 } else {
636 errorStringBuffer.push('Unexpected call to ', name,
637 actualArgsString, '.');
638 if (opt_expectation) {
639 errorStringBuffer.push('\nNext expected call was to ',
640 opt_expectation.name,
641 expectedArgsString);
642 }
643 }
644 this.$throwException(errorStringBuffer.join(''));
645};