environment.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362
  1. // Copyright 2014 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. goog.provide('goog.labs.testing.Environment');
  15. goog.require('goog.Thenable');
  16. goog.require('goog.array');
  17. goog.require('goog.asserts');
  18. goog.require('goog.debug.Console');
  19. goog.require('goog.testing.MockClock');
  20. goog.require('goog.testing.MockControl');
  21. goog.require('goog.testing.PropertyReplacer');
  22. goog.require('goog.testing.TestCase');
  23. goog.require('goog.testing.jsunit');
  24. /**
  25. * JsUnit environments allow developers to customize the existing testing
  26. * lifecycle by hitching additional setUp and tearDown behaviors to tests.
  27. *
  28. * Environments will run their setUp steps in the order in which they
  29. * are instantiated and registered. During tearDown, the environments will
  30. * unwind the setUp and execute in reverse order.
  31. *
  32. * See http://go/jsunit-env for more information.
  33. */
  34. goog.labs.testing.Environment = goog.defineClass(null, {
  35. /** @constructor */
  36. constructor: function() {
  37. var testcase = goog.labs.testing.EnvironmentTestCase_.getInstance();
  38. testcase.registerEnvironment_(this);
  39. // Record the active test case, in normal usage this is a singleton,
  40. // but while testing this case it is reset.
  41. goog.labs.testing.Environment.activeTestCase_ = testcase;
  42. /** @type {goog.testing.MockControl} */
  43. this.mockControl = null;
  44. /** @type {goog.testing.MockClock} */
  45. this.mockClock = null;
  46. /** @private {boolean} */
  47. this.shouldMakeMockControl_ = false;
  48. /** @private {boolean} */
  49. this.shouldMakeMockClock_ = false;
  50. /** @const {!goog.debug.Console} */
  51. this.console = goog.labs.testing.Environment.console_;
  52. /** @const {!goog.testing.PropertyReplacer} */
  53. this.replacer = new goog.testing.PropertyReplacer();
  54. },
  55. /** Runs immediately before the setUpPage phase of JsUnit tests. */
  56. setUpPage: function() {
  57. if (this.mockClock && this.mockClock.isDisposed()) {
  58. this.mockClock = new goog.testing.MockClock(true);
  59. }
  60. },
  61. /** Runs immediately after the tearDownPage phase of JsUnit tests. */
  62. tearDownPage: function() {
  63. // If we created the mockClock, we'll also dispose it.
  64. if (this.shouldMakeMockClock_) {
  65. this.mockClock.dispose();
  66. }
  67. },
  68. /** Runs immediately before the setUp phase of JsUnit tests. */
  69. setUp: goog.nullFunction,
  70. /** Runs immediately after the tearDown phase of JsUnit tests. */
  71. tearDown: function() {
  72. // Make sure promises and other stuff that may still be scheduled, get a
  73. // chance to run (and throw errors).
  74. if (this.mockClock) {
  75. for (var i = 0; i < 100; i++) {
  76. this.mockClock.tick(1000);
  77. }
  78. // If we created the mockClock, we'll also reset it.
  79. if (this.shouldMakeMockClock_) {
  80. this.mockClock.reset();
  81. }
  82. }
  83. // Reset all changes made by the PropertyReplacer.
  84. this.replacer.reset();
  85. // Make sure the user did not forget to call $replayAll & $verifyAll in
  86. // their test. This is a noop if they did.
  87. // This is important because:
  88. // - Engineers thinks that not all their tests need to replay and verify.
  89. // That lets tests sneak in that call mocks but never replay those calls.
  90. // - Then some well meaning maintenance engineer wants to update the test
  91. // with some new mock, adds a replayAll and BOOM the test fails
  92. // because completely unrelated mocks now get replayed.
  93. if (this.mockControl) {
  94. try {
  95. this.mockControl.$verifyAll();
  96. this.mockControl.$replayAll();
  97. this.mockControl.$verifyAll();
  98. } finally {
  99. this.mockControl.$resetAll();
  100. if (this.shouldMakeMockControl_) {
  101. // If we created the mockControl, we'll also tear it down.
  102. this.mockControl.$tearDown();
  103. }
  104. }
  105. }
  106. // Verifying the mockControl may throw, so if cleanup needs to happen,
  107. // add it further up in the function.
  108. },
  109. /**
  110. * Create a new {@see goog.testing.MockControl} accessible via
  111. * {@code env.mockControl} for each test. If your test has more than one
  112. * testing environment, don't call this on more than one of them.
  113. * @return {!goog.labs.testing.Environment} For chaining.
  114. */
  115. withMockControl: function() {
  116. if (!this.shouldMakeMockControl_) {
  117. this.shouldMakeMockControl_ = true;
  118. this.mockControl = new goog.testing.MockControl();
  119. }
  120. return this;
  121. },
  122. /**
  123. * Create a {@see goog.testing.MockClock} for each test. The clock will be
  124. * installed (override i.e. setTimeout) by default. It can be accessed
  125. * using {@code env.mockClock}. If your test has more than one testing
  126. * environment, don't call this on more than one of them.
  127. * @return {!goog.labs.testing.Environment} For chaining.
  128. */
  129. withMockClock: function() {
  130. if (!this.shouldMakeMockClock_) {
  131. this.shouldMakeMockClock_ = true;
  132. this.mockClock = new goog.testing.MockClock(true);
  133. }
  134. return this;
  135. },
  136. /**
  137. * Creates a basic strict mock of a {@code toMock}. For more advanced mocking,
  138. * please use the MockControl directly.
  139. * @param {Function} toMock
  140. * @return {!goog.testing.StrictMock}
  141. */
  142. mock: function(toMock) {
  143. if (!this.shouldMakeMockControl_) {
  144. throw new Error(
  145. 'MockControl not available on this environment. ' +
  146. 'Call withMockControl if this environment is expected ' +
  147. 'to contain a MockControl.');
  148. }
  149. return this.mockControl.createStrictMock(toMock);
  150. }
  151. });
  152. /**
  153. * @private {?goog.testing.TestCase}
  154. */
  155. goog.labs.testing.Environment.activeTestCase_ = null;
  156. // TODO(johnlenz): make this package private when it moves out of labs.
  157. /**
  158. * @return {?goog.testing.TestCase}
  159. */
  160. goog.labs.testing.Environment.getTestCaseIfActive = function() {
  161. return goog.labs.testing.Environment.activeTestCase_;
  162. };
  163. /** @private @const {!goog.debug.Console} */
  164. goog.labs.testing.Environment.console_ = new goog.debug.Console();
  165. // Activate logging to the browser's console by default.
  166. goog.labs.testing.Environment.console_.setCapturing(true);
  167. /**
  168. * An internal TestCase used to hook environments into the JsUnit test runner.
  169. * Environments cannot be used in conjunction with custom TestCases for JsUnit.
  170. * @private @final @constructor
  171. * @extends {goog.testing.TestCase}
  172. */
  173. goog.labs.testing.EnvironmentTestCase_ = function() {
  174. goog.labs.testing.EnvironmentTestCase_.base(this, 'constructor');
  175. /** @private {!Array<!goog.labs.testing.Environment>}> */
  176. this.environments_ = [];
  177. /** @private {!Object} */
  178. this.testobj_ = goog.global; // default
  179. // Automatically install this TestCase when any environment is used in a test.
  180. goog.testing.TestCase.initializeTestRunner(this);
  181. };
  182. goog.inherits(goog.labs.testing.EnvironmentTestCase_, goog.testing.TestCase);
  183. goog.addSingletonGetter(goog.labs.testing.EnvironmentTestCase_);
  184. /**
  185. * @param {!Object} obj An object providing the test and life cycle methods.
  186. * @override
  187. */
  188. goog.labs.testing.EnvironmentTestCase_.prototype.setTestObj = function(obj) {
  189. goog.asserts.assert(
  190. this.testobj_ == goog.global,
  191. 'A test method object has already been provided ' +
  192. 'and only one is supported.');
  193. this.testobj_ = obj;
  194. goog.labs.testing.EnvironmentTestCase_.base(this, 'setTestObj', obj);
  195. };
  196. /**
  197. * Override the default global scope discovery of lifecycle functions to prevent
  198. * overriding the custom environment setUp(Page)/tearDown(Page) logic.
  199. * @override
  200. */
  201. goog.labs.testing.EnvironmentTestCase_.prototype.autoDiscoverLifecycle =
  202. function() {
  203. if (this.testobj_['runTests']) {
  204. this.runTests = goog.bind(this.testobj_['runTests'], this.testobj_);
  205. }
  206. if (this.testobj_['shouldRunTests']) {
  207. this.shouldRunTests =
  208. goog.bind(this.testobj_['shouldRunTests'], this.testobj_);
  209. }
  210. };
  211. /**
  212. * Adds an environment to the JsUnit test.
  213. * @param {!goog.labs.testing.Environment} env
  214. * @private
  215. */
  216. goog.labs.testing.EnvironmentTestCase_.prototype.registerEnvironment_ =
  217. function(env) {
  218. this.environments_.push(env);
  219. };
  220. /** @override */
  221. goog.labs.testing.EnvironmentTestCase_.prototype.setUpPage = function() {
  222. var setUpPageFns = goog.array.map(this.environments_, function(env) {
  223. return goog.bind(env.setUpPage, env);
  224. }, this);
  225. // User defined setUpPage method.
  226. if (this.testobj_['setUpPage']) {
  227. setUpPageFns.push(goog.bind(this.testobj_['setUpPage'], this.testobj_));
  228. }
  229. return this.callAndChainPromises_(setUpPageFns);
  230. };
  231. /** @override */
  232. goog.labs.testing.EnvironmentTestCase_.prototype.setUp = function() {
  233. var setUpFns = [];
  234. // User defined configure method.
  235. if (this.testobj_['configureEnvironment']) {
  236. setUpFns.push(
  237. goog.bind(this.testobj_['configureEnvironment'], this.testobj_));
  238. }
  239. goog.array.forEach(this.environments_, function(env) {
  240. setUpFns.push(goog.bind(env.setUp, env));
  241. }, this);
  242. // User defined setUp method.
  243. if (this.testobj_['setUp']) {
  244. setUpFns.push(goog.bind(this.testobj_['setUp'], this.testobj_));
  245. }
  246. return this.callAndChainPromises_(setUpFns);
  247. };
  248. /**
  249. * Calls a chain of methods and makes sure to properly chain them if any of the
  250. * methods returns a thenable.
  251. * @param {!Array<function()>} fns
  252. * @return {!goog.Thenable|undefined}
  253. * @private
  254. */
  255. goog.labs.testing.EnvironmentTestCase_.prototype.callAndChainPromises_ =
  256. function(fns) {
  257. return goog.array.reduce(fns, function(previousResult, fn) {
  258. if (goog.Thenable.isImplementedBy(previousResult)) {
  259. return previousResult.then(function() {
  260. return fn();
  261. });
  262. }
  263. return fn();
  264. }, undefined /* initialValue */, this);
  265. };
  266. /** @override */
  267. goog.labs.testing.EnvironmentTestCase_.prototype.tearDown = function() {
  268. var firstException;
  269. // User defined tearDown method.
  270. if (this.testobj_['tearDown']) {
  271. try {
  272. this.testobj_['tearDown']();
  273. } catch (e) {
  274. if (!firstException) {
  275. firstException = e || new Error('Exception thrown: ' + String(e));
  276. }
  277. }
  278. }
  279. // Execute the tearDown methods for the environment in the reverse order
  280. // in which they were registered to "unfold" the setUp.
  281. goog.array.forEachRight(this.environments_, function(env) {
  282. // For tearDowns between tests make sure they run as much as possible to
  283. // avoid interference between tests.
  284. try {
  285. env.tearDown();
  286. } catch (e) {
  287. if (!firstException) {
  288. firstException = e || new Error('Exception thrown: ' + String(e));
  289. }
  290. }
  291. });
  292. if (firstException) {
  293. throw firstException;
  294. }
  295. };
  296. /** @override */
  297. goog.labs.testing.EnvironmentTestCase_.prototype.tearDownPage = function() {
  298. // User defined tearDownPage method.
  299. if (this.testobj_['tearDownPage']) {
  300. this.testobj_['tearDownPage']();
  301. }
  302. goog.array.forEachRight(
  303. this.environments_, function(env) { env.tearDownPage(); });
  304. };