@temporalio/testing
Version:
Temporal.io SDK Testing sub-package
323 lines • 14.6 kB
JavaScript
"use strict";
/**
* `npm i @temporalio/testing`
*
* Testing library for the SDK.
*
* [Documentation](https://docs.temporal.io/typescript/testing)
*
* @module
*/
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.MockActivityEnvironment = exports.defaultActivityInfo = exports.TestWorkflowEnvironment = exports.workflowInterceptorModules = exports.TimeSkippingWorkflowClient = void 0;
require("abort-controller/polyfill"); // eslint-disable-line import/no-unassigned-import
const node_path_1 = __importDefault(require("node:path"));
const node_events_1 = __importDefault(require("node:events"));
const client_1 = require("@temporalio/client");
const common_1 = require("@temporalio/common");
const time_1 = require("@temporalio/common/lib/time");
const worker_1 = require("@temporalio/worker");
const logger_1 = require("@temporalio/worker/lib/logger");
const activity_1 = require("@temporalio/worker/lib/activity");
const core_bridge_1 = require("@temporalio/core-bridge");
const internal_non_workflow_1 = require("@temporalio/common/lib/internal-non-workflow");
const connection_1 = require("./connection");
/**
* A client with the exact same API as the "normal" client with 1 exception,
* When this client waits on a Workflow's result, it will enable time skipping
* in the test server.
*/
class TimeSkippingWorkflowClient extends client_1.WorkflowClient {
constructor(options) {
super(options);
this.enableTimeSkipping = options.enableTimeSkipping;
this.testService = options.connection.testService;
}
/**
* Gets the result of a Workflow execution.
*
* @see {@link WorkflowClient.result}
*/
async result(workflowId, runId, opts) {
if (this.enableTimeSkipping) {
await this.testService.unlockTimeSkipping({});
try {
return await super.result(workflowId, runId, opts);
}
finally {
await this.testService.lockTimeSkipping({});
}
}
else {
return await super.result(workflowId, runId, opts);
}
}
}
exports.TimeSkippingWorkflowClient = TimeSkippingWorkflowClient;
/**
* A client with the exact same API as the "normal" client with one exception:
* when `TestEnvClient.workflow` (an instance of {@link TimeSkippingWorkflowClient}) waits on a Workflow's result, it will enable time skipping
* in the Test Server.
*/
class TestEnvClient extends client_1.Client {
constructor(options) {
super(options);
// Recreate the client (this isn't optimal but it's better than adding public methods just for testing).
// NOTE: we cast to "any" to work around `workflow` being a readonly attribute.
this.workflow = new TimeSkippingWorkflowClient({
...this.workflow.options,
connection: options.connection,
enableTimeSkipping: options.enableTimeSkipping,
});
}
}
/**
* Convenience workflow interceptors
*
* Contains a single interceptor for transforming `AssertionError`s into non
* retryable `ApplicationFailure`s.
*/
exports.workflowInterceptorModules = [node_path_1.default.join(__dirname, 'assert-to-failure-interceptor')];
function addDefaults(opts) {
return {
client: {},
...opts,
};
}
/**
* An execution environment for running Workflow integration tests.
*
* Runs an external server.
* By default, the Java test server is used which supports time skipping.
*/
class TestWorkflowEnvironment {
constructor(options, supportsTimeSkipping, server, connection, nativeConnection, namespace) {
this.options = options;
this.supportsTimeSkipping = supportsTimeSkipping;
this.server = server;
/**
* Wait for `durationMs` in "server time".
*
* This awaits using regular setTimeout in regular environments, or manually skips time in time-skipping environments.
*
* Useful for simulating events far into the future like completion of long running activities.
*
* **Time skippping**:
*
* The time skippping server toggles between skipped time and normal time depending on what it needs to execute.
*
* This method is _likely_ to resolve in less than `durationMs` of "real time".
*
* @param durationMs number of milliseconds or {@link https://www.npmjs.com/package/ms | ms-formatted string}
*
* @example
*
* `workflow.ts`
*
* ```ts
* const activities = proxyActivities({ startToCloseTimeout: 2_000_000 });
*
* export async function raceActivityAndTimer(): Promise<string> {
* return await Promise.race([
* wf.sleep(500_000).then(() => 'timer'),
* activities.longRunning().then(() => 'activity'),
* ]);
* }
* ```
*
* `test.ts`
*
* ```ts
* const worker = await Worker.create({
* connection: testEnv.nativeConnection,
* activities: {
* async longRunning() {
* await testEnv.sleep(1_000_000); // <-- sleep called here
* },
* },
* // ...
* });
* ```
*/
this.sleep = async (durationMs) => {
if (this.supportsTimeSkipping) {
await this.connection.testService.unlockTimeSkippingWithSleep({ duration: (0, time_1.msToTs)(durationMs) });
}
else {
await new Promise((resolve) => setTimeout(resolve, (0, time_1.msToNumber)(durationMs)));
}
};
this.connection = connection;
this.nativeConnection = nativeConnection;
this.namespace = namespace;
this.client = new TestEnvClient({
connection,
namespace: this.namespace,
enableTimeSkipping: supportsTimeSkipping,
...options.client,
});
// eslint-disable-next-line deprecation/deprecation
this.asyncCompletionClient = this.client.activity;
// eslint-disable-next-line deprecation/deprecation
this.workflowClient = this.client.workflow;
}
/**
* Start a time skipping workflow environment.
*
* This environment automatically skips to the next events in time when a workflow handle's `result` is awaited on
* (which includes {@link WorkflowClient.execute}). Before the result is awaited on, time can be manually skipped
* forward using {@link sleep}. The currently known time can be obtained via {@link currentTimeMs}.
*
* This environment will be powered by the Temporal Time Skipping Test Server (part of the [Java SDK](https://github.com/temporalio/sdk-java)).
* Note that the Time Skipping Test Server does not support full capabilities of the regular Temporal Server, and may
* occasionally present different behaviors. For general Workflow testing, it is generally preferable to use {@link createLocal}
* instead.
*
* Users can reuse this environment for testing multiple independent workflows, but not concurrently. Time skipping,
* which is automatically done when awaiting a workflow result and manually done on sleep, is global to the
* environment, not to the workflow under test. We highly recommend running tests serially when using a single
* environment or creating a separate environment per test.
*
* By default, the latest release of the Test Serveer will be downloaded and cached to a temporary directory
* (e.g. `$TMPDIR/temporal-test-server-sdk-typescript-*` or `%TEMP%/temporal-test-server-sdk-typescript-*.exe`). Note
* that existing cached binairies will be reused without validation that they are still up-to-date, until the SDK
* itself is updated. Alternatively, a specific version number of the Test Server may be provided, or the path to an
* existing Test Server binary may be supplied; see {@link LocalTestWorkflowEnvironmentOptions.server.executable}.
*
* Note that the Test Server implementation may be changed to another one in the future. Therefore, there is no
* guarantee that Test Server options, and particularly those provided through the `extraArgs` array, will continue to
* be supported in the future.
*
* IMPORTANT: At this time, the Time Skipping Test Server is not supported on ARM platforms. Execution on Apple
* silicon Macs will work if Rosetta 2 is installed.
*/
static async createTimeSkipping(opts) {
return await this.create({
server: { type: 'time-skipping', ...opts?.server },
client: opts?.client,
supportsTimeSkipping: true,
});
}
/**
* Start a full Temporal server locally.
*
* This environment is good for testing full server capabilities, but does not support time skipping like
* {@link createTimeSkipping} does. {@link supportsTimeSkipping} will always return `false` for this environment.
* {@link sleep} will sleep the actual amount of time and {@link currentTimeMs} will return the current time.
*
* This local environment will be powered by [Temporal CLI](https://github.com/temporalio/cli), which is a
* self-contained executable for Temporal. By default, Temporal's database will not be persisted to disk, and no UI
* will be launched.
*
* By default, the latest release of the CLI will be downloaded and cached to a temporary directory
* (e.g. `$TMPDIR/temporal-sdk-typescript-*` or `%TEMP%/temporal-sdk-typescript-*.exe`). Note that existing cached
* binairies will be reused without validation that they are still up-to-date, until the SDK itself is updated.
* Alternatively, a specific version number of the CLI may be provided, or the path to an existing CLI binary may be
* supplied; see {@link LocalTestWorkflowEnvironmentOptions.server.executable}.
*
* Note that the Dev Server implementation may be changed to another one in the future. Therefore, there is no
* guarantee that Dev Server options, and particularly those provided through the `extraArgs` array, will continue to
* be supported in the future.
*/
static async createLocal(opts) {
return await this.create({
server: { type: 'dev-server', ...opts?.server },
client: opts?.client,
namespace: opts?.server?.namespace,
supportsTimeSkipping: false,
});
}
/**
* Create a new test environment
*/
static async create(opts) {
const { supportsTimeSkipping, namespace, ...rest } = opts;
const optsWithDefaults = addDefaults((0, internal_non_workflow_1.filterNullAndUndefined)(rest));
const server = await worker_1.Runtime.instance().createEphemeralServer(optsWithDefaults.server);
const address = (0, core_bridge_1.getEphemeralServerTarget)(server);
const nativeConnection = await worker_1.NativeConnection.connect({ address });
const connection = await connection_1.Connection.connect({ address });
return new this(optsWithDefaults, supportsTimeSkipping, server, connection, nativeConnection, namespace);
}
/**
* Kill the test server process and close the connection to it
*/
async teardown() {
await this.connection.close();
await this.nativeConnection.close();
await worker_1.Runtime.instance().shutdownEphemeralServer(this.server);
}
/**
* Get the current time known to this environment.
*
* For non-time-skipping environments this is simply the system time. For time-skipping environments this is whatever
* time has been skipped to.
*/
async currentTimeMs() {
if (this.supportsTimeSkipping) {
const { time } = await this.connection.testService.getCurrentTime({});
return (0, time_1.tsToMs)(time);
}
else {
return Date.now();
}
}
}
exports.TestWorkflowEnvironment = TestWorkflowEnvironment;
/**
* Used as the default activity info for Activities executed in the {@link MockActivityEnvironment}
*/
exports.defaultActivityInfo = {
attempt: 1,
taskQueue: 'test',
isLocal: false,
taskToken: Buffer.from('test'),
activityId: 'test',
activityType: 'unknown',
workflowType: 'test',
base64TaskToken: Buffer.from('test').toString('base64'),
heartbeatTimeoutMs: undefined,
heartbeatDetails: undefined,
activityNamespace: 'default',
workflowNamespace: 'default',
workflowExecution: { workflowId: 'test', runId: 'dead-beef' },
scheduledTimestampMs: 1,
startToCloseTimeoutMs: 1000,
scheduleToCloseTimeoutMs: 1000,
currentAttemptScheduledTimestampMs: 1,
};
/**
* An execution environment for testing Activities.
*
* Mocks Activity {@link Context | activity.Context} and exposes hooks for cancellation and heartbeats.
*
* Note that the `Context` object used by this environment will be reused for all activities that are run in this
* environment. Consequently, once `cancel()` is called, any further activity that gets executed in this environment
* will immediately be in a cancelled state.
*/
class MockActivityEnvironment extends node_events_1.default.EventEmitter {
constructor(info, opts) {
super();
this.cancel = () => undefined;
const heartbeatCallback = (details) => this.emit('heartbeat', details);
const loadedDataConverter = {
payloadConverter: common_1.defaultPayloadConverter,
payloadCodecs: [],
failureConverter: common_1.defaultFailureConverter,
};
this.activity = new activity_1.Activity({ ...exports.defaultActivityInfo, ...info }, undefined, loadedDataConverter, heartbeatCallback, (0, logger_1.withMetadata)(opts?.logger ?? new worker_1.DefaultLogger(), { sdkComponent: common_1.SdkComponent.worker }), opts?.interceptors ?? []);
this.context = this.activity.context;
this.cancel = this.activity.cancel;
}
/**
* Run a function in Activity Context
*/
async run(fn, ...args) {
return this.activity.runNoEncoding(fn, { args, headers: {} });
}
}
exports.MockActivityEnvironment = MockActivityEnvironment;
//# sourceMappingURL=index.js.map