DBOS Testing Runtime
DBOS provides a testing runtime to facilitate writing unit tests for applications. Before running your tests, create and configure the runtime. In your tests, use the runtime's methods to invoke your application's functions. After your tests finish, destroy the runtime to release resources.
When writing tests, you are responsible for setting up and cleaning up your database schema, for example through migrations.
Create Testing Runtime
createTestingRuntime([userClasses], [configFilePath], [dropSysDB])
async function createTestingRuntime(
userClasses: object[] | undefined = undefined,
configFilePath: string = dbosConfigFilePath,
dropSysDB: boolean = true
): Promise<TestingRuntime>
Creates a testing runtime and loads user functions from provided userClasses
. By default, all classes and dependencies from the test file are loaded and registered.
Accepts an optional path to a configuration file, uses the default path (dbos-config.yaml
in the package root) otherwise.
This method also provides an option to keep system database data across test runs.
The defaults are generally sufficient as long as classes are at least indirectly referenced from the test file:
testRuntime = await createTestingRuntime();
However, to explicitly create a runtime loading functions from the Hello
class and using test-config.yaml
:
testRuntime = await createTestingRuntime([Hello], "test-config.yaml");
This method by default drops and re-creates the DBOS system database. You will lose all persisted system information such as workflow status. Don't run unit tests on your production database!
Also, make sure you close any open connections to the system database, otherwise, tests may time out because the DROP DATABASE
command would fail.
If you want to keep your system database across runs, you can specify dropSysDB = false
. For example, to load all classes and dependencies, use the default configuration file, and keep the system database:
testRuntime = await createTestingRuntime(undefined, "dbos-config.yaml", false);
Methods
- invoke(target, [workflowID, params])
- invokeWorkflow(target, [workflowID, params])
- startWorkflow(target, [workflowID, params])
- retrieveWorkflow(workflowID)
- getWorkflows(query)
- send(destinationID, message, [topic, idempotencyKey])
- getEvent(workflowID, key, [timeoutSeconds])
- getHandlersCallback()
- getConfig(key, defaultValue)
- queryUserDB(sql, ...params)
- destroy()
runtime.invoke(target, [workflowUUID, params])
invoke<T>(target: T, workflowID?: string, params?: WorkflowInvokeParams): InvokeFuncs<T>
Invoke a transaction or step.
To invoke workflows, use invokeWorkflow
or startWorkflow
instead.
The syntax for invoking function fn
in class Cls
with argument arg
is:
const output = await runtime.invoke(Cls).fn(arg);
You don't supply a context to an invoked function—the testing runtime does this for you.
You can also optionally provide additional parameters for invoke()
including the authenticated user and roles and an HTTPRequest. This is especially helpful if you want to test individual functions without running end-to-end HTTP serving. The parameters have the following structure:
interface WorkflowInvokeParams {
readonly authenticatedUser?: string; // The user who ran the function.
readonly authenticatedRoles?: string[]; // Roles the authenticated user has.
readonly request?: HTTPRequest; // The originating HTTP request.
}
runtime.invokeWorkflow(target, [workflowUUID, params])
invokeWorkflow<T>(target: T, workflowID?: string, params?: WorkflowInvokeParams): InvokeFuncs<T>
Invoke a workflow and wait for it to complete, returning its result.
The syntax for invoking workflow wf
in class Cls
with argument arg
is:
const output = await runtime.invokeWorkflow(Cls).wf(arg);
You don't supply a context to an invoked workflow—the testing runtime does this for you.
As with invoke, you can optionally provide a workflow idempotency key or workflow invocation parameters.
runtime.startWorkflow(target, [workflowUUID, params])
startWorkflow<T>(target: T, workflowID?: string, params?: WorkflowInvokeParams, queue?: WorkflowQueue): InvokeFuncs<T>
Start a workflow and return a handle to it but do not wait for it to complete.
The syntax for starting workflow wf
in class Cls
with argument arg
is:
const workflowHandle = await runtime.startWorkflow(Cls).wf(arg);
You don't supply a context to start a workflow—the testing runtime does this for you.
As with invoke, you can optionally provide a workflow idempotency key or workflow invocation parameters.
If the queue
argument is provided, the workflow may not start immediately. Start of execution will be determined by the queue and its contents.
runtime.retrieveWorkflow(workflowUUID)
retrieveWorkflow<R>(workflowID: string): WorkflowHandle<R>;
Returns a workflow handle for workflow workflowID.
R
is the return type of the target workflow.
runtime.getWorkflows(query)
getWorkflows(query: GetWorkflowsInput): Promise<GetWorkflowsOutput>;
Returns a list of workflow IDs matching the provided query parameters. See HandlerContext.getWorkflows()
for details.
runtime.send(destinationUUID, message, [topic, idempotencyKey])
send<T extends NonNullable<any>>(destinationUUID: string, message: T, topic?: string, idempotencyKey?: string): Promise<void>;
Sends message to destinationUUID.
Messages can optionally be associated with a topic.
You can provide an optional idempotency key to guarantee only a single message is sent even if send
is called more than once.
For more information, see our messages API tutorial.
runtime.getEvent(workflowUUID, key, [timeoutSeconds])
getEvent<T extends NonNullable<any>>(workflowID: string, key: string, timeoutSeconds?: number): Promise<T | null>;
Retrieves a value published by a workflowID with identifier key using the events API.
A call to getEvent
waits for the value to be published and returns null
in case of time out.
runtime.getHandlersCallback()
getHandlersCallback(): (req: IncomingMessage | Http2ServerRequest, res: ServerResponse | Http2ServerResponse) => Promise<void>;
Returns a request handler callback for node's native http/http2 server.
You can use this callback function to test handlers, for example, using supertest to send a GET
request to /greeting/dbos
URL and verify the response:
import request from "supertest";
const res = await request(testRuntime.getHandlersCallback()).get(
"/greeting/dbos"
);
expect(res.statusCode).toBe(200);
expect(res.text).toMatch("Hello, dbos! You have been greeted");
runtime.getConfig(key, defaultValue)
getConfig<T>(key: string): T | undefined;
getConfig<T>(key: string, defaultValue: T): T;
Retrieves a property specified in the application section of the configuration.
runtime.queryUserDB(sql, ...params)
queryUserDB<R>(sql: string, ...params: any[]): Promise<R[]>;
Executes a parameterized raw SQL query on the user database.
The type R
is the return type of the database row.
For example, to query the dbos_hello
table created during quickstart
and check greet_count
, using Jest:
const rows = await testRuntime.queryUserDB<dbos_hello>("SELECT * FROM dbos_hello WHERE name=$1", "dbos");
expect(rows[0].greet_count).toBe(1);
runtime.destroy()
destroy(): Promise<void>
Deconstructs the testing runtime and releases client connections to the database. Please remember to run this method after your tests!