name: temporal-ts-testing-suite description: > Use when writing tests for Temporal TypeScript Workflows and Activities, including unit tests, integration tests, time-skipping tests, mocking Activities, testing heartbeats and cancellation, asserting in Workflow context, and replaying Workflow Event Histories for determinism checks. Covers TestWorkflowEnvironment, MockActivityEnvironment, and Worker replay APIs from @temporalio/testing. version: 1.0.0
Temporal TypeScript Testing Suite
Instructions for writing tests for Temporal TypeScript Workflows and Activities using @temporalio/testing, including unit tests, integration tests, time-skipping, mocking, and replay-based determinism checks.
When to Use
Use this skill when you need to:
- Test an Activity function in isolation with
MockActivityEnvironment - Test a Workflow with real or mocked Activities using
TestWorkflowEnvironment - Test workflows that use long sleeps or timers via time-skipping
- Verify Workflow determinism after code changes with replay APIs
- Test heartbeat behavior or cancellation handling in Activities
- Assert inside Workflow sandbox code with
workflowInterceptorModules
| Scenario | Tool |
|---|---|
| Test an Activity function in isolation | MockActivityEnvironment |
| Test a Workflow with real or mocked Activities | TestWorkflowEnvironment + Worker |
| Test a Workflow that uses long sleeps or timers | Time-skipping (createTimeSkipping) |
| Verify Workflow determinism after code changes | Worker.runReplayHistory / runReplayHistories |
| Test heartbeat behavior in an Activity | MockActivityEnvironment + on('heartbeat') |
| Test Activity cancellation handling | MockActivityEnvironment.cancel() |
| Assert inside Workflow sandbox code | workflowInterceptorModules |
How to Use
Environment Context Gathering
Before writing or running tests, confirm the following with the user:
- Where is Temporal running? (local dev server, Docker Compose, Kubernetes, Temporal Cloud)
- If not local, what is the full Temporal server URL? (e.g.,
my-namespace.tmprl.cloud:7233) - Is ingress set up for the Temporal HTTP API? (needed if tests must reach the server from CI or a remote machine)
For local testing with TestWorkflowEnvironment, no external server is needed -- it starts an in-memory test server automatically.
Overview
The @temporalio/testing package provides:
- TestWorkflowEnvironment -- an in-memory Temporal server with time-skipping support for Workflow integration tests.
- MockActivityEnvironment -- a lightweight harness to run Activities in isolation with mocked context, heartbeat listening, and cancellation.
- Replay APIs --
Worker.runReplayHistoryandWorker.runReplayHistoriesto verify Workflow determinism against saved Event Histories.
Install:
npm install @temporalio/testing
Quick Reference
| API | Import | Purpose |
|---|---|---|
TestWorkflowEnvironment.createTimeSkipping() |
@temporalio/testing |
Start in-memory test server with time skip |
testEnv.client |
(from env) | Client connected to test server |
testEnv.nativeConnection |
(from env) | NativeConnection for creating Workers |
testEnv.sleep(duration) |
(from env) | Manually advance test server time |
testEnv.teardown() |
(from env) | Shut down test server |
MockActivityEnvironment |
@temporalio/testing |
Run Activity with mocked context |
env.run(activityFn, ...args) |
(from MockActivityEnvironment) | Execute Activity function |
env.cancel() |
(from MockActivityEnvironment) | Cancel the Activity context |
env.on('heartbeat', cb) |
(from MockActivityEnvironment) | Listen for heartbeat events |
Worker.runReplayHistory(opts, history) |
@temporalio/worker |
Replay single Event History |
Worker.runReplayHistories(opts, histories) |
@temporalio/worker |
Replay multiple Event Histories |
workflowInterceptorModules |
@temporalio/testing |
Convert AssertionError to ApplicationFailure |
Implementation Patterns
1. Test Server Setup (Jest)
Create one TestWorkflowEnvironment per test suite. Reuse it across tests.
import { TestWorkflowEnvironment } from '@temporalio/testing';
import { Worker } from '@temporalio/worker';
import { v4 as uuid4 } from 'uuid';
let testEnv: TestWorkflowEnvironment;
beforeAll(async () => {
testEnv = await TestWorkflowEnvironment.createTimeSkipping();
});
afterAll(async () => {
await testEnv?.teardown();
});
Jest config requirement: must set testEnvironment: 'node'. jsdom is NOT supported. Minimum Jest version: 27.0.0.
2. Integration Test with Worker
import { myWorkflow } from './workflows';
test('myWorkflow returns expected result', async () => {
const taskQueue = 'test-' + uuid4();
const worker = await Worker.create({
connection: testEnv.nativeConnection,
taskQueue,
workflowsPath: require.resolve('./workflows'),
});
const result = await worker.runUntil(
testEnv.client.workflow.execute(myWorkflow, {
workflowId: uuid4(),
taskQueue,
}),
);
expect(result).toEqual('expected');
});
3. Mock Activities
Provide partial mock implementations to the Worker instead of real Activities.
import type * as activities from './activities';
const mockActivities: Partial<typeof activities> = {
makeHTTPRequest: async () => '99',
};
const worker = await Worker.create({
connection: testEnv.nativeConnection,
taskQueue: 'test',
workflowsPath: require.resolve('./workflows'),
activities: mockActivities,
});
4. Test Activity in Isolation
import { MockActivityEnvironment } from '@temporalio/testing';
const env = new MockActivityEnvironment({ attempt: 2 });
const result = await env.run(myActivity, arg1, arg2);
expect(result).toBe(expectedValue);
Pass partial ActivityInfo fields to the constructor as needed (e.g., { attempt: 2 }).
5. Listen to Heartbeats
const env = new MockActivityEnvironment();
const heartbeats: unknown[] = [];
env.on('heartbeat', (detail: unknown) => {
heartbeats.push(detail);
});
await env.run(myHeartbeatingActivity);
expect(heartbeats).toEqual([1, 2, 3]);
Note: MockActivityEnvironment does NOT throttle heartbeats (unlike a real Worker).
6. Test Activity Cancellation
import { CancelledFailure } from '@temporalio/activity';
const env = new MockActivityEnvironment();
env.on('heartbeat', () => {
env.cancel(); // cancel after first heartbeat
});
await expect(env.run(myCancellableActivity)).rejects.toThrow(CancelledFailure);
7. Automatic Time Skipping
When using testEnv.client.workflow.execute() or .result(), the test server automatically fast-forwards through sleep() calls and condition() timeouts. Activities run in normal time.
// Workflow has: await sleep('1 day')
// This test completes in milliseconds, not 1 day
const result = await worker.runUntil(
testEnv.client.workflow.execute(sleeperWorkflow, {
workflowId: uuid4(),
taskQueue: 'test',
}),
);
8. Manual Time Skipping
Use .start() instead of .execute() to keep the server in normal time mode, then call testEnv.sleep() to advance time manually. Useful for testing intermediate states.
const handle = await testEnv.client.workflow.start(myWorkflow, {
workflowId: uuid4(),
taskQueue: 'test',
});
worker.run(); // don't await -- let it run in background
// Advance time and check intermediate state
await testEnv.sleep('25 hours');
const days = await handle.query(daysQuery);
expect(days).toBe(1);
9. Skip Time Inside Mock Activities
Call testEnv.sleep() from within a mock Activity to simulate long-running work while allowing the Workflow's timers to fire.
const mockActivities = {
async processOrder() {
await testEnv.sleep('2 days'); // Workflow timers fire during this
},
async sendNotificationEmail() {
emailSent = true;
},
};
10. Assert in Workflow Context
By default, a failed assert in Workflow code causes infinite Workflow Task retries. Use workflowInterceptorModules to convert AssertionError into ApplicationFailure that fails the Workflow Execution.
import { workflowInterceptorModules } from '@temporalio/testing';
const worker = await Worker.create({
connection: testEnv.nativeConnection,
interceptors: {
workflowModules: workflowInterceptorModules,
},
workflowsPath: require.resolve('./workflows'),
});
11. Replay a Single Event History
import { Worker } from '@temporalio/worker';
import fs from 'fs';
const history = JSON.parse(await fs.promises.readFile('./history.json', 'utf8'));
await Worker.runReplayHistory(
{ workflowsPath: require.resolve('./workflows') },
history,
);
// Throws DeterminismViolationError if code is incompatible
12. Replay in Bulk (CI Pipeline)
const executions = client.workflow.list({
query: 'TaskQueue="my-queue" and StartTime > "2024-01-01T00:00:00"',
});
const histories = executions.intoHistories();
const results = Worker.runReplayHistories(
{ workflowsPath: require.resolve('./workflows') },
histories,
);
for await (const result of results) {
if (result.error) {
console.error('Replay failed', result);
process.exitCode = 1;
}
}
13. Test Non-Workflow Functions in Workflow Context
Point workflowsPath at the file containing the function, then execute it as if it were a Workflow.
const worker = await Worker.create({
connection: testEnv.nativeConnection,
workflowsPath: require.resolve('./workflows/helper-functions'),
});
const result = await worker.runUntil(
testEnv.client.workflow.execute(functionToTest, {
workflowId: uuid4(),
taskQueue: 'test',
}),
);
If the function starts a Child Workflow, that child Workflow must be re-exported from the same file.
Common Mistakes
| Mistake | Fix |
|---|---|
Using testEnvironment: 'jsdom' in Jest config |
Set testEnvironment: 'node' |
Creating a new TestWorkflowEnvironment per test |
Create once in beforeAll, share across tests |
Forgetting await testEnv.teardown() in afterAll |
Always teardown to release resources |
Using worker.run() and then calling .execute() |
Use worker.runUntil(client.workflow.execute(...)) so the Worker shuts down after the Workflow completes |
Calling .execute() or .result() when you want manual time control |
Use .start() instead, then call testEnv.sleep() to advance time |
Failed assert in Workflow causing infinite Task retries |
Add workflowInterceptorModules to Worker interceptors |
| Mocking only some Activities but Worker expects all | Use Partial<typeof activities> -- only mock what the test needs |
| Not re-exporting Child Workflows from test file | Child Workflows must be exported from the same file as workflowsPath |
Replaying with wrong workflowsPath |
Point to the file that exports the exact Workflow Definition matching the history |
Using same taskQueue string across parallel tests |
Generate unique task queue names per test (e.g., 'test-' + uuid4()) |
Debugging Tips
Test hangs indefinitely: Likely the Worker is not connected to the test server. Ensure you pass
testEnv.nativeConnectiontoWorker.create().Time-skipping not working: Verify you called
TestWorkflowEnvironment.createTimeSkipping(). If using manual time skip, make sure you did NOT call.execute()or.result()(those trigger automatic skip mode).DeterminismViolationError on replay: Your Workflow code changed in a non-backward-compatible way. Use Temporal versioning (
patched/deprecatePatch) to handle differences.ReplayError (not DeterminismViolationError): Something other than non-determinism went wrong -- check that the history file is valid JSON and matches the expected format.
Activity mock not being called: Confirm the
taskQueuein the Worker matches the one in the Workflow execution options.MockActivityEnvironment heartbeat not throttled: This is by design. The mock does not throttle; real Workers do. Do not rely on heartbeat timing in mock tests.
Time is global: Time skipping applies to ALL running tests in the same
TestWorkflowEnvironmentinstance. Run tests that need different time behaviors in series or use separate environments.
Further Reading
If this skill does not answer your question, use Context7 to search /temporalio/sdk-typescript or /temporalio/samples-typescript for more details. Useful queries:
- "How to set up TestWorkflowEnvironment for integration tests"
- "MockActivityEnvironment usage examples"
- "Workflow replay and determinism testing"
- "Time skipping test patterns in TypeScript"