The @osdk/integration-testing package allows you to test TypeScript Ontology SDK (OSDK) code against a local Ontology server. You supply Ontology metadata and test data, then use an OSDK Client to run your code. The local server evaluates object queries against that data.
Use integration tests to check how filters, object reads, links, and aggregations work together. Use unit tests when you want to specify the result of each client call with a stub.
The library provides three main parts:
IntegrationServer: Starts and stops the local Ontology server through the Foundry CLI.SeedClient: Creates and changes the test data on that server.IntegrationClient: Sends OSDK requests to the local server and supports stubs for function queries.@osdk/integration-testing and prepare your environmentRun integration tests in a Node.js environment. The library starts local processes and is not intended for browser tests. Install the library and @osdk/seed-helpers as development dependencies:
Copied!1npm install --save-dev @osdk/integration-testing @osdk/seed-helpers
Your project must also have compatible versions of the @osdk/api and @osdk/client peer dependencies, plus your generated TypeScript SDK.
The examples on this page use Vitest ↗. You can use another Node.js test runner with equivalent setup and cleanup hooks. To use Vitest, install it as a development dependency:
Copied!1npm install --save-dev vitest
The example below assumes that your SDK contains an Employee object type with a string primary key named employeeId and a string property named office.
Assume you have a function you want to test that takes an OSDK Client and reads Ontology entities. For example, src/countEmployeesInOffice.ts counts the employees in a given office:
Copied!1 2 3 4 5 6 7 8 9 10 11 12 13import type { Client } from "@osdk/client"; import { Employee } from "your-app-sdk"; export async function countEmployeesInOffice( client: Client, officeId: string, ): Promise<number> { const result = await client(Employee) .where({ office: { $eq: officeId } }) .aggregate({ $select: { $count: "unordered" } }); return result.$count; }
The following test file, test/countEmployeesInOffice.integration.test.ts, passes an integration client to the function and checks its result against seeded data:
Copied!1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42import { createIntegrationServer, type IntegrationClient, type IntegrationServer, type SeedClient, } from "@osdk/integration-testing"; import { afterAll, beforeAll, beforeEach, expect, it } from "vitest"; import { Employee } from "your-app-sdk"; import metadata from "your-app-sdk/experimental/ontology-metadata" with { type: "json", }; import { countEmployeesInOffice } from "../src/countEmployeesInOffice.js"; let server: IntegrationServer | undefined; let client: IntegrationClient; let seed: SeedClient; beforeAll(async () => { server = await createIntegrationServer({ metadata }); await server.start(); client = await server.getClient(); seed = await server.getSeedClient(); }, 180_000); beforeEach(async () => { client.clearStubs(); await seed.set(); }, 30_000); afterAll(async () => { await server?.stop(); }, 30_000); it("counts only employees in the requested office", async () => { await seed((s) => { s.create(Employee, { employeeId: "employee-1", office: "north" }); s.create(Employee, { employeeId: "employee-2", office: "north" }); s.create(Employee, { employeeId: "employee-3", office: "south" }); }); expect(await countEmployeesInOffice(client, "north")).toBe(2); }, 30_000);
Run the test:
Copied!1npx vitest run test/countEmployeesInOffice.integration.test.ts
The test starts the server, seeds three objects, and passes the integration client to the function. The server evaluates the filter and aggregation. You do not register a stub for either operation. The longer hook timeout allows time for the local server to start, and afterAll stops it after the test finishes.
A seed is the collection of objects and links that you load into the local server. A SeedRef identifies an object in that seed. Use seed references to update objects or create links, and fetch objects through the integration client when your code needs an OSDK instance.
Each asynchronous seed operation applies the accumulated seed to the server. Await it before you read the data, as seed.ref() is synchronous and only reads the current seed.
| Method | Purpose |
|---|---|
await seed.create(Type, properties) | Creates an object and returns its reference. Include the primary key and required properties. A duplicate primary key for the same type causes an error. |
seed.ref(Type, primaryKey) | Returns a reference to a seeded object, or undefined if it does not exist. |
await seed.update(ref, properties) | Merges properties into an object without changing its primary key. Omitted properties stay unchanged. If the object is missing, this creates it. |
await seed.delete(ref) | Removes an object from the seed. Does nothing if the object is missing. |
await seed.link(source, linkApiName, target) | Adds many-to-many links to a target reference or an array of target references. |
await seed.unlink(source, linkApiName, target) | Removes the specified seeded links. |
await seed.set(output) | Replaces the entire seed with a SeedOutput. Use await seed.set() to clear it. |
await seed.addAll(output) | Adds a SeedOutput to the current seed. Objects must not duplicate existing primary keys. |
await seed(callback) | Calls a synchronous SeedBuilder callback, applies the seed once, and returns the callback's result. |
await seed(output) | Adds a SeedOutput, with the same behavior as seed.addAll(output). |
For example, create an employee, change their office, read the updated object, and delete it:
Copied!1 2 3 4 5 6 7 8 9 10 11const employeeRef = await seed.create(Employee, { employeeId: "employee-1", office: "north", }); await seed.update(employeeRef, { office: "south" }); const employee = await client(Employee).fetchOne(employeeRef.$primaryKey); expect(employee.office).toBe("south"); await seed.delete(employeeRef);
Use the callback form to make several changes and apply them in one request. Builder methods inside the callback are synchronous. Do not make the callback async.
This example also assumes an Office object type with a string primary key named officeId and a many-to-many Employee link named visitedOffices:
Copied!1 2 3 4 5 6 7 8 9 10 11 12import { Employee, Office } from "your-app-sdk"; const refs = await seed((s) => { const office = s.create(Office, { officeId: "north" }); const employee = s.create(Employee, { employeeId: "employee-1" }); s.link(employee, "visitedOffices", [office]); return { employee, office }; }); const employee = await client(Employee).fetchOne(refs.employee.$primaryKey); const offices = await employee.$link.visitedOffices.fetchPage(); expect(offices.data.map((office) => office.$primaryKey)).toEqual(["north"]);
The local server currently requires string primary keys for both ends of links supplied through seed.link(). For a link backed by a foreign key, seed the foreign key property on the object.
Use createSeedWithMetadata from @osdk/seed-helpers to build a fixture without sending it to a server. It returns output, which holds the seed data, and context, which holds the callback's return value:
Copied!1 2 3 4 5 6 7 8 9 10 11import { createSeedWithMetadata } from "@osdk/seed-helpers"; const fixture = createSeedWithMetadata(metadata, (s) => { const employee = s.create(Employee, { employeeId: "employee-1", office: "north" }); return { employee }; }); beforeEach(async () => { client.clearStubs(); await seed.set(fixture.output); }, 30_000);
Use this hook in place of the empty seed hook in the first example. Each test starts with the same data. For example, fixture.context.employee.$primaryKey gives you the employee's primary key for an assertion.
The local Ontology server does not execute function queries. Register their expected results on the integration client with whenQuery. Your object reads still route to the local server.
For a generated query named addOne with a numeric n parameter, use:
Copied!1 2 3 4 5 6 7 8 9import { addOne } from "your-app-sdk"; client.whenQuery(addOne, { n: 5 }).thenReturn(6); client.whenQuery(addOne, { n: 99 }).thenThrow(new Error("Query failed")); expect(await client(addOne).executeFunction({ n: 5 })).toBe(6); await expect(client(addOne).executeFunction({ n: 99 })).rejects.toThrow( "Query failed", );
Use the query definition and parameters that your code passes. client.clearStubs() removes the registered query stubs, but it does not clear the server's seed data.
When you share a server across tests, reset its data with seed.set() or seed.set(fixture.output) before each test. Clear query stubs separately with client.clearStubs().
Run tests that share a server and seed client sequentially. For concurrent tests, create a separate server for each independent test context. Each server has its own run directory, but tests that use the same server share its data.
Always call server.stop() in a cleanup hook or finally block, including when setup or assertions fail. Local integration tests check behavior supported by the local Ontology server, but they do not replace tests of permissions, deployed functions, or external services in Foundry.