# 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 at6//7//      http://www.apache.org/licenses/LICENSE-2.08//9// Unless required by applicable law or agreed to in writing, software10// 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 and13// limitations under the License.1415/**16 * @fileoverview This file defines base classes used for creating mocks in17 * JavaScript. The API was inspired by EasyMock.18 *19 * The basic API is:20 * <ul>21 *   <li>Create an object to be mocked22 *   <li>Create a mock object, passing in the above object to the constructor23 *   <li>Set expectations by calling methods on the mock object24 *   <li>Call $replay() on the mock object25 *   <li>Pass the mock to code that will make real calls on it26 *   <li>Call $verify() to make sure that expectations were met27 * </ul>28 *29 * For examples, please see the unit tests for LooseMock and StrictMock.30 *31 * Still TODO32 *   implement better (and pluggable) argument matching33 *   Have the exceptions for LooseMock show the number of expected/actual calls34 *   loose and strict mocks share a lot of code - move it to the base class35 *36 */3738goog.provide('goog.testing.Mock');39goog.provide('goog.testing.MockExpectation');4041goog.require('goog.array');42goog.require('goog.object');43goog.require('goog.testing.JsUnitException');44goog.require('goog.testing.MockInterface');45goog.require('goog.testing.mockmatchers');46474849/**50 * This is a class that represents an expectation.51 * @param {string} name The name of the method for this expectation.52 * @constructor53 * @final54 */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;6162  /**63   * An array of error messages for expectations not met.64   * @type {Array}65   */66  this.errorMessages = [];67};686970/**71 * The minimum number of times this method should be called.72 * @type {number}73 */74goog.testing.MockExpectation.prototype.minCalls = 1;757677/**78  * The maximum number of times this method should be called.79  * @type {number}80  */81goog.testing.MockExpectation.prototype.maxCalls = 1;828384/**85 * The value that this method should return.86 * @type {*}87 */88goog.testing.MockExpectation.prototype.returnValue;899091/**92 * The value that will be thrown when the method is called93 * @type {*}94 */95goog.testing.MockExpectation.prototype.exceptionToThrow;969798/**99 * The arguments that are expected to be passed to this function100 * @type {Array.<*>}101 */102goog.testing.MockExpectation.prototype.argumentList;103104105/**106 * The number of times this method is called by real code.107 * @type {number}108 */109goog.testing.MockExpectation.prototype.actualCalls = 0;110111112/**113 * The number of times this method is called during the verification phase.114 * @type {number}115 */116goog.testing.MockExpectation.prototype.verificationCalls = 0;117118119/**120 * The function which will be executed when this method is called.121 * Method arguments will be passed to this function, and return value122 * of this function will be returned by the method.123 * @type {Function}124 */125goog.testing.MockExpectation.prototype.toDo;126127128/**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};135136137/**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};144145146/**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};153154155156/**157 * The base class for a mock object.158 * @param {Object|Function} objectToMock The object that should be mocked, or159 *    the constructor of an object to mock.160 * @param {boolean=} opt_mockStaticMethods An optional argument denoting that161 *     a mock should be constructed from the static functions of a class.162 * @param {boolean=} opt_createProxy An optional argument denoting that163 *     a proxy for the target mock should be created.164 * @constructor165 * @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 * @constructor176 * @final177 */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  }187188  if (goog.isFunction(objectToMock) && !opt_mockStaticMethods) {189    this.$initializeFunctions_(objectToMock.prototype);190  } else {191    this.$initializeFunctions_(objectToMock);192  }193  this.$argumentListVerifiers_ = {};194};195196197/**198 * Option that may be passed when constructing function, method, and199 * constructor mocks. Indicates that the expected calls should be accepted in200 * any order.201 * @const202 * @type {number}203 */204goog.testing.Mock.LOOSE = 1;205206207/**208 * Option that may be passed when constructing function, method, and209 * constructor mocks. Indicates that the expected calls should be accepted in210 * the recorded order only.211 * @const212 * @type {number}213 */214goog.testing.Mock.STRICT = 0;215216217/**218 * This array contains the name of the functions that are part of the base219 * Object prototype.220 * Basically a copy of goog.object.PROTOTYPE_FIELDS_.221 * @const222 * @type {!Array.<string>}223 * @private224 */225goog.testing.Mock.PROTOTYPE_FIELDS_ = [226  'constructor',227  'hasOwnProperty',228  'isPrototypeOf',229  'propertyIsEnumerable',230  'toLocaleString',231  'toString',232  'valueOf'233];234235236/**237 * A proxy for the mock.  This can be used for dependency injection in lieu of238 * the mock if the test requires a strict instanceof check.239 * @type {Object}240 */241goog.testing.Mock.prototype.$proxy = null;242243244/**245 * Map of argument name to optional argument list verifier function.246 * @type {Object}247 */248goog.testing.Mock.prototype.$argumentListVerifiers_;249250251/**252 * Whether or not we are in recording mode.253 * @type {boolean}254 * @private255 */256goog.testing.Mock.prototype.$recording_ = true;257258259/**260 * The expectation currently being created. All methods that modify the261 * current expectation return the Mock object for easy chaining, so this is262 * where we keep track of the expectation that's currently being modified.263 * @type {goog.testing.MockExpectation}264 * @protected265 */266goog.testing.Mock.prototype.$pendingExpectation;267268269/**270 * First exception thrown by this mock; used in $verify.271 * @type {Object}272 * @private273 */274goog.testing.Mock.prototype.$threwException_ = null;275276277/**278 * Initializes the functions on the mock object.279 * @param {Object} objectToMock The object being mocked.280 * @private281 */282goog.testing.Mock.prototype.$initializeFunctions_ = function(objectToMock) {283  // Gets the object properties.284  var enumerableProperties = goog.object.getKeys(objectToMock);285286  // The non enumerable properties are added if they override the ones in the287  // Object prototype. This is due to the fact that IE8 does not enumerate any288  // of the prototype Object functions even when overriden and mocking these is289  // 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 ALL293    // mocks.294    if (objectToMock[prop] !== Object.prototype[prop]) {295      enumerableProperties.push(prop);296    }297  }298299  // 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};310311312/**313 * Registers a verfifier function to use when verifying method argument lists.314 * @param {string} methodName The name of the method for which the verifierFn315 *     should be used.316 * @param {Function} fn Argument list verifier function.  Should take 2 argument317 *     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};325326327/**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, returns331 *    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 to336    // 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};350351352/**353 * Records the currently pending expectation, intended to be overridden by a354 * subclass.355 * @protected356 */357goog.testing.Mock.prototype.$recordExpectation = function() {};358359360/**361 * Records an actual method call, intended to be overridden by a362 * subclass. The subclass must find the pending expectation and return the363 * 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 * @protected368 */369goog.testing.Mock.prototype.$recordCall = function(name, args) {370  return undefined;371};372373374/**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};383384385/**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};402403404/**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};413414415/**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};424425426/**427 * Specifies a function to call for currently pending expectation.428 * Note, that using this method overrides declarations made429 * 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};437438439/**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};448449450/**451 * Allows the expectation to be called any number of times, as long as it's452 * 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};459460461/**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};470471472/**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};481482483/**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};492493494/**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};504505506/**507 * Switches from recording to replay mode.508 * @override509 */510goog.testing.Mock.prototype.$replay = function() {511  this.$recording_ = false;512};513514515/**516 * Resets the state of this mock object. This clears all pending expectations517 * without verifying, and puts the mock in recording mode.518 * @override519 */520goog.testing.Mock.prototype.$reset = function() {521  this.$recording_ = true;522  this.$threwException_ = null;523  delete this.$pendingExpectation;524};525526527/**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 * @protected533 */534goog.testing.Mock.prototype.$throwException = function(comment, opt_message) {535  this.$recordAndThrow(new goog.testing.JsUnitException(comment, opt_message));536};537538539/**540 * Throws an exception and records that an exception was thrown.541 * @param {Object} ex Exception.542 * @throws {Object} #ex.543 * @protected544 */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    }555556    if (!this.$threwException_) {557      // Only remember first exception thrown.558      this.$threwException_ = ex;559    }560  }561  throw ex;562};563564565/**566 * Verify that all of the expectations were met. Should be overridden by567 * subclasses.568 * @override569 */570goog.testing.Mock.prototype.$verify = function() {571  if (this.$threwException_) {572    throw this.$threwException_;573  }574};575576577/**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;592593  return verifierFn(expectation.argumentList, args, expectation);594};595596597/**598 * Render the provided argument array to a string to help599 * 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};614615616/**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) : '';629630  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};