Skip to content
This repository has been archived by the owner on Aug 1, 2024. It is now read-only.

Commit

Permalink
Split off an EnvironmentBase class that does not depend on mocks or…
Browse files Browse the repository at this point in the history
… `goog.testing.asserts`.

This makes Environment-based features importable without creating globals for testing assertions.

RELNOTES: n/a
PiperOrigin-RevId: 543605712
Change-Id: I8d92f17f0691e3cee2b35ffede36f5c60d7f9210
  • Loading branch information
SLaks authored and Copybara-Service committed Jun 27, 2023
1 parent 75b5252 commit 5918355
Show file tree
Hide file tree
Showing 7 changed files with 336 additions and 277 deletions.
16 changes: 13 additions & 3 deletions closure/goog/labs/testing/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -34,14 +34,12 @@ closure_js_library(
srcs = ["environment.js"],
lenient = True,
deps = [
"//closure/goog/asserts",
":environmentbase",
"//closure/goog/debug:console",
"//closure/goog/promise:thenable",
"//closure/goog/testing:jsunit",
"//closure/goog/testing:mockclock",
"//closure/goog/testing:mockcontrol",
"//closure/goog/testing:propertyreplacer",
"//closure/goog/testing:testcase",
],
)

Expand Down Expand Up @@ -90,6 +88,18 @@ closure_js_library(
],
)

closure_js_library(
name = "environmentbase",
testonly = True,
srcs = ["environmentbase.js"],
lenient = True,
deps = [
"//closure/goog/asserts",
"//closure/goog/promise",
"//closure/goog/testing:testcase",
],
)

alias(
name = "numbermatcher",
actual = ":matchers",
Expand Down
276 changes: 6 additions & 270 deletions closure/goog/labs/testing/environment.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,7 @@ const DebugConsole = goog.require('goog.debug.Console');
const MockClock = goog.require('goog.testing.MockClock');
const MockControl = goog.require('goog.testing.MockControl');
const PropertyReplacer = goog.require('goog.testing.PropertyReplacer');
const TestCase = goog.require('goog.testing.TestCase');
const Thenable = goog.require('goog.Thenable');
const asserts = goog.require('goog.asserts');
const {EnvironmentBase} = goog.require('goog.labs.testing.EnvironmentBase');

/** @suppress {extraRequire} Declares globals */
goog.require('goog.testing.jsunit');
Expand All @@ -29,16 +27,9 @@ goog.require('goog.testing.jsunit');
*
* See http://go/jsunit-env for more information.
*/
class Environment {
class Environment extends EnvironmentBase {
constructor() {
// Use the same EnvironmentTestCase instance across all Environment objects.
if (!Environment.activeTestCase_) {
const testcase = new EnvironmentTestCase();

Environment.activeTestCase_ = testcase;
}
Environment.activeTestCase_.registerEnvironment_(this);

super();
/**
* Mocks are not type-checkable. To reduce burden on tests that are type
* checked, this is typed as "?" to turn off JSCompiler checking.
Expand All @@ -64,32 +55,28 @@ class Environment {
* Runs immediately before the setUpPage phase of JsUnit tests.
* @return {!IThenable<*>|undefined} An optional Promise which must be
* resolved before the test is executed.
* @override
*/
setUpPage() {
if (this.hasMockClock()) {
this.mockClock.install();
}
}

/** Runs immediately after the tearDownPage phase of JsUnit tests. */
/** @override Runs immediately after the tearDownPage phase of JsUnit tests */
tearDownPage() {
// If we created the mockClock, we'll also dispose it.
if (this.hasMockClock()) {
this.mockClock.uninstall();
}
}

/**
* Runs immediately before the setUp phase of JsUnit tests.
* @return {!IThenable<*>|undefined} An optional Promise which must be
* resolved before the test case is executed.
*/
setUp() {}

/**
* Runs immediately after the tearDown phase of JsUnit tests.
* @return {!IThenable<*>|undefined} An optional Promise which must be
* resolved before the next test case is executed.
* @override
*/
tearDown() {
// Make sure promises and other stuff that may still be scheduled,
Expand Down Expand Up @@ -219,261 +206,10 @@ class Environment {
}
}

/**
* @private {?EnvironmentTestCase}
*/
Environment.activeTestCase_ = null;

// TODO(johnlenz): make this package private when it moves out of labs.
/**
* @return {?TestCase}
* @nocollapse
*/
Environment.getTestCaseIfActive = function() {
return Environment.activeTestCase_;
};

/** @private @const {!DebugConsole} */
Environment.console_ = new DebugConsole();

// Activate logging to the browser's console by default.
Environment.console_.setCapturing(true);

/**
* An internal TestCase used to hook environments into the JsUnit test runner.
* Environments cannot be used in conjunction with custom TestCases for JsUnit.
* @private @final @constructor
* @extends {TestCase}
*/
function EnvironmentTestCase() {
EnvironmentTestCase.base(this, 'constructor', document.title);

/** @private {!Array<!Environment>}> */
this.environments_ = [];

/** @private {!Object} */
this.testobj_ = goog.global; // default

// Automatically install this TestCase when any environment is used in a test.
TestCase.initializeTestRunner(this);
}
goog.inherits(EnvironmentTestCase, TestCase);

/**
* Override setLifecycleObj to allow incoming test object to provide only
* runTests and shouldRunTests. The other lifecycle methods are controlled by
* this environment.
* @param {!Object} obj
* @override
*/
EnvironmentTestCase.prototype.setLifecycleObj = function(obj) {
asserts.assert(
this.testobj_ == goog.global,
'A test method object has already been provided ' +
'and only one is supported.');

// Store the test object so we can call lifecyle methods when needed.
this.testobj_ = obj;

if (this.testobj_['runTests']) {
this.runTests = this.testobj_['runTests'].bind(this.testobj_);
}
if (this.testobj_['shouldRunTests']) {
this.shouldRunTests = this.testobj_['shouldRunTests'].bind(this.testobj_);
}
};

/**
* @override
* @return {!TestCase.Test}
*/
EnvironmentTestCase.prototype.createTest = function(
name, ref, scope, objChain) {
return new EnvironmentTest(name, ref, scope, objChain);
};

/**
* Adds an environment to the JsUnit test.
* @param {!Environment} env
* @private
*/
EnvironmentTestCase.prototype.registerEnvironment_ = function(env) {
this.environments_.push(env);
};

/**
* @override
* @return {!IThenable<*>|undefined}
*/
EnvironmentTestCase.prototype.setUpPage = function() {
const setUpPageFns = this.environments_.map(env => {
return () => env.setUpPage();
});

// User defined setUpPage method.
if (this.testobj_['setUpPage']) {
setUpPageFns.push(() => this.testobj_['setUpPage']());
}
return this.callAndChainPromises_(setUpPageFns);
};

/**
* @override
* @return {!IThenable<*>|undefined}
*/
EnvironmentTestCase.prototype.setUp = function() {
const setUpFns = [];
// User defined configure method.
if (this.testobj_['configureEnvironment']) {
setUpFns.push(() => this.testobj_['configureEnvironment']());
}
const test = this.getCurrentTest();
if (test instanceof EnvironmentTest) {
setUpFns.push(...test.configureEnvironments);
}

this.environments_.forEach(env => {
setUpFns.push(() => env.setUp());
}, this);

// User defined setUp method.
if (this.testobj_['setUp']) {
setUpFns.push(() => this.testobj_['setUp']());
}
return this.callAndChainPromises_(setUpFns);
};

/**
* Calls a chain of methods and makes sure to properly chain them if any of the
* methods returns a thenable.
* @param {!Array<function()>} fns
* @param {boolean=} ensureAllFnsCalled If true, this method calls each function
* even if one of them throws an Error or returns a rejected Promise. If
* there were any Errors thrown (or Promises rejected), the first Error will
* be rethrown after all of the functions are called.
* @return {!IThenable<*>|undefined}
* @private
*/
EnvironmentTestCase.prototype.callAndChainPromises_ = function(
fns, ensureAllFnsCalled) {
// Using await here (and making callAndChainPromises_ an async method)
// causes many tests across google3 to start failing with errors like this:
// "Timed out while waiting for a promise returned from setUp to resolve".

const isThenable = (v) => Thenable.isImplementedBy(v) ||
(typeof goog.global['Promise'] === 'function' &&
v instanceof goog.global['Promise']);

// Record the first error that occurs so that it can be rethrown in the case
// where ensureAllFnsCalled is set.
let firstError;
const recordFirstError = (e) => {
if (!firstError) {
firstError = e instanceof Error ? e : new Error(e);
}
};

// Call the fns, chaining results that are Promises.
let lastFnResult;
for (const fn of fns) {
if (isThenable(lastFnResult)) {
// The previous fn was async, so chain the next fn.
const rejectedHandler = ensureAllFnsCalled ? (e) => {
recordFirstError(e);
return fn();
} : undefined;
lastFnResult = lastFnResult.then(() => fn(), rejectedHandler);
} else {
// The previous fn was not async, so simply call the next fn.
try {
lastFnResult = fn();
} catch (e) {
if (!ensureAllFnsCalled) {
throw e;
}
recordFirstError(e);
}
}
}

// After all of the fns have been called, either throw the first error if
// there was one, or otherwise return the result of the last fn.
const resultFn = () => {
if (firstError) {
throw firstError;
}
return lastFnResult;
};
return isThenable(lastFnResult) ? lastFnResult.then(resultFn, resultFn) :
resultFn();
};

/**
* @override
* @return {!IThenable<*>|undefined}
*/
EnvironmentTestCase.prototype.tearDown = function() {
const tearDownFns = [];
// User defined tearDown method.
if (this.testobj_['tearDown']) {
tearDownFns.push(() => this.testobj_['tearDown']());
}

// Execute the tearDown methods for the environment in the reverse order
// in which they were registered to "unfold" the setUp.
const reverseEnvironments = [...this.environments_].reverse();
reverseEnvironments.forEach(env => {
tearDownFns.push(() => env.tearDown());
});
// For tearDowns between tests make sure they run as much as possible to avoid
// interference between tests.
return this.callAndChainPromises_(
tearDownFns, /* ensureAllFnsCalled= */ true);
};

/** @override */
EnvironmentTestCase.prototype.tearDownPage = function() {
// User defined tearDownPage method.
if (this.testobj_['tearDownPage']) {
this.testobj_['tearDownPage']();
}

const reverseEnvironments = [...this.environments_].reverse();
reverseEnvironments.forEach(env => {
env.tearDownPage();
});
};

/**
* An internal Test used to hook environments into the JsUnit test runner.
* @param {string} name The test name.
* @param {function()} ref Reference to the test function or test object.
* @param {?Object=} scope Optional scope that the test function should be
* called in.
* @param {!Array<!Object>=} objChain A chain of objects used to populate setUps
* and tearDowns.
* @private
* @final
* @constructor
* @extends {TestCase.Test}
*/
function EnvironmentTest(name, ref, scope, objChain) {
EnvironmentTest.base(this, 'constructor', name, ref, scope, objChain);

/**
* @type {!Array<function()>}
*/
this.configureEnvironments =
(objChain || [])
.filter((obj) => typeof obj.configureEnvironment === 'function')
.map(/**
* @param {{configureEnvironment: function()}} obj
* @return {function()}
*/
function(obj) {
return obj.configureEnvironment.bind(obj);
});
}
goog.inherits(EnvironmentTest, TestCase.Test);

exports = Environment;
3 changes: 2 additions & 1 deletion closure/goog/labs/testing/environment_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const PropertyReplacer = goog.require('goog.testing.PropertyReplacer');
const TestCase = goog.require('goog.testing.TestCase');
const asserts = goog.require('goog.asserts');
const testingTestSuite = goog.require('goog.testing.testSuite');
const {EnvironmentBase} = goog.require('goog.labs.testing.EnvironmentBase');

let testCase = null;
let mockControl = null;
Expand All @@ -30,7 +31,7 @@ const env = new Environment();
function setUpTestCase() {
// Clear the activeTestCase_ field to make an instance of Environment create a
// new EnvironmentTestCase instance.
Environment.activeTestCase_ = null;
EnvironmentBase.activeTestCase_ = null;
new Environment(); // Assigns a new value to Environment.activeTestCase_.
testCase = Environment.getTestCaseIfActive();
}
Expand Down
Loading

0 comments on commit 5918355

Please sign in to comment.