Path: blob/trunk/third_party/closure/goog/testing/mock.js
4506 views
/**1* @license2* Copyright The Closure Library Authors.3* SPDX-License-Identifier: Apache-2.04*/56/**7* @fileoverview This file defines base classes used for creating mocks in8* JavaScript. The API was inspired by EasyMock.9*10* The basic API is:11* <ul>12* <li>Create an object to be mocked13* <li>Create a mock object, passing in the above object to the constructor14* <li>Set expectations by calling methods on the mock object15* <li>Call $replay() on the mock object16* <li>Pass the mock to code that will make real calls on it17* <li>Call $verify() to make sure that expectations were met18* </ul>19*20* For examples, please see the unit tests for LooseMock and StrictMock.21*22* Still TODO23* implement better (and pluggable) argument matching24* Have the exceptions for LooseMock show the number of expected/actual calls25* loose and strict mocks share a lot of code - move it to the base class26*/2728goog.setTestOnly('goog.testing.Mock');29goog.provide('goog.testing.Mock');30goog.provide('goog.testing.MockExpectation');3132goog.require('goog.Promise');33goog.require('goog.asserts');34goog.require('goog.object');35goog.require('goog.promise.Resolver');36goog.require('goog.testing.JsUnitException');37goog.require('goog.testing.MockInterface');38goog.require('goog.testing.mockmatchers');39404142/**43* This is a class that represents an expectation.44* @param {string} name The name of the method for this expectation.45* @constructor46* @final47*/48goog.testing.MockExpectation = function(name) {49'use strict';50/**51* The name of the method that is expected to be called.52* @type {string}53*/54this.name = name;5556/**57* An array of error messages for expectations not met.58* @type {Array<string>}59*/60this.errorMessages = [];61};626364/**65* The minimum number of times this method should be called.66* @type {number}67*/68goog.testing.MockExpectation.prototype.minCalls = 1;697071/**72* The maximum number of times this method should be called.73* @type {number}74*/75goog.testing.MockExpectation.prototype.maxCalls = 1;767778/**79* The value that this method should return.80* @type {*}81*/82goog.testing.MockExpectation.prototype.returnValue;838485/**86* The value that will be thrown when the method is called87* @type {*}88*/89goog.testing.MockExpectation.prototype.exceptionToThrow;909192/**93* The arguments that are expected to be passed to this function94* @type {Array<*>}95*/96goog.testing.MockExpectation.prototype.argumentList;979899/**100* The number of times this method is called by real code.101* @type {number}102*/103goog.testing.MockExpectation.prototype.actualCalls = 0;104105106/**107* The number of times this method is called during the verification phase.108* @type {number}109*/110goog.testing.MockExpectation.prototype.verificationCalls = 0;111112113/**114* The function which will be executed when this method is called.115* Method arguments will be passed to this function, and return value116* of this function will be returned by the method.117* @type {Function}118*/119goog.testing.MockExpectation.prototype.toDo;120121122/**123* Allow expectation failures to include messages.124* @param {string} message The failure message.125*/126goog.testing.MockExpectation.prototype.addErrorMessage = function(message) {127'use strict';128this.errorMessages.push(message);129};130131132/**133* Get the error messages seen so far.134* @return {string} Error messages separated by \n.135*/136goog.testing.MockExpectation.prototype.getErrorMessage = function() {137'use strict';138return this.errorMessages.join('\n');139};140141142/**143* Get how many error messages have been seen so far.144* @return {number} Count of error messages.145*/146goog.testing.MockExpectation.prototype.getErrorMessageCount = function() {147'use strict';148return this.errorMessages.length;149};150151152153/**154* The base class for a mock object.155* @param {Object|Function} objectToMock The object that should be mocked, or156* the constructor of an object to mock.157* @param {boolean=} opt_mockStaticMethods An optional argument denoting that158* a mock should be constructed from the static functions of a class.159* @param {boolean=} opt_createProxy An optional argument denoting that160* a proxy for the target mock should be created.161* @constructor162* @implements {goog.testing.MockInterface}163*/164goog.testing.Mock = function(165objectToMock, opt_mockStaticMethods, opt_createProxy) {166'use strict';167if (!goog.isObject(objectToMock) && typeof objectToMock !== 'function') {168throw new Error('objectToMock must be an object or constructor.');169}170if (opt_createProxy && !opt_mockStaticMethods &&171typeof objectToMock === 'function') {172/**173* @constructor174* @final175*/176var tempCtor = function() {};177goog.inherits(tempCtor, objectToMock);178this.$proxy = new tempCtor();179} else if (180opt_createProxy && opt_mockStaticMethods &&181typeof objectToMock === 'function') {182throw new Error('Cannot create a proxy when opt_mockStaticMethods is true');183} else if (opt_createProxy && typeof objectToMock !== 'function') {184throw new Error('Must have a constructor to create a proxy');185}186187if (typeof objectToMock === 'function' && !opt_mockStaticMethods) {188this.$initializeFunctions_(objectToMock.prototype);189} else {190this.$initializeFunctions_(objectToMock);191}192this.$argumentListVerifiers_ = {};193194/** @protected {?goog.promise.Resolver<undefined>} */195this.waitingForExpectations = null;196};197198199/**200* Option that may be passed when constructing function, method, and201* constructor mocks. Indicates that the expected calls should be accepted in202* any order.203* @const204* @type {number}205*/206goog.testing.Mock.LOOSE = 1;207208209/**210* Option that may be passed when constructing function, method, and211* constructor mocks. Indicates that the expected calls should be accepted in212* the recorded order only.213* @const214* @type {number}215*/216goog.testing.Mock.STRICT = 0;217218219/**220* Asserts that a mock object is in record mode. This avoids type system errors221* from mock expectations.222*223* Usage:224*225* ```226* const record = goog.require('goog.testing.Mock.record');227*228* record(mockObject).someMethod(ignoreArgument).$returns(42);229* record(mockFunction)(ignoreArgument).$returns(42);230* ```231*232* @param {?} obj A mock in record mode.233* @return {?} The same object.234*/235goog.testing.Mock.record = function(obj) {236'use strict';237// If the user passes a method of a mock object, grab the object.238const mockObj = obj.$$mockObj ? obj.$$mockObj : obj;239goog.asserts.assert(240mockObj.$recording_ !== undefined,241'%s is not a mock. Did you pass a real object to record()?', obj);242goog.asserts.assert(243mockObj.$recording_,244'Your mock is in replay mode. You can only call record(mock) before mock.$replay()');245return obj;246};247248249/**250* This array contains the name of the functions that are part of the base251* Object prototype.252* Basically a copy of goog.object.PROTOTYPE_FIELDS_.253* @const254* @type {!Array<string>}255* @private256*/257goog.testing.Mock.OBJECT_PROTOTYPE_FIELDS_ = [258'constructor', 'hasOwnProperty', 'isPrototypeOf', 'propertyIsEnumerable',259'toLocaleString', 'toString', 'valueOf'260];261262263/**264* This array contains the name of the functions that are part of the base265* Function prototype. The restricted field 'caller' and 'arguments' are266* excluded.267* @const268* @type {!Array<string>}269* @private270*/271goog.testing.Mock.FUNCTION_PROTOTYPE_FIELDS_ = ['apply', 'bind', 'call'];272273274/**275* A proxy for the mock. This can be used for dependency injection in lieu of276* the mock if the test requires a strict instanceof check.277* @type {?Object}278*/279goog.testing.Mock.prototype.$proxy = null;280281282/**283* Map of argument name to optional argument list verifier function.284* @type {Object}285*/286goog.testing.Mock.prototype.$argumentListVerifiers_;287288289/**290* Whether or not we are in recording mode.291* @type {boolean}292* @private293*/294goog.testing.Mock.prototype.$recording_ = true;295296297/**298* The expectation currently being created. All methods that modify the299* current expectation return the Mock object for easy chaining, so this is300* where we keep track of the expectation that's currently being modified.301* @type {goog.testing.MockExpectation}302* @protected303*/304goog.testing.Mock.prototype.$pendingExpectation;305306307/**308* First exception thrown by this mock; used in $verify.309* @type {?Object}310* @private311*/312goog.testing.Mock.prototype.$threwException_ = null;313314315/**316* Initializes the functions on the mock object.317* @param {Object} objectToMock The object being mocked.318* @private319*/320goog.testing.Mock.prototype.$initializeFunctions_ = function(objectToMock) {321'use strict';322// Gets the object properties.323var enumerableProperties = goog.object.getAllPropertyNames(324objectToMock, false /* opt_includeObjectPrototype */,325false /* opt_includeFunctionPrototype */);326327if (typeof objectToMock === 'function') {328for (var i = 0; i < goog.testing.Mock.FUNCTION_PROTOTYPE_FIELDS_.length;329i++) {330var prop = goog.testing.Mock.FUNCTION_PROTOTYPE_FIELDS_[i];331// Look at b/6758711 if you're considering adding ALL properties to ALL332// mocks.333if (objectToMock[prop] !== Function.prototype[prop]) {334enumerableProperties.push(prop);335}336}337}338339// The non enumerable properties are added if they override the ones in the340// Object prototype. This is due to the fact that IE8 does not enumerate any341// of the prototype Object functions even when overridden and mocking these is342// sometimes needed.343for (var i = 0; i < goog.testing.Mock.OBJECT_PROTOTYPE_FIELDS_.length; i++) {344var prop = goog.testing.Mock.OBJECT_PROTOTYPE_FIELDS_[i];345// Look at b/6758711 if you're considering adding ALL properties to ALL346// mocks.347if (objectToMock[prop] !== Object.prototype[prop]) {348enumerableProperties.push(prop);349}350}351352// Adds the properties to the mock.353for (var i = 0; i < enumerableProperties.length; i++) {354var prop = enumerableProperties[i];355if (typeof objectToMock[prop] == 'function') {356this[prop] = goog.bind(this.$mockMethod, this, prop);357this[prop].$$mockObj = this; // Save a reference for record().358if (this.$proxy) {359this.$proxy[prop] = goog.bind(this.$mockMethod, this, prop);360}361}362}363};364365366/**367* Registers a verifier function to use when verifying method argument lists.368* @param {string} methodName The name of the method for which the verifierFn369* should be used.370* @param {Function} fn Argument list verifier function. Should take 2 argument371* arrays as arguments, and return true if they are considered equivalent.372* @return {!goog.testing.Mock} This mock object.373*/374goog.testing.Mock.prototype.$registerArgumentListVerifier = function(375methodName, fn) {376'use strict';377this.$argumentListVerifiers_[methodName] = fn;378return this;379};380381382/**383* The function that replaces all methods on the mock object.384* @param {string} name The name of the method being mocked.385* @return {*} In record mode, returns the mock object. In replay mode, returns386* whatever the creator of the mock set as the return value.387*/388goog.testing.Mock.prototype.$mockMethod = function(name) {389'use strict';390try {391// Shift off the name argument so that args contains the arguments to392// the mocked method.393var args = Array.prototype.slice.call(arguments, 1);394if (this.$recording_) {395this.$pendingExpectation = new goog.testing.MockExpectation(name);396this.$pendingExpectation.argumentList = args;397this.$recordExpectation();398return this;399} else {400return this.$recordCall(name, args);401}402} catch (ex) {403this.$recordAndThrow(ex, true /* rethrow */);404}405};406407408/**409* Records the currently pending expectation, intended to be overridden by a410* subclass.411* @protected412*/413goog.testing.Mock.prototype.$recordExpectation = function() {};414415416/**417* Records an actual method call, intended to be overridden by a418* subclass. The subclass must find the pending expectation and return the419* correct value.420* @param {string} name The name of the method being called.421* @param {Array<?>} args The arguments to the method.422* @return {*} The return expected by the mock.423* @protected424*/425goog.testing.Mock.prototype.$recordCall = function(name, args) {426'use strict';427return undefined;428};429430431/**432* If the expectation expects to throw, this method will throw.433* @param {goog.testing.MockExpectation} expectation The expectation.434*/435goog.testing.Mock.prototype.$maybeThrow = function(expectation) {436'use strict';437if (typeof expectation.exceptionToThrow != 'undefined') {438throw expectation.exceptionToThrow;439}440};441442443/**444* If this expectation defines a function to be called,445* it will be called and its result will be returned.446* Otherwise, if the expectation expects to throw, it will throw.447* Otherwise, this method will return defined value.448* @param {goog.testing.MockExpectation} expectation The expectation.449* @param {Array<?>} args The arguments to the method.450* @return {*} The return value expected by the mock.451*/452goog.testing.Mock.prototype.$do = function(expectation, args) {453'use strict';454if (typeof expectation.toDo == 'undefined') {455this.$maybeThrow(expectation);456return expectation.returnValue;457} else {458return expectation.toDo.apply(this, args);459}460};461462463/**464* Specifies a return value for the currently pending expectation.465* @param {*} val The return value.466* @return {!goog.testing.Mock} This mock object.467*/468goog.testing.Mock.prototype.$returns = function(val) {469'use strict';470this.$pendingExpectation.returnValue = val;471return this;472};473474475/**476* Specifies a value for the currently pending expectation to throw.477* @param {*} val The value to throw.478* @return {!goog.testing.Mock} This mock object.479*/480goog.testing.Mock.prototype.$throws = function(val) {481'use strict';482this.$pendingExpectation.exceptionToThrow = val;483return this;484};485486487/**488* Specifies a function to call for currently pending expectation.489* Note, that using this method overrides declarations made490* using $returns() and $throws() methods.491* @param {Function} func The function to call.492* @return {!goog.testing.Mock} This mock object.493*/494goog.testing.Mock.prototype.$does = function(func) {495'use strict';496this.$pendingExpectation.toDo = func;497return this;498};499500501/**502* Allows the expectation to be called 0 or 1 times.503* @return {!goog.testing.Mock} This mock object.504*/505goog.testing.Mock.prototype.$atMostOnce = function() {506'use strict';507this.$pendingExpectation.minCalls = 0;508this.$pendingExpectation.maxCalls = 1;509return this;510};511512513/**514* Allows the expectation to be called any number of times, as long as it's515* called once.516* @return {!goog.testing.Mock} This mock object.517*/518goog.testing.Mock.prototype.$atLeastOnce = function() {519'use strict';520this.$pendingExpectation.maxCalls = Infinity;521return this;522};523524525/**526* Allows the expectation to be called exactly once.527* @return {!goog.testing.Mock} This mock object.528*/529goog.testing.Mock.prototype.$once = function() {530'use strict';531this.$pendingExpectation.minCalls = 1;532this.$pendingExpectation.maxCalls = 1;533return this;534};535536537/**538* Disallows the expectation from being called.539* @return {!goog.testing.Mock} This mock object.540*/541goog.testing.Mock.prototype.$never = function() {542'use strict';543this.$pendingExpectation.minCalls = 0;544this.$pendingExpectation.maxCalls = 0;545return this;546};547548549/**550* Allows the expectation to be called any number of times.551* @return {!goog.testing.Mock} This mock object.552*/553goog.testing.Mock.prototype.$anyTimes = function() {554'use strict';555this.$pendingExpectation.minCalls = 0;556this.$pendingExpectation.maxCalls = Infinity;557return this;558};559560561/**562* Specifies the number of times the expectation should be called.563* @param {number} times The number of times this method will be called.564* @return {!goog.testing.Mock} This mock object.565*/566goog.testing.Mock.prototype.$times = function(times) {567'use strict';568this.$pendingExpectation.minCalls = times;569this.$pendingExpectation.maxCalls = times;570return this;571};572573574/**575* Switches from recording to replay mode.576* @override577*/578goog.testing.Mock.prototype.$replay = function() {579'use strict';580this.$recording_ = false;581};582583584/**585* Resets the state of this mock object. This clears all pending expectations586* without verifying, and puts the mock in recording mode.587* @override588*/589goog.testing.Mock.prototype.$reset = function() {590'use strict';591this.$recording_ = true;592this.$threwException_ = null;593delete this.$pendingExpectation;594if (this.waitingForExpectations) {595this.waitingForExpectations = null;596}597};598599600/**601* Throws an exception and records that an exception was thrown.602* @param {string} comment A short comment about the exception.603* @param {?string=} opt_message A longer message about the exception.604* @throws {Object} JsUnitException object.605* @protected606*/607goog.testing.Mock.prototype.$throwException = function(comment, opt_message) {608'use strict';609this.$recordAndThrow(new goog.testing.JsUnitException(comment, opt_message));610};611612613/**614* Throws an exception and records that an exception was thrown.615* @param {Object} ex Exception.616* @param {boolean=} rethrow True if this exception has already been thrown. If617* so, we should not report it to TestCase (since it was already reported at618* the original throw). This is necessary to avoid logging it twice, because619* assertThrowsJsUnitException only removes one record.620* @throws {Object} #ex.621* @protected622*/623goog.testing.Mock.prototype.$recordAndThrow = function(ex, rethrow) {624'use strict';625if (this.waitingForExpectations) {626this.waitingForExpectations.resolve();627}628if (this.$recording_) {629ex = new goog.testing.JsUnitException(630'Threw an exception while in record mode, did you $replay?',631ex.toString());632}633// If it's an assert exception, record it.634if (ex['isJsUnitException']) {635if (!this.$threwException_) {636// Only remember first exception thrown.637this.$threwException_ = ex;638}639640// Don't fail if JSUnit isn't loaded. Instead, the test can catch the error641// normally. Other test frameworks won't get automatic failures if assertion642// errors are swallowed.643var getTestCase =644goog.getObjectByName('goog.testing.TestCase.getActiveTestCase');645var testCase = getTestCase && getTestCase();646if (testCase && !rethrow) {647testCase.raiseAssertionException(ex);648}649}650throw ex;651};652653654/** @override */655goog.testing.Mock.prototype.$waitAndVerify = function() {656'use strict';657goog.asserts.assert(658!this.$recording_,659'$waitAndVerify should be called after recording calls.');660this.waitingForExpectations = goog.Promise.withResolver();661var verify = goog.bind(this.$verify, this);662return this.waitingForExpectations.promise.then(function() {663'use strict';664return new goog.Promise(function(resolve, reject) {665'use strict';666setTimeout(function() {667'use strict';668try {669verify();670} catch (e) {671reject(e);672}673resolve();674}, 0);675});676});677};678679680/**681* Verify that all of the expectations were met. Should be overridden by682* subclasses.683* @override684*/685goog.testing.Mock.prototype.$verify = function() {686'use strict';687if (this.$threwException_) {688throw this.$threwException_;689}690};691692693/**694* Verifies that a method call matches an expectation.695* @param {goog.testing.MockExpectation} expectation The expectation to check.696* @param {string} name The name of the called method.697* @param {Array<*>?} args The arguments passed to the mock.698* @return {boolean} Whether the call matches the expectation.699*/700goog.testing.Mock.prototype.$verifyCall = function(expectation, name, args) {701'use strict';702if (expectation.name != name) {703return false;704}705var verifierFn =706this.$argumentListVerifiers_.hasOwnProperty(expectation.name) ?707this.$argumentListVerifiers_[expectation.name] :708goog.testing.mockmatchers.flexibleArrayMatcher;709710return verifierFn(expectation.argumentList, args, expectation);711};712713714/**715* Render the provided argument array to a string to help716* clients with debugging tests.717* @param {Array<*>?} args The arguments passed to the mock.718* @return {string} Human-readable string.719*/720goog.testing.Mock.prototype.$argumentsAsString = function(args) {721'use strict';722var retVal = [];723for (var i = 0; i < args.length; i++) {724try {725retVal.push(goog.typeOf(args[i]));726} catch (e) {727retVal.push('[unknown]');728}729}730return '(' + retVal.join(', ') + ')';731};732733734/**735* Throw an exception based on an incorrect method call.736* @param {string} name Name of method called.737* @param {Array<*>?} args Arguments passed to the mock.738* @param {goog.testing.MockExpectation=} opt_expectation Expected next call,739* if any.740*/741goog.testing.Mock.prototype.$throwCallException = function(742name, args, opt_expectation) {743'use strict';744var errorStringBuffer = [];745var actualArgsString = this.$argumentsAsString(args);746var expectedArgsString = opt_expectation ?747this.$argumentsAsString(opt_expectation.argumentList) :748'';749750if (opt_expectation && opt_expectation.name == name) {751errorStringBuffer.push(752'Bad arguments to ', name, '().\n', 'Actual: ', actualArgsString, '\n',753'Expected: ', expectedArgsString, '\n',754opt_expectation.getErrorMessage());755} else {756errorStringBuffer.push(757'Unexpected call to ', name, actualArgsString, '.',758'\nDid you forget to $replay?');759if (opt_expectation) {760errorStringBuffer.push(761'\nNext expected call was to ', opt_expectation.name,762expectedArgsString);763}764}765this.$throwException(errorStringBuffer.join(''));766};767768769