@camunda8/process-test

Camunda Process Test for Node.js

THIS IS AN EARLY PREVIEW IN ACTIVE DEVELOPMENT

A comprehensive testing framework for Camunda process automation in Node.js/TypeScript, inspired by the Java camunda-process-test-java library.

  • ๐Ÿš€ Easy Setup: Simple decorator-based or function-based test configuration
  • ๐Ÿณ Container Management: Automatic Camunda/Zeebe container lifecycle management using TestContainers
  • ๐Ÿงน Automatic Cleanup: Smart resource cleanup with auto-deletion in REMOTE mode
  • ๐Ÿ” Rich Assertions: Fluent API for verifying process execution, user tasks, and decisions
  • ๐ŸŽญ Job Worker Mocking: Powerful mocking capabilities for service tasks
  • ๐Ÿ”ง gRPC Worker Support: Test external workers connecting via Zeebe gRPC API
  • ๐Ÿค– Automatic Worker Management: Zero-configuration worker lifecycle management - no manual cleanup needed
  • โฐ Time Control: Full control over Zeebe's internal clock for testing timers and timeouts
  • ๐Ÿ”ง TypeScript Support: Full TypeScript support with type definitions
  • ๐Ÿงช Jest Integration: Seamless integration with Jest testing framework
  • ๐Ÿ› Debug Mode: Comprehensive debugging for Docker operations and test execution

Get started with a new Camunda process test project in seconds using the built-in scaffolding command:

# Install Camunda Process Test
npm install @camunda8/process-test --save-dev

# Generate configuration files
npx @camunda8/process-test config:init

This will:

  • โœ… Auto-detect your project setup (TypeScript, Jest)
  • โœ… Generate camunda-test-config.json with sensible defaults
  • โœ… Create Jest configuration if Jest is detected
  • โœ… Provide setup instructions for missing dependencies
# Preview what files would be created without writing them
npx @camunda8/process-test config:init --dry-run

# Force Jest configuration generation even if Jest isn't detected
npx @camunda8/process-test config:init --jest

# Get help for available options
npx @camunda8/process-test config:init --help

The scaffolding tool intelligently detects your project setup:

  • TypeScript projects: Generates TypeScript-compatible Jest configuration
  • JavaScript projects: Generates standard Jest configuration
  • Existing configs: Won't overwrite existing configuration files
  • Missing dependencies: Provides helpful installation commands
npm install @camunda8/process-test --save-dev

# Peer dependencies
npm install jest @types/jest --save-dev
  • Docker Desktop - Must be running for container management in MANAGED mode
  • Camunda 8 Run - Can be used instead of Docker in REMOTE mode
  • Node.js - Version 20+
  • Jest - Version 29+ for test execution
  • Camunda 8.8.0-alpha6 - Minimum version for test execution (uses search APIs)
import { Camunda8 } from '@camunda8/sdk';
import {
setupCamundaProcessTest,
CamundaAssert
} from '@camunda8/process-test';

describe('Order Process', () => {
/**
* setupCamundaProcessTest() should be called outside test blocks, and before any beforeAll or afterAll blocks in your test.
* It will install beforeAll, beforeEach, afterAll, and afterEach hooks to manage test state
* between tests, including setting up and recycling any containers.
*/
const setup = setupCamundaProcessTest();

test('should complete order process', async () => {
/**
* getContext() returns a CamundaTestContext object. This has methods for deploying resources
* and starting process instances that track the resources and dispose of them after each test.
* This is the best practice for test isolation.
*/
const context = setup.getContext();

// Deploy process
await context.deployResources(['./processes/order-process.bpmn']);

/**
* The Mock Job Worker will complete only one job. Awaiting this means that the test will continue only
* when a job has been completed.
*
* It is important to run tests using either `--runInBand` or `maxWorkers: 1`.
* Both worker mocks and external workers are managed by the framework, and after a test runs, all running
* workers are stopped. This will cause unpredictable behaviour if two tests are running at the same time.
*/
await context.mockJobWorker('collect-money')
.thenComplete({ paid: true });

await context.mockJobWorker('ship-parcel')
.thenComplete({ tracking: 'TR123456' });

// Start process instance
const processInstance = await context.createProcessInstance({
processDefinitionId: 'order-process',
variables: { orderId: 'order-123', amount: 99.99 }
});

// Verify process execution
const assertion = CamundaAssert.assertThat(processInstance);
await assertion.isCompleted();
await assertion.hasVariables({
paid: true,
tracking: 'TR123456'
});
}, 60000); // 60 second timeout for container startup
});
import { Camunda8 } from '@camunda8/sdk';
import {
CamundaProcessTest,
CamundaAssert,
CamundaProcessTestContext
} from '@camunda8/process-test';

@CamundaProcessTest
class MyProcessTest {
private client!: Camunda8; // Automatically injected
private context!: CamundaProcessTestContext; // Automatically injected

async testOrderProcess() {
// Deploy process
await this.context.deployResources(['./processes/order-process.bpmn']);

// Mock job workers
await this.context.mockJobWorker('collect-money')
.thenComplete({ paid: true });

await this.context.mockJobWorker('ship-parcel')
.thenComplete({ tracking: 'TR123456' });

// Start process instance
const processInstance = await this.context.createProcessInstance({
processDefinitionId: 'order-process',
variables: { orderId: 'order-123', amount: 99.99 }
});

// Verify process execution
const assertion = CamundaAssert.assertThat(processInstance);
await assertion.isCompleted();
await assertion.hasVariables({
paid: true,
tracking: 'TR123456'
});
}
}

Configure the testing framework via configuration files, environment variables, and Jest configuration.

The framework supports simple configuration discovery with two methods:

  1. Project root discovery (default): Searches for camunda-test-config.json at the project root
  2. Environment variable override: Use CAMUNDA_TEST_CONFIG_FILE to specify a custom config file

The framework uses the following priority order:

  1. Environment variables (highest priority) - Override individual properties
  2. CAMUNDA_TEST_CONFIG_FILE override - Specify custom config file path
  3. Project root configuration - Default camunda-test-config.json at project root
  4. Framework defaults (lowest priority)

Create a camunda-test-config.json file in your project root or test directory:

{
"camundaDockerImageName": "camunda/camunda",
"camundaDockerImageVersion": "8.8.0",
"connectorsDockerImageName": "camunda/connectors-bundle",
"connectorsDockerImageVersion": "8.8.0",
"runtimeMode": "MANAGED"
}

Project Structure with Configuration:

my-project/
โ”œโ”€โ”€ camunda-test-config.json # Main configuration file
โ”œโ”€โ”€ configs/ # Matrix testing configs
โ”‚ โ”œโ”€โ”€ v8.8.0.json # Version-specific
โ”‚ โ”œโ”€โ”€ v8.8.1.json
โ”‚ โ”œโ”€โ”€ staging.json # Environment-specific
โ”‚ โ””โ”€โ”€ production.json
โ”œโ”€โ”€ package.json
โ””โ”€โ”€ test/
โ””โ”€โ”€ shared-tests/ # Tests that run against all configs
โ””โ”€โ”€ common.test.ts

Matrix Testing Examples:

# Test against different configurations using environment variable
CAMUNDA_TEST_CONFIG_FILE=configs/v8.8.0.json npm test
CAMUNDA_TEST_CONFIG_FILE=configs/v8.8.1.json npm test
CAMUNDA_TEST_CONFIG_FILE=configs/staging.json npm test
Property Description Default Environment Variable
camundaDockerImageName Zeebe container image name camunda/camunda CAMUNDA_DOCKER_IMAGE_NAME
camundaDockerImageVersion Zeebe container image version 8.8.0 CAMUNDA_DOCKER_IMAGE_VERSION
connectorsDockerImageName Connectors container image name camunda/connectors-bundle CONNECTORS_DOCKER_IMAGE_NAME
connectorsDockerImageVersion Connectors container image version 8.8.0 CONNECTORS_DOCKER_IMAGE_VERSION
runtimeMode Runtime mode (MANAGED or REMOTE) MANAGED CAMUNDA_RUNTIME_MODE
zeebeClientId Client ID for OAuth authentication "" ZEEBE_CLIENT_ID
zeebeClientSecret Client secret for OAuth authentication "" ZEEBE_CLIENT_SECRET
camundaOauthUrl OAuth URL for authentication "" CAMUNDA_OAUTH_URL
zeebeRestAddress REST API address for remote Zeebe "" ZEEBE_REST_ADDRESS
zeebeGrpcAddress gRPC API address for remote Zeebe workers Auto-derived from REST address ZEEBE_GRPC_ADDRESS
zeebeTokenAudience Token audience for OAuth "" ZEEBE_TOKEN_AUDIENCE
zeebeClientLogLevel gRPC client log level (NONE, ERROR, WARN, INFO, DEBUG) NONE ZEEBE_CLIENT_LOG_LEVEL
camundaAuthStrategy Authentication strategy "" (auto-detect) CAMUNDA_AUTH_STRATEGY
camundaMonitoringApiAddress Monitoring API address Auto-calculated from REST address:9600 CAMUNDA_MONITORING_API_ADDRESS
connectorsRestApiAddress Connectors API address Auto-calculated from REST address:8085 CONNECTORS_REST_API_ADDRESS
flushProcesses Cancel all active process instances on startup (REMOTE mode only) false CAMUNDA_FLUSH_PROCESSES
testScope Test organization hint "" -
description Human-readable description "" -

Project Root Discovery: The framework finds the project root by searching up the directory tree for package.json, then looks for camunda-test-config.json in that directory.

Environment Variable Override: Set CAMUNDA_TEST_CONFIG_FILE to the path of any configuration file (relative to project root or absolute path). This completely bypasses the default project root discovery.

# Relative to project root
CAMUNDA_TEST_CONFIG_FILE=configs/staging.json npm test

# Absolute path
CAMUNDA_TEST_CONFIG_FILE=/path/to/config.json npm test

Production-ready setup:

{
"camundaDockerImageName": "camunda/camunda",
"camundaDockerImageVersion": "8.8.0-alpha6",
"connectorsDockerImageName": "camunda/connectors-bundle",
"connectorsDockerImageVersion": "8.8.0-alpha6",
"runtimeMode": "MANAGED"
}

Development with specific versions:

{
"camundaDockerImageName": "camunda/camunda",
"camundaDockerImageVersion": "8.8.0",
"runtimeMode": "MANAGED"
}

C8Run example (auto-calculated APIs):

{
"runtimeMode": "REMOTE",
"zeebeRestAddress": "http://localhost:8080",
"zeebeGrpcAddress": "grpc://localhost:26500",
"camundaAuthStrategy": "NONE"
}

Remote runtime (existing Camunda instance):

{
"runtimeMode": "REMOTE",
"zeebeRestAddress": "https://your-cluster.region.zeebe.camunda.io:443",
"zeebeGrpcAddress": "grpcs://your-cluster.region.zeebe.camunda.io:443",
"zeebeClientId": "your-client-id",
"zeebeClientSecret": "your-client-secret",
"camundaOauthUrl": "https://login.cloud.camunda.io/oauth/token",
"zeebeTokenAudience": "zeebe.camunda.io"
}

SaaS example with explicit API addresses:

{
"runtimeMode": "REMOTE",
"zeebeRestAddress": "https://your-cluster.region.zeebe.camunda.io:443",
"zeebeGrpcAddress": "grpcs://your-cluster.region.zeebe.camunda.io:443",
"zeebeClientId": "your-client-id",
"zeebeClientSecret": "your-client-secret",
"camundaOauthUrl": "https://login.cloud.camunda.io/oauth/token",
"zeebeTokenAudience": "zeebe.camunda.io",
"camundaMonitoringApiAddress": "https://your-cluster.region.zeebe.camunda.io:9600",
"connectorsRestApiAddress": "https://your-cluster.region.zeebe.camunda.io:8085"
}

Self-managed example with custom ports:

{
"runtimeMode": "REMOTE",
"zeebeRestAddress": "http://camunda.mycompany.com:8080",
"zeebeGrpcAddress": "grpc://camunda.mycompany.com:26500",
"camundaAuthStrategy": "NONE",
"camundaMonitoringApiAddress": "http://camunda.mycompany.com:9600",
"connectorsRestApiAddress": "http://connectors.mycompany.com:8085"
}

Test environment with process cleanup:

{
"runtimeMode": "REMOTE",
"zeebeRestAddress": "http://test-camunda:8080",
"camundaAuthStrategy": "NONE",
"flushProcesses": true
}

Override configuration file settings or set additional options:

# Environment configuration
CAMUNDA_DOCKER_IMAGE_VERSION=8.8.0-alpha6
CAMUNDA_DOCKER_IMAGE_NAME=camunda/camunda
CONNECTORS_DOCKER_IMAGE_VERSION=8.8.0-alpha6
CAMUNDA_RUNTIME_MODE=MANAGED

# Remote runtime configuration (C8Run)
ZEEBE_REST_ADDRESS=http://localhost:8080
CAMUNDA_RUNTIME_MODE=REMOTE

# Remote runtime configuration (SaaS)
ZEEBE_REST_ADDRESS=https://your-cluster.region.zeebe.camunda.io:443
ZEEBE_GRPC_ADDRESS=grpcs://your-cluster.region.zeebe.camunda.io:443 # Secure gRPC
ZEEBE_CLIENT_ID=your-client-id
ZEEBE_CLIENT_SECRET=your-client-secret
CAMUNDA_OAUTH_URL=https://login.cloud.camunda.io/oauth/token
ZEEBE_TOKEN_AUDIENCE=zeebe.camunda.io
CAMUNDA_AUTH_STRATEGY=OAUTH
CAMUNDA_RUNTIME_MODE=REMOTE

# Remote runtime configuration (C8Run - insecure)
ZEEBE_REST_ADDRESS=http://localhost:8080
ZEEBE_GRPC_ADDRESS=grpc://localhost:26500 # Insecure gRPC for local development
CAMUNDA_AUTH_STRATEGY=NONE
CAMUNDA_RUNTIME_MODE=REMOTE

# Runtime configuration
CAMUNDA_CONNECTORS_ENABLED=true

# Process management (REMOTE mode only)
CAMUNDA_FLUSH_PROCESSES=true # Cancel all active process instances on startup (use with caution)

# Debug settings
DEBUG=camunda:* # Enable debug logging

# Matrix testing override
CAMUNDA_TEST_CONFIG_FILE=configs/staging.json # Use specific config file

The framework supports powerful matrix testing capabilities for CI/CD pipelines using the CAMUNDA_TEST_CONFIG_FILE environment variable to specify different configuration files.

name: Matrix Testing
on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        config:
          - configs/v8.8.0.json
          - configs/v8.8.1.json
          - configs/v8.9.0.json
          - configs/staging.json
          - configs/production.json
    
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '18'
      
      - name: Install dependencies
        run: npm ci
      
      - name: Run tests with specific config
        env:
          CAMUNDA_TEST_CONFIG_FILE: ${{ matrix.config }}
        run: npm test

configs/v8.8.0.json - Version-specific testing

{
"description": "Camunda 8.8.0 testing",
"camundaDockerImageVersion": "8.8.0",
"connectorsDockerImageVersion": "8.8.0",
"runtimeMode": "MANAGED"
}

configs/staging.json - Environment-specific testing

{
"description": "Staging environment testing",
"runtimeMode": "REMOTE",
"zeebeRestAddress": "https://staging.company.com:8080",
"zeebeGrpcAddress": "grpcs://staging.company.com:26500",
"camundaAuthStrategy": "NONE"
}

configs/production.json - Production validation

{
"description": "Production environment validation",
"runtimeMode": "REMOTE",
"zeebeRestAddress": "https://prod.company.com:8080",
"zeebeGrpcAddress": "grpcs://prod.company.com:26500",
"zeebeClientId": "${PROD_CLIENT_ID}",
"zeebeClientSecret": "${PROD_CLIENT_SECRET}",
"camundaOauthUrl": "https://login.cloud.camunda.io/oauth/token"
}
# Test against different configurations locally
for config in configs/*.json; do
echo "Testing with $config"
CAMUNDA_TEST_CONFIG_FILE="$config" npm test
done

# Test specific combinations
CAMUNDA_TEST_CONFIG_FILE=configs/v8.8.0.json npm run test:integration
CAMUNDA_TEST_CONFIG_FILE=configs/staging.json npm run test:e2e

Jest configuration in jest.config.js:

module.exports = {
preset: 'ts-jest', // If you are testing TypeScript
testEnvironment: 'node',
// Global timeout - will be overridden per test as needed
testTimeout: process.env.CI ? 300000 : 30000,
detectOpenHandles: true,
forceExit: true,
maxWorkers: 1, // Run tests sequentially to avoid container conflicts
};

Essential is to set the test timeout high enough to pull and start the container if you are running in MANAGED mode.

The framework supports connecting to remote Camunda instances instead of managing local Docker containers. This is useful for testing against:

  • C8Run - Local development runtime
  • Self-managed - Your own Camunda installations
  • Camunda SaaS - Cloud-hosted Camunda instances

For local or internal instances without authentication:

{
"runtimeMode": "REMOTE",
"zeebeRestAddress": "http://localhost:8080",
"zeebeGrpcAddress": "grpc://localhost:26500",
"camundaAuthStrategy": "NONE"
}

Required for Camunda SaaS instances:

{
"runtimeMode": "REMOTE",
"zeebeRestAddress": "https://your-cluster.region.zeebe.camunda.io:443",
"zeebeGrpcAddress": "grpcs://your-cluster.region.zeebe.camunda.io:26500",
"zeebeClientId": "your-client-id",
"zeebeClientSecret": "your-client-secret",
"camundaOauthUrl": "https://login.cloud.camunda.io/oauth/token",
"zeebeTokenAudience": "zeebe.camunda.io"
}

The framework automatically calculates monitoring and connectors API addresses:

  • Monitoring API: REST address with port 9600
  • Connectors API: REST address with port 8085

For zeebeRestAddress: "https://example.com:443":

  • Monitoring API โ†’ https://example.com:9600
  • Connectors API โ†’ https://example.com:8085

You can override these defaults by explicitly setting:

  • camundaMonitoringApiAddress
  • connectorsRestApiAddress

When testing against a REMOTE engine (such as C8Run), you may want to clean up any active process instances from previous test runs to ensure test isolation. The framework provides the flushProcesses option:

{
"runtimeMode": "REMOTE",
"zeebeRestAddress": "http://localhost:8080",
"flushProcesses": true
}

Environment Variable:

CAMUNDA_FLUSH_PROCESSES=true

โš ๏ธ Important Safety Notes:

  • Only works in REMOTE mode - Never enabled for managed containers
  • Defaults to false - Must be explicitly enabled
  • Cancels ALL active process instances - Use only in test/development environments
  • Cannot be undone - Cancelled instances cannot be resumed

When to Use:

  • โœ… Testing against dedicated test environments
  • โœ… Local C8Run development instances
  • โœ… Isolated staging environments
  • โŒ Never use in production environments
  • โŒ Never use in shared environments with important data

Example Usage:

// Configuration file approach
{
"runtimeMode": "REMOTE",
"zeebeRestAddress": "http://test-camunda:8080",
"flushProcesses": true // Cleanup before each test run
}

// Environment variable approach
CAMUNDA_FLUSH_PROCESSES=true npm test

Use deployResources() for deploying resources with optional automatic cleanup support:

// Deploy single resource
await context.deployResources(['./processes/my-process.bpmn']);

// Deploy multiple resources at once
await context.deployResources([
'./processes/order-process.bpmn',
'./decisions/approval.dmn',
'./forms/order-form.form'
]);

// Deploy with automatic cleanup
// Resources will be automatically deleted after the test completes
await context.deployResources(
['./processes/my-process.bpmn'],
{ autoDelete: true }
);

Auto-Delete Feature: When autoDelete: true is specified, deployed resources are automatically tracked and deleted after each test.

Using the framework's context object to create process instances means that any active process instance created in a test is cancelled in the afterEach lifecycle, ensuring test isolation.

const camunda = client.getCamundaRestClient();

const processInstance = await context.createProcessInstance({
processDefinitionId: 'my-process',
variables: { input: 'test-data' }
});

The framework operates in two modes with different cleanup behaviors:

  • Default mode using TestContainers
  • Automatic container recycling between test files (not between tests in a file)
  • No manual cleanup needed between test files - fresh environment for each test file
  • Recommended for development and CI/CD pipelines
  • Connects to existing Camunda instance
  • Manual cleanup required to prevent resource accumulation
  • Use autoDelete: true for automatic resource cleanup
  • Recommended for integration testing against live environments
// Auto-cleanup example 
await context.deployResources(
['./processes/test-process.bpmn'],
{ autoDelete: true } // Automatically deleted after this test
);

Best Practices:

  • Use autoDelete: true when testing against REMOTE environments

Note that each Job Worker Mock will only process one job. If you want to complete more than one job, then you should create a real job worker in the test, using the injected client.

// Complete successfully
await context.mockJobWorker('payment-service')
.thenComplete({ transactionId: 'tx-123' });

// Throw business error
await context.mockJobWorker('validation-service')
.thenThrowBpmnError('VALIDATION_ERROR', 'Invalid data');

// Throw technical error
await context.mockJobWorker('external-api')
.thenThrowError('Connection timeout');

// Custom behavior
await context.mockJobWorker('complex-task')
.withHandler(async (job) => {
const { amount } = job.variables;
if (amount > 1000) {
return { approved: false };
}
return { approved: true };
});

For testing external workers, use the framework's client:

test('should process jobs with external gRPC worker', async () => {
/**
* getClient() returns a managed instance of the Camunda8 class configured for connection to
* the test engine. This client is also managed to stop any polling workers when the test ends.
* This means that you do not need to manually manage closing workers in your tests.
*/
const client = setup.getClient();
const context = setup.getContext();

// Deploy process with service task
await context.deployResources(['./processes/worker-process.bpmn']);

// Create external gRPC worker - closing the worker on test completion is automatically managed by framework
const grpcClient = client.getZeebeGrpcApiClient();
grpcClient.createWorker({
taskType: 'external-task',
taskHandler: (job) => {
// Process the job
const { input } = job.variables;
return job.complete({
output: `Processed: ${input}`,
timestamp: new Date().toISOString()
});
}
});

// Start process instance
const processInstance = await context.createProcessInstance({
processDefinitionId: 'worker-process',
variables: { input: 'test-data' }
});

// Verify process completion
const assertion = CamundaAssert.assertThat(processInstance);
await assertion.isCompleted();
await assertion.hasVariables({
output: 'Processed: test-data'
});

// Worker is automatically closed by the framework during test cleanup
});

The framework automatically manages the lifecycle of both gRPC and REST job workers:

Automatic Registration & Cleanup:

  • Workers created via client.getZeebeGrpcApiClient().createWorker() are automatically stopped after a test
  • Workers created via client.getCamundaRestClient().createJobWorker() are automatically stopped after a test
  • All workers are automatically closed/stopped during test cleanup
  • No need for manual worker.close() or worker.stop() calls
  • No need for try/finally blocks around worker creation
  • It is important to run tests using jest's --runInBand option or maxWorkers: 1 configuration option. All workers are stopped at the end of a test.

Worker testing pattern without Lifecycle Management:

const worker = grpcClient.createWorker(config);
try {
// test logic
} finally {
worker.close(); // Manual cleanup required
}

Tests with our Automatic Lifecycle Management:

grpcClient.createWorker(config);
// test logic - worker automatically cleaned up by framework!

For REMOTE mode with external Zeebe clusters:

{
"runtimeMode": "REMOTE",
"zeebeRestAddress": "https://cluster.region.zeebe.camunda.io:443",
"zeebeGrpcAddress": "grpcs://cluster.region.zeebe.camunda.io:443",
"zeebeClientId": "your-client-id",
"zeebeClientSecret": "your-client-secret"
}

Or use environment variables:

ZEEBE_REST_ADDRESS=https://cluster.region.zeebe.camunda.io:443
ZEEBE_GRPC_ADDRESS=grpcs://cluster.region.zeebe.camunda.io:443 # Note: protocol-based TLS
ZEEBE_CLIENT_ID=your-client-id
ZEEBE_CLIENT_SECRET=your-client-secret

Protocol-based TLS Detection:

  • grpc:// - Insecure connection (local development, C8Run)
  • grpcs:// - Secure TLS connection (SaaS, production)

The framework automatically configures TLS based on the protocol in ZEEBE_GRPC_ADDRESS.

gRPC Client Logging: By default, the gRPC client logging is suppressed to reduce noise. You can enable it for debugging:

# Enable detailed logging
ZEEBE_CLIENT_LOG_LEVEL=INFO npm test

# Available levels: NONE (default), ERROR, WARN, INFO, DEBUG
ZEEBE_CLIENT_LOG_LEVEL=DEBUG npm test

Or in configuration file:

{
"zeebeClientLogLevel": "INFO"
}
const assertion = CamundaAssert.assertThat(processInstance);

// Basic state assertions
await assertion.isCompleted(); // Process finished successfully
await assertion.isActive(); // Process is still running
await assertion.isTerminated(); // Process was terminated

// Variable assertions
await assertion.hasVariables({ status: 'approved' });
await assertion.hasVariable('orderStatus', 'completed');

// Activity assertions
await assertion.hasCompletedElements('task1', 'task2');
await assertion.hasActiveElements('waiting-task');

// Error assertions
await assertion.hasNoIncidents();
await assertion.hasIncidentWithMessage('timeout');
const userTaskAssertion = CamundaAssert.assertThatUserTask({ 
type: 'elementId',
value: 'approve-task'
});

await userTaskAssertion.exists();
await userTaskAssertion.isAssignedTo('john.doe');
await userTaskAssertion.isUnassigned();
await userTaskAssertion.hasCandidateGroups('managers');
await userTaskAssertion.hasVariables({ priority: 'high' });
await userTaskAssertion.complete({ approved: true });
const decisionAssertion = CamundaAssert.assertThatDecision({ 
type: 'decisionId',
value: 'approval'
});

await decisionAssertion.wasEvaluated();
await decisionAssertion.hasResult({ approved: true });
await decisionAssertion.hasResultContaining({ score: 85 });

โš ๏ธ IMPORTANT WARNING: Time manipulation may fail in REMOTE mode (SaaS/C8Run environments). For reliable timer testing, use MANAGED mode with Docker containers. SaaS and C8Run typically reject clock modification attempts even with ZEEBE_CLOCK_CONTROLLED=true.

Control Zeebe's internal clock for testing time-based processes like timers, timeouts, and scheduled tasks.

// Increase time for timer testing (async - advances Zeebe's actual clock)
await context.increaseTime({ hours: 24 });
await context.increaseTime({ minutes: 30 });
await context.increaseTime(5000); // milliseconds

// Get current test time
const currentTime = context.getCurrentTime();
test('should complete timer-based process', async () => {
const context = setup.getContext();

// Deploy process with timer event
await context.deployResources(['./processes/timer-process.bpmn']);

// Mock service task after timer
await context.mockJobWorker('after-timer')
.thenComplete({ timerCompleted: true });

// Start process
const processInstance = await context.createProcessInstance({
processDefinitionId: 'timer-process',
variables: {}
});

// Verify process is waiting at timer
const assertion1 = CamundaAssert.assertThat(processInstance);
await assertion1.isActive();
await assertion1.hasActiveElements('wait-timer');

// Advance time to trigger timer (advances Zeebe's internal clock)
await context.increaseTime({ hours: 1 });

// Verify timer triggered and process completed
const assertion2 = CamundaAssert.assertThat(processInstance);
await assertion2.isCompleted();
await assertion2.hasCompletedElements('wait-timer', 'after-timer-task');
});

The framework provides direct access to Zeebe's clock management through the CamundaClock utility:

import { CamundaClock } from '@camunda8/process-test';

// Create clock instance (usually done automatically by the framework)
const runtime = setup.getRuntime();
const clock = new CamundaClock(runtime);

// Get current Zeebe time
const currentTime = await clock.getCurrentTime();

// Advance clock by milliseconds
await clock.addTime(60000); // 1 minute

// Advance using convenience method
await clock.advanceTime(1, 'hours');
await clock.advanceTime(30, 'minutes');
await clock.advanceTime(5, 'seconds');

// Reset clock to system time
await clock.resetClock();
// All these are equivalent to 1 hour
await context.increaseTime(3600000); // milliseconds
await context.increaseTime({ hours: 1 });
await context.increaseTime({ minutes: 60 });
await context.increaseTime({ seconds: 3600 });

// Combined durations
await context.increaseTime({
days: 1,
hours: 2,
minutes: 30,
seconds: 45
});
// Timer tests: Use MANAGED mode (Docker containers)
describe('Timer-based processes', () => {
// Configuration: camunda-test-config.json
// { "runtimeMode": "MANAGED" }

test('should handle 24-hour timer', async () => {
// Deploy and start process with timer
await context.deployProcess('./timer-process.bpmn');

// This works reliably in MANAGED mode
await context.increaseTime({ hours: 24 });

// Verify timer completion...
});
});

// Non-timer tests: Can use REMOTE mode
describe('Business logic processes', () => {
// Configuration: camunda-test-config.json
// { "runtimeMode": "REMOTE", "zeebeRestAddress": "..." }

test('should complete approval workflow', async () => {
// No time manipulation needed - works great in REMOTE mode
// Test business logic, user tasks, service tasks, etc.
});
});

Notes:

  • MANAGED mode only: Time manipulation is designed for MANAGED mode (Docker containers) where ZEEBE_CLOCK_CONTROLLED=true is automatically set.
  • REMOTE mode limitations: SaaS and C8Run environments typically reject clock modification attempts, even with ZEEBE_CLOCK_CONTROLLED=true.
  • Testing strategy: Use MANAGED mode for timer-based tests, REMOTE mode for other functionality tests.
  • Framework behavior: The framework will warn when attempting time manipulation in REMOTE mode but will still attempt the operation.

Enable comprehensive debugging to inspect Docker operations, process deployments, and test execution:

# Enable all debugging
DEBUG=camunda:* npm test

# Enable specific categories
DEBUG=camunda:test:container npm test # Docker operations
DEBUG=camunda:test:deploy npm test # Process deployments
DEBUG=camunda:test:runtime npm test # Runtime lifecycle
DEBUG=camunda:test:logs npm test # Container log capture
  • camunda:test:runtime - Runtime startup/shutdown
  • camunda:test:container - Docker container operations
  • camunda:test:docker - Docker image pulls and versions
  • camunda:test:deploy - BPMN/DMN deployments
  • camunda:test:worker - Job worker operations
  • camunda:test:context - Test context lifecycle
  • camunda:test:logs - Container log capture

When debugging is enabled, detailed container logs are saved to ./camunda-test-logs/:

  • elasticsearch-{timestamp}.log - Elasticsearch startup and operation logs
  • camunda-{timestamp}.log - Camunda broker logs with BPMN processing details
  • connectors-{timestamp}.log - Connector runtime logs (if enabled)

For detailed debugging instructions, see DEBUG.md.

This repository includes working examples:

All examples include BPMN/DMN files in examples/resources/.

# Build first
npm run build

# Run simple example
npm test examples/simple.test.ts

# Run gRPC worker example
npm test examples/grpc-worker.test.ts

# Run with debugging
DEBUG=camunda:* npm test examples/debug.test.ts

# Run all examples
npm test examples/

When testing against shared environments (SaaS, C8Run, etc.), use auto-cleanup to prevent resource accumulation:

describe('Payment Process Tests', () => {
const setup = setupCamundaProcessTest();

test('should handle successful payment in REMOTE environment', async () => {
const context = setup.getContext();

// Deploy with automatic cleanup
await context.deployResources([
'./processes/payment-process.bpmn',
'./decisions/credit-check.dmn'
], {
autoDelete: true // Resources deleted automatically after test
});

// Test logic here...
// Resources will be automatically cleaned up in afterEach()
});

test('should handle multiple resource types', async () => {
const context = setup.getContext();

// Check runtime mode for conditional cleanup
const mode = context.getRuntimeMode();
const shouldCleanup = mode === 'REMOTE';

await context.deployResources([
'./processes/order-process.bpmn',
'./forms/customer-form.form',
'./decisions/approval.dmn'
], {
autoDelete: shouldCleanup
});

// Test implementation...
});
});
describe('Order Integration Test', () => {
const setup = setupCamundaProcessTest();

test('should complete order flow', async () => {
const client = setup.getClient();
const context = setup.getContext();

// Deploy all related processes and decisions
await context.deployProcess('./processes/order-process.bpmn');
await context.deployProcess('./processes/payment-process.bpmn');
await context.deployDecision('./decisions/credit-check.dmn');

// Mock external services
await context.mockJobWorker('credit-check-service')
.thenComplete({ creditScore: 750 });

await context.mockJobWorker('payment-gateway')
.thenComplete({ transactionId: 'tx-12345', status: 'success' });

// Test the complete flow
const camunda = client.getCamundaRestClient();
const orderInstance = await camunda.createProcessInstance({
processDefinitionId: 'order-process',
variables: { customerId: 'c123', amount: 599.99 }
});

const assertion = CamundaAssert.assertThat(orderInstance);
await assertion.isCompleted();
await assertion.hasVariables({
creditScore: 750,
paymentStatus: 'success',
orderStatus: 'completed'
});
});
});
test('should handle payment failure', async () => {
const client = setup.getClient();
const context = setup.getContext();

await context.deployProcess('./processes/payment-process.bpmn');

// Simulate payment failure
await context.mockJobWorker('payment-service')
.thenThrowBpmnError('PAYMENT_FAILED', 'Insufficient funds');

const camunda = client.getCamundaRestClient();
const processInstance = await camunda.createProcessInstance({
processDefinitionId: 'payment-process',
variables: { amount: 1000000 } // Large amount
});

// Verify error handling path
const assertion = CamundaAssert.assertThat(processInstance);
await assertion.isCompleted();
await assertion.hasCompletedElements('payment-failed-event', 'notify-customer');
});
  • First run: 3-5 minutes (image downloads)
  • Subsequent runs: 30-60 seconds (cached images)
  • Parallel tests: Use maxWorkers: 1 in Jest config
# Pre-pull images to speed up tests
docker pull camunda/camunda:8.8.0-alpha6

# Clean up containers after testing
docker container prune -f

Solution: Start Docker Desktop

# Check Docker is running
docker ps

# If not running, start Docker Desktop

Solution: Increase Jest timeout or check Docker resources

# Run with longer timeout
npm test --testTimeout=300000

# Check Docker resources in Docker Desktop settings

Solution: Clean up existing containers

# Stop all Camunda containers
docker stop $(docker ps -q --filter ancestor=camunda/camunda)

# Or restart Docker Desktop
# See container startup details
DEBUG=camunda:test:container npm test

# Check deployment issues
DEBUG=camunda:test:deploy npm test

# Monitor runtime problems
DEBUG=camunda:test:runtime npm test

# Debug worker lifecycle management
DEBUG=camunda:test:worker npm test

# Monitor test cleanup operations
DEBUG=camunda:test:cleanup npm test

# Capture container logs for detailed inspection
DEBUG=camunda:test:logs npm test
# Then check ./camunda-test-logs/ for detailed container logs

๐Ÿ“– Complete API Documentation - Comprehensive TypeDoc-generated API documentation

  • setupCamundaProcessTest(): Function for test setup
  • @CamundaProcessTest: Decorator for test classes
  • CamundaAssert: Main assertion entry point
  • CamundaProcessTestContext: Test context and utilities
  • CamundaClock: Clock management for time-based testing
  • JobWorkerMock: Job worker mocking utilities
  • deployResources(paths, options?): Deploy multiple resources with optional auto-cleanup
    • paths: Array of file paths to BPMN, DMN, or Form files
    • options.autoDelete: Boolean - automatically delete resources after test (REMOTE mode only)
  • deployProcess(path, processId?): Deploy single BPMN process (deprecated)
  • deployDecision(path): Deploy single DMN decision (deprecated)
  • createProcessInstance(request): Create and start process instance with automatic tracking
    • Instances are automatically cancelled in REMOTE mode during test cleanup
  • createProcessInstanceWithResult(request): Create process instance and await completion
    • Instances are automatically cancelled in REMOTE mode if still running during cleanup
  • getRuntimeMode(): Get current runtime mode ('MANAGED' | 'REMOTE')
  • getClient(): Get Camunda 8 client instance
  • getGatewayAddress(): Get Zeebe gateway address
  • getConnectorsAddress(): Get connectors runtime address
  • mockJobWorker(jobType): Create a mock job worker for the specified job type
  • gRPC Workers: Workers created via client.getZeebeGrpcApiClient().createWorker() are automatically registered and closed
  • REST Job Workers: Workers created via client.getCamundaRestClient().createJobWorker() are automatically registered and stopped
  • Lifecycle Management: All workers are automatically cleaned up during test teardown
  • No Manual Cleanup: No need for try/finally blocks or manual worker.close() calls
  • Debug Support: Use DEBUG=camunda:test:worker to monitor worker registration and cleanup
  • increaseTime(duration): Advance Zeebe's internal clock
  • getCurrentTime(): Get current test time
  • resetTime(): Reset time to system time
  • waitUntil(condition, options?): Wait for a condition with polling
  • resetTestState(): Reset test state between test methods
  • cleanupTestData(): Clean up test data after test methods

The framework supports two runtime modes and provides a way to detect which mode is active for differential test behavior:

// Function approach
const setup = setupCamundaProcessTest();
const context = setup.getContext();
const runtimeMode = context.getRuntimeMode(); // Returns 'MANAGED' | 'REMOTE'

// Decorator approach
@CamundaProcessTest
class MyTest {
private context!: CamundaProcessTestContext;

async testWithRuntimeSpecificBehavior() {
const runtimeMode = this.context.getRuntimeMode();

if (runtimeMode === 'MANAGED') {
// Test behavior specific to Docker container environment
// - Can test container logs
// - Can test container restart scenarios
// - Full control over Camunda lifecycle
} else {
// Test behavior specific to remote Camunda instance
// - Cannot control container lifecycle
// - May have different performance characteristics
// - Need to handle external dependencies
}
}
}

Runtime Modes:

  • MANAGED (default): Uses Docker containers managed by TestContainers

    • Full control over Camunda lifecycle
    • Isolated test environment
    • Container logs available for debugging
    • Slower startup (container initialization)
  • REMOTE: Connects to external Camunda instance (SaaS, C8Run, etc.)

    • No container management overhead
    • Faster test execution
    • Shared environment considerations
    • Limited control over Camunda state
  • ProcessInstanceAssert: Process instance assertions
  • UserTaskAssert: User task assertions
  • DecisionInstanceAssert: Decision instance assertions
  • Element Selectors: { type: 'id' | 'name' | 'type' | 'custom', value: string | function }
  • Process Instance Selectors: { type: 'key' | 'processId' | 'custom', value: string | function }
  • User Task Selectors: { type: 'key' | 'elementId' | 'assignee' | 'custom', value: string | function }
  • Decision Selectors: { type: 'key' | 'decisionId' | 'processInstanceKey' | 'custom', value: string | function }

We welcome contributions! Please see our Contributing Guide for details.

This library includes comprehensive CI/CD capabilities with sophisticated GitHub Actions workflows:

  • Unit Tests: Fast TypeScript-based tests (npm test)
  • Integration Tests: Full Docker-based Camunda tests (npm run test:integration)
  • Docker Optimization: Pre-pull of Camunda images for faster CI execution
  • Extended Timeouts: 45-minute timeout for complex integration scenarios
  • Environment Configuration: Proper CI environment variables and debug settings
  • Parallel Execution: Separate jobs for code quality and integration testing
  • Comprehensive Coverage: Both unit and integration tests run on every PR/push
# Unit tests (fast, no Docker required)
npm test

# Integration tests (requires Docker)
npm run test:integration

# Run specific integration test
npm run test:integration -- --testNamePattern="simple"

Apache License 2.0 - see LICENSE file for details.