Documentation Index
Fetch the complete documentation index at: https://mintlify.com/tailor-platform/sdk/llms.txt
Use this file to discover all available pages before exploring further.
This guide covers testing patterns for Tailor Platform SDK applications using Vitest.
For a complete working example with full test code, use the testing template:
npm create @tailor-platform/sdk -- --template testing <your-project-name>
Unit Tests
Unit tests verify resolver logic without requiring deployment.
Simple Resolver Testing
Test resolvers by directly calling resolver.body() with mock inputs.
src/resolver/simple.test.ts
import { unauthenticatedTailorUser } from "@tailor-platform/sdk";
import resolver from "./simple";
describe("add resolver", () => {
test("basic functionality", async () => {
const result = await resolver.body({
input: { left: 1, right: 2 },
user: unauthenticatedTailorUser,
env: {},
});
expect(result).toBe(3);
});
});
Example resolver:
import { createResolver, t } from "@tailor-platform/sdk";
const resolver = createResolver({
name: "add",
operation: "query",
input: {
left: t.int(),
right: t.int(),
},
body: (context) => {
return context.input.left + context.input.right;
},
output: t.int(),
});
export default resolver;
Use unauthenticatedTailorUser for testing logic that doesn’t depend on user context. Best for calculations and data transformations without database dependencies.
Mock TailorDB Client
Mock the global tailordb.Client using vi.stubGlobal() to simulate database operations and control responses for each query.
src/resolver/mockTailordb.test.ts
import { unauthenticatedTailorUser } from "@tailor-platform/sdk/test";
import { afterAll, afterEach, beforeAll, describe, expect, test, vi } from "vitest";
import resolver from "./mockTailordb";
describe("incrementUserAge resolver", () => {
// Mock queryObject method to simulate database interactions
const mockQueryObject = vi.fn();
beforeAll(() => {
vi.stubGlobal("tailordb", {
Client: vi.fn(
class {
connect = vi.fn();
end = vi.fn();
queryObject = mockQueryObject;
},
),
});
});
afterAll(() => {
vi.unstubAllGlobals();
});
afterEach(() => {
mockQueryObject.mockReset();
});
test("basic functionality", async () => {
// Mock database responses for each query in sequence
mockQueryObject.mockResolvedValueOnce({}); // Begin transaction
mockQueryObject.mockResolvedValueOnce({ rows: [{ age: 30 }] }); // Select
mockQueryObject.mockResolvedValueOnce({}); // Update
mockQueryObject.mockResolvedValueOnce({}); // Commit
const result = await resolver.body({
input: { email: "test@example.com" },
user: unauthenticatedTailorUser,
env: {},
});
expect(result).toEqual({ oldAge: 30, newAge: 31 });
expect(mockQueryObject).toHaveBeenCalledTimes(4);
});
test("user not found", async () => {
mockQueryObject.mockResolvedValueOnce({}); // Begin transaction
mockQueryObject.mockResolvedValueOnce({ rows: [] }); // No rows
mockQueryObject.mockResolvedValueOnce({}); // Rollback
const result = resolver.body({
input: { email: "test@example.com" },
user: unauthenticatedTailorUser,
env: {},
});
await expect(result).rejects.toThrowError();
expect(mockQueryObject).toHaveBeenCalledTimes(3);
});
});
This approach lets you:
- Control exact database responses (query results, errors)
- Verify database interaction flow (transactions, queries)
- Test transaction rollback scenarios
- Best for: Business logic with simple database operations
Dependency Injection Pattern
Extract database operations into a DbOperations interface, allowing business logic to be tested independently from Kysely implementation.
First, structure your resolver to accept database operations:
src/resolver/decrementUserAge.ts
import { createResolver, t } from "@tailor-platform/sdk";
import { getDB } from "generated/tailordb";
export interface DbOperations {
transaction: (fn: (ops: DbOperations) => Promise<unknown>) => Promise<void>;
getUser: (email: string, forUpdate: boolean) => Promise<{ email: string; age: number }>;
updateUser: (user: { email: string; age: number }) => Promise<void>;
}
export async function decrementUserAge(
email: string,
dbOperations: DbOperations,
): Promise<{ oldAge: number; newAge: number }> {
let oldAge: number;
let newAge: number;
await dbOperations.transaction(async (ops) => {
const user = await ops.getUser(email, true);
oldAge = user.age;
newAge = user.age - 1;
await ops.updateUser({ ...user, age: newAge });
});
return { oldAge, newAge };
}
export default createResolver({
name: "decrementUserAge",
operation: "mutation",
input: { email: t.string() },
body: async (context) => {
const db = getDB("tailordb");
const dbOperations = createDbOperations(db);
return await decrementUserAge(context.input.email, dbOperations);
},
output: t.object({ oldAge: t.number(), newAge: t.number() }),
});
Then test by mocking the interface:
src/resolver/decrementUserAge.test.ts
import { DbOperations, decrementUserAge } from "./decrementUserAge";
describe("decrementUserAge resolver", () => {
test("basic functionality", async () => {
// Mock DbOperations implementation
const dbOperations = {
transaction: vi.fn(
async (fn: (ops: DbOperations) => Promise<unknown>) => await fn(dbOperations),
),
getUser: vi.fn().mockResolvedValue({ email: "test@example.com", age: 30 }),
updateUser: vi.fn(),
} as DbOperations;
const result = await decrementUserAge("test@example.com", dbOperations);
expect(result).toEqual({ oldAge: 30, newAge: 29 });
expect(dbOperations.getUser).toHaveBeenCalledExactlyOnceWith("test@example.com", true);
expect(dbOperations.updateUser).toHaveBeenCalledExactlyOnceWith(
expect.objectContaining({ age: 29 }),
);
});
});
This pattern:
- Tests business logic independently from Kysely implementation details
- Mocks high-level operations instead of low-level SQL queries
- Best for: Complex business logic with multiple database operations
Workflow Tests
Test workflows locally without deploying to Tailor Platform.
Job Unit Tests
Test individual job logic by calling .body() directly:
src/workflow/calculation.test.ts
import workflow, { addNumbers, calculate } from "./workflows/calculation";
describe("workflow jobs", () => {
test("addNumbers.body() adds two numbers", () => {
const result = addNumbers.body({ a: 2, b: 3 }, { env: {} });
expect(result).toBe(5);
});
});
Mocking Dependent Jobs
For jobs that trigger other jobs, mock the dependencies using vi.spyOn():
src/workflow/calculation.test.ts
import { afterEach, vi } from "vitest";
import workflow, { addNumbers, calculate, multiplyNumbers } from "./workflows/calculation";
describe("workflow with dependencies", () => {
afterEach(() => {
vi.restoreAllMocks();
});
test("calculate.body() with mocked dependent jobs", async () => {
// Mock the trigger methods for dependent jobs
vi.spyOn(addNumbers, "trigger").mockResolvedValue(5);
vi.spyOn(multiplyNumbers, "trigger").mockResolvedValue(10);
const result = await calculate.body({ a: 2, b: 3 }, { env: {} });
expect(addNumbers.trigger).toHaveBeenCalledWith({ a: 2, b: 3 });
expect(result).toBe(10);
});
});
To execute dependent jobs without mocking, and they require env, use vi.stubEnv(WORKFLOW_TEST_ENV_KEY, ...) and call .trigger() directly as shown in the integration test section below.
Integration Tests with .trigger()
Test the full workflow execution using workflow.mainJob.trigger():
src/workflow/calculation.test.ts
import { WORKFLOW_TEST_ENV_KEY } from "@tailor-platform/sdk/test";
import { afterEach, vi } from "vitest";
import workflow from "./workflows/calculation";
describe("workflow integration", () => {
afterEach(() => {
vi.unstubAllEnvs();
});
test("workflow.mainJob.trigger() executes all jobs", async () => {
// Set environment variables for the workflow
vi.stubEnv(WORKFLOW_TEST_ENV_KEY, JSON.stringify({ NODE_ENV: "test" }));
// No mocking - all jobs execute their actual body functions
const result = await workflow.mainJob.trigger({ a: 3, b: 4 });
expect(result).toBe(21); // (3 + 4) * 3 = 21
});
});
Key points:
- Use
.body() for unit testing individual job logic
- Use
vi.spyOn(job, "trigger").mockResolvedValue(...) to mock dependent jobs when they don’t need env
- If dependent jobs require
env, use vi.stubEnv(WORKFLOW_TEST_ENV_KEY, ...) and call .trigger() instead of mocking
- Use
workflow.mainJob.trigger() to execute the full workflow chain and get the result
- Best for: Testing workflow orchestration and job dependencies
End-to-End (E2E) Tests
E2E tests verify your application works correctly when deployed to Tailor Platform. They test the full stack including GraphQL API, database operations, and authentication.
Setting Up E2E Tests
The examples below use graphql-request as a lightweight GraphQL client.
pnpm add -D graphql-request
Global Setup
Create a global setup file that retrieves deployment information before running tests:
import { getMachineUserToken, show } from "@tailor-platform/sdk/cli";
import type { TestProject } from "vitest/node";
declare module "vitest" {
export interface ProvidedContext {
url: string;
token: string;
}
}
export async function setup(project: TestProject) {
const app = await show();
const tokens = await getMachineUserToken({ name: "admin" });
project.provide("url", app.url);
project.provide("token", tokens.accessToken);
}
Test Files
Create tests that use injected credentials to send real queries to your deployed application:
import { randomUUID } from "node:crypto";
import { gql, GraphQLClient } from "graphql-request";
import { describe, expect, inject, test } from "vitest";
function createGraphQLClient() {
const endpoint = new URL("/query", inject("url")).href;
return new GraphQLClient(endpoint, {
headers: {
Authorization: `Bearer ${inject("token")}`,
},
errorPolicy: "all",
});
}
describe("resolver", () => {
const graphQLClient = createGraphQLClient();
describe("incrementUserAge", () => {
const uuid = randomUUID();
test("prepare data", async () => {
const query = gql`
mutation {
createUser(input: {
name: "alice"
email: "alice-${uuid}@example.com"
age: 30
}) {
id
}
}
`;
const result = await graphQLClient.rawRequest(query);
expect(result.errors).toBeUndefined();
});
test("basic functionality", async () => {
const query = gql`
mutation {
incrementUserAge(email: "alice-${uuid}@example.com") {
oldAge
newAge
}
}
`;
const result = await graphQLClient.rawRequest(query);
expect(result.errors).toBeUndefined();
expect(result.data).toEqual({
incrementUserAge: { oldAge: 30, newAge: 31 },
});
});
});
});
Workflow E2E Tests
import { randomUUID } from "node:crypto";
import { describe, expect, test } from "vitest";
import { startWorkflow } from "@tailor-platform/sdk/cli";
import config from "../tailor.config";
import simpleCalculationWorkflow from "../src/workflow/simple";
import userProfileSyncWorkflow from "../src/workflow/wrapTailordb";
describe.concurrent("workflow", () => {
test("simple-calculation: execute workflow and verify success", { timeout: 120000 }, async () => {
const { executionId, wait } = await startWorkflow({
workflow: simpleCalculationWorkflow,
authInvoker: config.auth.invoker("admin"),
arg: { a: 2, b: 3 },
});
console.log(`[simple-calculation] Execution ID: ${executionId}`);
const result = await wait();
expect(result).toMatchObject({
workflowName: "simple-calculation",
status: "SUCCESS",
});
});
test("user-profile-sync: execute workflow and verify success", { timeout: 120000 }, async () => {
const uuid = randomUUID();
const testEmail = `workflow-test-${uuid}@example.com`;
const { executionId, wait } = await startWorkflow({
workflow: userProfileSyncWorkflow,
authInvoker: config.auth.invoker("admin"),
arg: {
name: "workflow-test-user",
email: testEmail,
age: 25,
},
});
console.log(`[user-profile-sync] Execution ID: ${executionId}`);
const result = await wait();
expect(result).toMatchObject({
workflowName: "user-profile-sync",
status: "SUCCESS",
});
});
});
Vitest Configuration
Configure Vitest to use the global setup:
import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
include: ["e2e/**/*.test.ts"],
globalSetup: ["e2e/globalSetup.ts"],
},
});
Key points:
- Tests run against actual deployed application
inject("url") and inject("token") provide deployment credentials automatically
- Machine user authentication enables API access without manual token management
- Verify database persistence and API contracts
- Best for: Integration testing, end-to-end API validation
Testing Best Practices
Test Organization
- Unit tests: Place alongside source files (
src/resolver/*.test.ts)
- E2E tests: Separate directory (
e2e/*.test.ts)
- Test fixtures: Create reusable mocks and helpers
When to Use Each Pattern
| Pattern | Use Case | Deployment Required |
|---|
| Simple resolver test | Pure logic, calculations | No |
| Mock TailorDB Client | Database interactions | No |
| Dependency Injection | Complex business logic | No |
| Workflow unit tests | Individual job logic | No |
| Workflow integration | Full workflow orchestration | No |
| E2E tests | Full stack validation | Yes |
Common Gotchas
- Always restore mocks with
vi.restoreAllMocks() or vi.unstubAllGlobals() in afterEach or afterAll
- E2E tests require unique identifiers (use
randomUUID()) to avoid conflicts
- Remember to clean up test data after E2E tests when possible
Next Steps