# 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 |
38 | goog.provide('goog.testing.Mock');
39 | goog.provide('goog.testing.MockExpectation');
40 |
41 | goog.require('goog.array');
42 | goog.require('goog.object');
43 | goog.require('goog.testing.JsUnitException');
44 | goog.require('goog.testing.MockInterface');
45 | goog.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 | */
55 | goog.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 | */
74 | goog.testing.MockExpectation.prototype.minCalls = 1;
75 |
76 |
77 | /**
78 | * The maximum number of times this method should be called.
79 | * @type {number}
80 | */
81 | goog.testing.MockExpectation.prototype.maxCalls = 1;
82 |
83 |
84 | /**
85 | * The value that this method should return.
86 | * @type {*}
87 | */
88 | goog.testing.MockExpectation.prototype.returnValue;
89 |
90 |
91 | /**
92 | * The value that will be thrown when the method is called
93 | * @type {*}
94 | */
95 | goog.testing.MockExpectation.prototype.exceptionToThrow;
96 |
97 |
98 | /**
99 | * The arguments that are expected to be passed to this function
100 | * @type {Array<*>}
101 | */
102 | goog.testing.MockExpectation.prototype.argumentList;
103 |
104 |
105 | /**
106 | * The number of times this method is called by real code.
107 | * @type {number}
108 | */
109 | goog.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 | */
116 | goog.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 | */
125 | goog.testing.MockExpectation.prototype.toDo;
126 |
127 |
128 | /**
129 | * Allow expectation failures to include messages.
130 | * @param {string} message The failure message.
131 | */
132 | goog.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 | */
141 | goog.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 | */
150 | goog.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 | */
167 | goog.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 | */
204 | goog.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 | */
214 | goog.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 | */
225 | goog.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 | */
241 | goog.testing.Mock.prototype.$proxy = null;
242 |
243 |
244 | /**
245 | * Map of argument name to optional argument list verifier function.
246 | * @type {Object}
247 | */
248 | goog.testing.Mock.prototype.$argumentListVerifiers_;
249 |
250 |
251 | /**
252 | * Whether or not we are in recording mode.
253 | * @type {boolean}
254 | * @private
255 | */
256 | goog.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 | */
266 | goog.testing.Mock.prototype.$pendingExpectation;
267 |
268 |
269 | /**
270 | * First exception thrown by this mock; used in $verify.
271 | * @type {Object}
272 | * @private
273 | */
274 | goog.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 | */
282 | goog.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 | */
320 | goog.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 | */
333 | goog.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 | */
357 | goog.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 | */
369 | goog.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 | */
378 | goog.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 | */
394 | goog.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 | */
409 | goog.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 | */
420 | goog.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 | */
433 | goog.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 | */
443 | goog.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 | */
455 | goog.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 | */
465 | goog.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 | */
476 | goog.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 | */
487 | goog.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 | */
499 | goog.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 | */
510 | goog.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 | */
520 | goog.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 | */
534 | goog.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 | */
545 | goog.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 | */
570 | goog.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 | */
584 | goog.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 | */
603 | goog.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 | */
623 | goog.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 | };