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} |
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 | }; |