# Integration testing TypeScript OSDK code 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](https://www.palantir.com/docs/foundry/ontology-sdk/typescript-osdk-integration-testing/typescript-osdk-testing.md) 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](../../superrepo/foundry-cli/index.md). * **`SeedClient`:** Creates and changes the test data on that server. * **`IntegrationClient`:** Sends OSDK requests to the local server and supports stubs for function queries. ## Install `@osdk/integration-testing` and prepare your environment Run 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: ```bash npm 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 ↗](https://vitest.dev/). You can use another Node.js test runner with equivalent setup and cleanup hooks. To use Vitest, install it as a development dependency: ```bash npm install --save-dev vitest ``` ## Write your first test 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: ```typescript import type { Client } from "@osdk/client"; import { Employee } from "your-app-sdk"; export async function countEmployeesInOffice( client: Client, officeId: string, ): Promise { 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: ```typescript import { 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: ```bash npx 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. ## Create and change test data 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: ```typescript const 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); ``` ### Create objects and links together 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`: ```typescript import { 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. ### Reuse a seed across tests 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: ```typescript import { 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. ## Stub function queries 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: ```typescript import { 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. ## Keep tests independent 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.