mockclassfactory.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588
  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. * @fileoverview This file defines a factory that can be used to mock and
  16. * replace an entire class. This allows for mocks to be used effectively with
  17. * "new" instead of having to inject all instances. Essentially, a given class
  18. * is replaced with a proxy to either a loose or strict mock. Proxies locate
  19. * the appropriate mock based on constructor arguments.
  20. *
  21. * The usage is:
  22. * <ul>
  23. * <li>Create a mock with one of the provided methods with a specifc set of
  24. * constructor arguments
  25. * <li>Set expectations by calling methods on the mock object
  26. * <li>Call $replay() on the mock object
  27. * <li>Instantiate the object as normal
  28. * <li>Call $verify() to make sure that expectations were met
  29. * <li>Call reset on the factory to revert all classes back to their original
  30. * state
  31. * </ul>
  32. *
  33. * For examples, please see the unit test.
  34. *
  35. */
  36. goog.setTestOnly('goog.testing.MockClassFactory');
  37. goog.provide('goog.testing.MockClassFactory');
  38. goog.provide('goog.testing.MockClassRecord');
  39. goog.require('goog.array');
  40. goog.require('goog.object');
  41. goog.require('goog.testing.LooseMock');
  42. goog.require('goog.testing.StrictMock');
  43. goog.require('goog.testing.TestCase');
  44. goog.require('goog.testing.mockmatchers');
  45. /**
  46. * A record that represents all the data associated with a mock replacement of
  47. * a given class.
  48. * @param {Object} namespace The namespace in which the mocked class resides.
  49. * @param {string} className The name of the class within the namespace.
  50. * @param {Function} originalClass The original class implementation before it
  51. * was replaced by a proxy.
  52. * @param {Function} proxy The proxy that replaced the original class.
  53. * @constructor
  54. * @final
  55. */
  56. goog.testing.MockClassRecord = function(
  57. namespace, className, originalClass, proxy) {
  58. /**
  59. * A standard closure namespace (e.g. goog.foo.bar) that contains the mock
  60. * class referenced by this MockClassRecord.
  61. * @type {Object}
  62. * @private
  63. */
  64. this.namespace_ = namespace;
  65. /**
  66. * The name of the class within the provided namespace.
  67. * @type {string}
  68. * @private
  69. */
  70. this.className_ = className;
  71. /**
  72. * The original class implementation.
  73. * @type {Function}
  74. * @private
  75. */
  76. this.originalClass_ = originalClass;
  77. /**
  78. * The proxy being used as a replacement for the original class.
  79. * @type {Function}
  80. * @private
  81. */
  82. this.proxy_ = proxy;
  83. /**
  84. * A mocks that will be constructed by their argument list. The entries are
  85. * objects with the format {'args': args, 'mock': mock}.
  86. * @type {Array<Object>}
  87. * @private
  88. */
  89. this.instancesByArgs_ = [];
  90. };
  91. /**
  92. * A mock associated with the static functions for a given class.
  93. * @type {goog.testing.StrictMock|goog.testing.LooseMock|null}
  94. * @private
  95. */
  96. goog.testing.MockClassRecord.prototype.staticMock_ = null;
  97. /**
  98. * A getter for this record's namespace.
  99. * @return {Object} The namespace.
  100. */
  101. goog.testing.MockClassRecord.prototype.getNamespace = function() {
  102. return this.namespace_;
  103. };
  104. /**
  105. * A getter for this record's class name.
  106. * @return {string} The name of the class referenced by this record.
  107. */
  108. goog.testing.MockClassRecord.prototype.getClassName = function() {
  109. return this.className_;
  110. };
  111. /**
  112. * A getter for the original class.
  113. * @return {Function} The original class implementation before mocking.
  114. */
  115. goog.testing.MockClassRecord.prototype.getOriginalClass = function() {
  116. return this.originalClass_;
  117. };
  118. /**
  119. * A getter for the proxy being used as a replacement for the original class.
  120. * @return {Function} The proxy.
  121. */
  122. goog.testing.MockClassRecord.prototype.getProxy = function() {
  123. return this.proxy_;
  124. };
  125. /**
  126. * A getter for the static mock.
  127. * @return {goog.testing.StrictMock|goog.testing.LooseMock|null} The static
  128. * mock associated with this record.
  129. */
  130. goog.testing.MockClassRecord.prototype.getStaticMock = function() {
  131. return this.staticMock_;
  132. };
  133. /**
  134. * A setter for the static mock.
  135. * @param {goog.testing.StrictMock|goog.testing.LooseMock} staticMock A mock to
  136. * associate with the static functions for the referenced class.
  137. */
  138. goog.testing.MockClassRecord.prototype.setStaticMock = function(staticMock) {
  139. this.staticMock_ = staticMock;
  140. };
  141. /**
  142. * Adds a new mock instance mapping. The mapping connects a set of function
  143. * arguments to a specific mock instance.
  144. * @param {Array<?>} args An array of function arguments.
  145. * @param {goog.testing.StrictMock|goog.testing.LooseMock} mock A mock
  146. * associated with the supplied arguments.
  147. */
  148. goog.testing.MockClassRecord.prototype.addMockInstance = function(args, mock) {
  149. this.instancesByArgs_.push({args: args, mock: mock});
  150. };
  151. /**
  152. * Finds the mock corresponding to a given argument set. Throws an error if
  153. * there is no appropriate match found.
  154. * @param {Array<?>} args An array of function arguments.
  155. * @return {goog.testing.StrictMock|goog.testing.LooseMock|null} The mock
  156. * corresponding to a given argument set.
  157. */
  158. goog.testing.MockClassRecord.prototype.findMockInstance = function(args) {
  159. for (var i = 0; i < this.instancesByArgs_.length; i++) {
  160. var instanceArgs = this.instancesByArgs_[i].args;
  161. if (goog.testing.mockmatchers.flexibleArrayMatcher(instanceArgs, args)) {
  162. return this.instancesByArgs_[i].mock;
  163. }
  164. }
  165. return null;
  166. };
  167. /**
  168. * Resets this record by reverting all the mocked classes back to the original
  169. * implementation and clearing out the mock instance list.
  170. */
  171. goog.testing.MockClassRecord.prototype.reset = function() {
  172. this.namespace_[this.className_] = this.originalClass_;
  173. this.instancesByArgs_ = [];
  174. };
  175. /**
  176. * A factory used to create new mock class instances. It is able to generate
  177. * both static and loose mocks. The MockClassFactory is a singleton since it
  178. * tracks the classes that have been mocked internally.
  179. * @constructor
  180. * @final
  181. */
  182. goog.testing.MockClassFactory = function() {
  183. if (goog.testing.MockClassFactory.instance_) {
  184. return goog.testing.MockClassFactory.instance_;
  185. }
  186. /**
  187. * A map from class name -> goog.testing.MockClassRecord.
  188. * @type {Object}
  189. * @private
  190. */
  191. this.mockClassRecords_ = {};
  192. goog.testing.MockClassFactory.instance_ = this;
  193. };
  194. /**
  195. * A singleton instance of the MockClassFactory.
  196. * @type {goog.testing.MockClassFactory?}
  197. * @private
  198. */
  199. goog.testing.MockClassFactory.instance_ = null;
  200. /**
  201. * The names of the fields that are defined on Object.prototype.
  202. * @type {Array<string>}
  203. * @private
  204. */
  205. goog.testing.MockClassFactory.PROTOTYPE_FIELDS_ = [
  206. 'constructor', 'hasOwnProperty', 'isPrototypeOf', 'propertyIsEnumerable',
  207. 'toLocaleString', 'toString', 'valueOf'
  208. ];
  209. /**
  210. * Iterates through a namespace to find the name of a given class. This is done
  211. * solely to support compilation since string identifiers would break down.
  212. * Tests usually aren't compiled, but the functionality is supported.
  213. * @param {Object} namespace A javascript namespace (e.g. goog.testing).
  214. * @param {Function} classToMock The class whose name should be returned.
  215. * @return {string} The name of the class.
  216. * @private
  217. */
  218. goog.testing.MockClassFactory.prototype.getClassName_ = function(
  219. namespace, classToMock) {
  220. var namespaces;
  221. if (namespace === goog.global) {
  222. namespaces = goog.testing.TestCase.getGlobals();
  223. } else {
  224. namespaces = [namespace];
  225. }
  226. for (var i = 0; i < namespaces.length; i++) {
  227. for (var prop in namespaces[i]) {
  228. if (namespaces[i][prop] === classToMock) {
  229. return prop;
  230. }
  231. }
  232. }
  233. throw Error('Class is not a part of the given namespace');
  234. };
  235. /**
  236. * Returns whether or not a given class has been mocked.
  237. * @param {string} className The name of the class.
  238. * @return {boolean} Whether or not the given class name has a MockClassRecord.
  239. * @private
  240. */
  241. goog.testing.MockClassFactory.prototype.classHasMock_ = function(className) {
  242. return !!this.mockClassRecords_[className];
  243. };
  244. /**
  245. * Returns a proxy constructor closure. Since this is a constructor, "this"
  246. * refers to the local scope of the constructed object thus bind cannot be
  247. * used.
  248. * @param {string} className The name of the class.
  249. * @param {Function} mockFinder A bound function that returns the mock
  250. * associated with a class given the constructor's argument list.
  251. * @return {!Function} A proxy constructor.
  252. * @private
  253. */
  254. goog.testing.MockClassFactory.prototype.getProxyCtor_ = function(
  255. className, mockFinder) {
  256. return function() {
  257. this.$mock_ = mockFinder(className, arguments);
  258. if (!this.$mock_) {
  259. // The "arguments" variable is not a proper Array so it must be converted.
  260. var args = Array.prototype.slice.call(arguments, 0);
  261. throw Error(
  262. 'No mock found for ' + className + ' with arguments ' +
  263. args.join(', '));
  264. }
  265. };
  266. };
  267. /**
  268. * Returns a proxy function for a mock class instance. This function cannot
  269. * be used with bind since "this" must refer to the scope of the proxy
  270. * constructor.
  271. * @param {string} fnName The name of the function that should be proxied.
  272. * @return {!Function} A proxy function.
  273. * @private
  274. */
  275. goog.testing.MockClassFactory.prototype.getProxyFunction_ = function(fnName) {
  276. return function() {
  277. return this.$mock_[fnName].apply(this.$mock_, arguments);
  278. };
  279. };
  280. /**
  281. * Find a mock instance for a given class name and argument list.
  282. * @param {string} className The name of the class.
  283. * @param {Array<?>} args The argument list to match.
  284. * @return {goog.testing.StrictMock|goog.testing.LooseMock} The mock found for
  285. * the given argument list.
  286. * @private
  287. */
  288. goog.testing.MockClassFactory.prototype.findMockInstance_ = function(
  289. className, args) {
  290. return this.mockClassRecords_[className].findMockInstance(args);
  291. };
  292. /**
  293. * Create a proxy class. A proxy will pass functions to the mock for a class.
  294. * The proxy class only covers prototype methods. A static mock is not build
  295. * simultaneously since it might be strict or loose. The proxy class inherits
  296. * from the target class in order to preserve instanceof checks.
  297. * @param {Object} namespace A javascript namespace (e.g. goog.testing).
  298. * @param {Function} classToMock The class that will be proxied.
  299. * @param {string} className The name of the class.
  300. * @return {!Function} The proxy for provided class.
  301. * @private
  302. */
  303. goog.testing.MockClassFactory.prototype.createProxy_ = function(
  304. namespace, classToMock, className) {
  305. var proxy =
  306. this.getProxyCtor_(className, goog.bind(this.findMockInstance_, this));
  307. var protoToProxy = classToMock.prototype;
  308. // Preserve base() call in mocked class
  309. var classToMockBase = classToMock.base;
  310. goog.inherits(proxy, classToMock);
  311. proxy.base = classToMockBase;
  312. for (var prop in protoToProxy) {
  313. if (goog.isFunction(protoToProxy[prop])) {
  314. proxy.prototype[prop] = this.getProxyFunction_(prop);
  315. }
  316. }
  317. // For IE the for-in-loop does not contain any properties that are not
  318. // enumerable on the prototype object (for example isPrototypeOf from
  319. // Object.prototype) and it will also not include 'replace' on objects that
  320. // extend String and change 'replace' (not that it is common for anyone to
  321. // extend anything except Object).
  322. // TODO (arv): Implement goog.object.getIterator and replace this loop.
  323. goog.array.forEach(
  324. goog.testing.MockClassFactory.PROTOTYPE_FIELDS_, function(field) {
  325. if (Object.prototype.hasOwnProperty.call(protoToProxy, field)) {
  326. proxy.prototype[field] = this.getProxyFunction_(field);
  327. }
  328. }, this);
  329. this.mockClassRecords_[className] = new goog.testing.MockClassRecord(
  330. namespace, className, classToMock, proxy);
  331. namespace[className] = proxy;
  332. return proxy;
  333. };
  334. /**
  335. * Gets either a loose or strict mock for a given class based on a set of
  336. * arguments.
  337. * @param {Object} namespace A javascript namespace (e.g. goog.testing).
  338. * @param {Function} classToMock The class that will be mocked.
  339. * @param {boolean} isStrict Whether or not the mock should be strict.
  340. * @param {IArrayLike<?>} ctorArgs The arguments associated with this
  341. * instance's constructor.
  342. * @return {!goog.testing.StrictMock|!goog.testing.LooseMock} The mock created
  343. * for the provided class.
  344. * @private
  345. */
  346. goog.testing.MockClassFactory.prototype.getMockClass_ = function(
  347. namespace, classToMock, isStrict, ctorArgs) {
  348. var className = this.getClassName_(namespace, classToMock);
  349. // The namespace and classToMock variables should be removed from the
  350. // passed in argument stack.
  351. ctorArgs = goog.array.slice(ctorArgs, 2);
  352. if (goog.isFunction(classToMock)) {
  353. var mock = isStrict ? new goog.testing.StrictMock(classToMock) :
  354. new goog.testing.LooseMock(classToMock);
  355. if (!this.classHasMock_(className)) {
  356. this.createProxy_(namespace, classToMock, className);
  357. } else {
  358. var instance = this.findMockInstance_(className, ctorArgs);
  359. if (instance) {
  360. throw Error(
  361. 'Mock instance already created for ' + className +
  362. ' with arguments ' + ctorArgs.join(', '));
  363. }
  364. }
  365. this.mockClassRecords_[className].addMockInstance(ctorArgs, mock);
  366. return mock;
  367. } else {
  368. throw Error(
  369. 'Cannot create a mock class for ' + className + ' of type ' +
  370. typeof classToMock);
  371. }
  372. };
  373. /**
  374. * Gets a strict mock for a given class.
  375. * @param {Object} namespace A javascript namespace (e.g. goog.testing).
  376. * @param {Function} classToMock The class that will be mocked.
  377. * @param {...*} var_args The arguments associated with this instance's
  378. * constructor.
  379. * @return {!goog.testing.StrictMock} The mock created for the provided class.
  380. */
  381. goog.testing.MockClassFactory.prototype.getStrictMockClass = function(
  382. namespace, classToMock, var_args) {
  383. return /** @type {!goog.testing.StrictMock} */ (
  384. this.getMockClass_(namespace, classToMock, true, arguments));
  385. };
  386. /**
  387. * Gets a loose mock for a given class.
  388. * @param {Object} namespace A javascript namespace (e.g. goog.testing).
  389. * @param {Function} classToMock The class that will be mocked.
  390. * @param {...*} var_args The arguments associated with this instance's
  391. * constructor.
  392. * @return {goog.testing.LooseMock} The mock created for the provided class.
  393. */
  394. goog.testing.MockClassFactory.prototype.getLooseMockClass = function(
  395. namespace, classToMock, var_args) {
  396. return /** @type {goog.testing.LooseMock} */ (
  397. this.getMockClass_(namespace, classToMock, false, arguments));
  398. };
  399. /**
  400. * Creates either a loose or strict mock for the static functions of a given
  401. * class.
  402. * @param {Function} classToMock The class whose static functions will be
  403. * mocked. This should be the original class and not the proxy.
  404. * @param {string} className The name of the class.
  405. * @param {Function} proxy The proxy that will replace the original class.
  406. * @param {boolean} isStrict Whether or not the mock should be strict.
  407. * @return {!goog.testing.StrictMock|!goog.testing.LooseMock} The mock created
  408. * for the static functions of the provided class.
  409. * @private
  410. */
  411. goog.testing.MockClassFactory.prototype.createStaticMock_ = function(
  412. classToMock, className, proxy, isStrict) {
  413. var mock = isStrict ? new goog.testing.StrictMock(classToMock, true) :
  414. new goog.testing.LooseMock(classToMock, false, true);
  415. for (var prop in classToMock) {
  416. if (goog.isFunction(classToMock[prop])) {
  417. proxy[prop] = goog.bind(mock.$mockMethod, mock, prop);
  418. } else if (classToMock[prop] !== classToMock.prototype) {
  419. proxy[prop] = classToMock[prop];
  420. }
  421. }
  422. this.mockClassRecords_[className].setStaticMock(mock);
  423. return mock;
  424. };
  425. /**
  426. * Gets either a loose or strict mock for the static functions of a given class.
  427. * @param {Object} namespace A javascript namespace (e.g. goog.testing).
  428. * @param {Function} classToMock The class whose static functions will be
  429. * mocked. This should be the original class and not the proxy.
  430. * @param {boolean} isStrict Whether or not the mock should be strict.
  431. * @return {goog.testing.StrictMock|goog.testing.LooseMock} The mock created
  432. * for the static functions of the provided class.
  433. * @private
  434. */
  435. goog.testing.MockClassFactory.prototype.getStaticMock_ = function(
  436. namespace, classToMock, isStrict) {
  437. var className = this.getClassName_(namespace, classToMock);
  438. if (goog.isFunction(classToMock)) {
  439. if (!this.classHasMock_(className)) {
  440. var proxy = this.createProxy_(namespace, classToMock, className);
  441. var mock =
  442. this.createStaticMock_(classToMock, className, proxy, isStrict);
  443. return mock;
  444. }
  445. if (!this.mockClassRecords_[className].getStaticMock()) {
  446. var proxy = this.mockClassRecords_[className].getProxy();
  447. var originalClass = this.mockClassRecords_[className].getOriginalClass();
  448. var mock =
  449. this.createStaticMock_(originalClass, className, proxy, isStrict);
  450. return mock;
  451. } else {
  452. var mock = this.mockClassRecords_[className].getStaticMock();
  453. var mockIsStrict = mock instanceof goog.testing.StrictMock;
  454. if (mockIsStrict != isStrict) {
  455. var mockType =
  456. mock instanceof goog.testing.StrictMock ? 'strict' : 'loose';
  457. var requestedType = isStrict ? 'strict' : 'loose';
  458. throw Error(
  459. 'Requested a ' + requestedType + ' static mock, but a ' + mockType +
  460. ' mock already exists.');
  461. }
  462. return mock;
  463. }
  464. } else {
  465. throw Error(
  466. 'Cannot create a mock for the static functions of ' + className +
  467. ' of type ' + typeof classToMock);
  468. }
  469. };
  470. /**
  471. * Gets a strict mock for the static functions of a given class.
  472. * @param {Object} namespace A javascript namespace (e.g. goog.testing).
  473. * @param {Function} classToMock The class whose static functions will be
  474. * mocked. This should be the original class and not the proxy.
  475. * @return {goog.testing.StrictMock} The mock created for the static functions
  476. * of the provided class.
  477. */
  478. goog.testing.MockClassFactory.prototype.getStrictStaticMock = function(
  479. namespace, classToMock) {
  480. return /** @type {goog.testing.StrictMock} */ (
  481. this.getStaticMock_(namespace, classToMock, true));
  482. };
  483. /**
  484. * Gets a loose mock for the static functions of a given class.
  485. * @param {Object} namespace A javascript namespace (e.g. goog.testing).
  486. * @param {Function} classToMock The class whose static functions will be
  487. * mocked. This should be the original class and not the proxy.
  488. * @return {goog.testing.LooseMock} The mock created for the static functions
  489. * of the provided class.
  490. */
  491. goog.testing.MockClassFactory.prototype.getLooseStaticMock = function(
  492. namespace, classToMock) {
  493. return /** @type {goog.testing.LooseMock} */ (
  494. this.getStaticMock_(namespace, classToMock, false));
  495. };
  496. /**
  497. * Resests the factory by reverting all mocked classes to their original
  498. * implementations and removing all MockClassRecords.
  499. */
  500. goog.testing.MockClassFactory.prototype.reset = function() {
  501. goog.object.forEach(
  502. this.mockClassRecords_, function(record) { record.reset(); });
  503. this.mockClassRecords_ = {};
  504. };