Skip to main content

@platform/sdk-testing

@platform/sdk-testing provides everything needed to write isolated, fast, reliable tests for Platform modules — without needing a running backend. All SDK primitives can be replaced with type-identical mocks that track calls, control return values, and simulate errors.


Installation

pnpm add -D @platform/sdk-testing

kernel.mock()

kernel.mock() replaces the real kernel instance with a fully-mocked version. All SDK methods are replaced with vi.fn() (Vitest) or jest.fn() equivalents that return safe defaults:

import { kernel } from '@platform/sdk-core';
import { mockKernel, resetKernel } from '@platform/sdk-testing';

describe('ContactForm', () => {
let mock: ReturnType<typeof mockKernel>;

beforeEach(() => {
mock = mockKernel();
// All kernel methods are now vi.fn() returning safe defaults
});

afterEach(() => {
resetKernel(); // Restore real kernel for next test
});

it('creates a contact on submit', async () => {
mock.data().create.mockResolvedValue({
id: '01j9pcont000000000000001',
name: 'Alice Johnson',
});

const { getByLabelText, getByRole } = render(<ContactForm />);
await userEvent.type(getByLabelText('Name'), 'Alice Johnson');
await userEvent.type(getByLabelText('Email'), '[email protected]');
await userEvent.click(getByRole('button', { name: 'Save' }));

expect(mock.data().create).toHaveBeenCalledWith('contacts', {
name: 'Alice Johnson',
});
});
});

Mock API Coverage

mockKernel() mocks every SDK primitive:

SDKMocked methods
kernel.data()create, retrieve, list, update, delete, analytics, transaction
kernel.auth()getUser, login, logout, hasPermission, hasRole
kernel.money()credit, debit, transfer, hold, confirm, cancel, reversal, getBalance
kernel.files()upload, download, getUrl, uploadImage, processImage, getThumbnail
kernel.events()publish, subscribe, emit, onEvent
kernel.notify()send, sendBatch, sendFromTemplate, broadcast
kernel.audit()record, recordBatch, query, getEntityHistory
kernel.flags()isEnabled, getVariant, snapshot, listFlags

Fixtures

Pre-built data fixtures with realistic ULID identifiers. Use them as return values for mocked SDK calls:

import {
userFixture,
tenantFixture,
walletFixture,
fileFixture,
contactFixture,
} from '@platform/sdk-testing/fixtures';

// Use a fixture directly
mock.auth().getUser.mockResolvedValue(userFixture());

// Override specific fields
const adminUser = userFixture({
roles: ['admin'],
});

// Build a wallet with a specific balance
const richWallet = walletFixture({
availableCents: 1_000_000, // $10,000.00
currency: 'USD',
});

Available Fixtures

FixtureDefault fields
userFixture(overrides?)id, email, firstName, lastName, tenantId, roles: ['member'], emailVerified: true
tenantFixture(overrides?)id, name, plan: 'starter', status: 'active', createdAt
walletFixture(overrides?)id, currency: 'USD', availableCents: 10000, frozenCents: 0, pendingCents: 0
fileFixture(overrides?)id, fileName, mimeType: 'image/webp', sizeBytes: 204800, bucket: 'avatars', status: 'available'
contactFixture(overrides?)id, name, email, phone, status: 'active', tenantId
transactionFixture(overrides?)id, type: 'credit', status: 'completed', amountCents: 5000, currency: 'USD'
auditRecordFixture(overrides?)id, action, entityType, entityId, userId, tenantId, timestamp

createTestContext()

Create a realistic request context for testing server-side module code (API handlers, event consumers):

import { createTestContext } from '@platform/sdk-testing';

describe('PayoutHandler', () => {
it('approves payout for finance admin', async () => {
const ctx = createTestContext({
userId: '01j9pusr0000000000000001',
tenantId: '01j9pten0000000000000001',
roles: ['finance-admin'],
permissions: ['finance.payouts.approve'],
});

const result = await approvePayout(ctx, {
payoutId: '01j9ppay0000000000000001',
});

expect(result.status).toBe('approved');
});

it('rejects payout for non-admin user', async () => {
const ctx = createTestContext({
userId: '01j9pusr0000000000000002',
roles: ['member'],
permissions: [],
});

await expect(approvePayout(ctx, { payoutId: '01j9ppay...' }))
.rejects.toMatchObject({ status: 403 });
});
});

renderWithKernel()

React Testing Library wrapper that wraps the component under test with a mocked kernel context, ThemeProvider, and QueryClientProvider:

import { renderWithKernel } from '@platform/sdk-testing';

it('shows contact list', async () => {
const { mock, getByText } = renderWithKernel(<ContactList />, {
flags: { 'new-list-layout': true },
user: userFixture({ roles: ['admin'] }),
});

mock.data().list.mockResolvedValue({
data: [contactFixture({ name: 'Alice Johnson' })],
meta: { cursor: null, hasMore: false, total: 1 },
});

await waitFor(() => expect(getByText('Alice Johnson')).toBeInTheDocument());
});

renderWithKernel() returns everything from RTL's render() plus the mock object for setting up expectations.


waitForAudit()

Assert that a specific audit record was emitted during a test without relying on timing:

import { waitForAudit } from '@platform/sdk-testing';

it('audits payout approval', async () => {
const { mock } = renderWithKernel(<PayoutApprovalButton payoutId="01j9ppay..." />);

await userEvent.click(screen.getByRole('button', { name: 'Approve' }));

await waitForAudit(mock, {
action: 'payout.approved',
entityType: 'payout',
entityId: '01j9ppay...',
});
});

waitForEvent()

Assert that a specific platform event was published:

import { waitForEvent } from '@platform/sdk-testing';

it('publishes crm.deal.closed event on deal close', async () => {
const { mock } = renderWithKernel(<DealCloseButton dealId="01j9pdeal..." />);

await userEvent.click(screen.getByRole('button', { name: 'Close Deal' }));

await waitForEvent(mock, {
type: 'crm.deal.closed',
payload: expect.objectContaining({ dealId: '01j9pdeal...' }),
});
});

Simulating Errors

Test error handling by configuring mock rejections:

// Simulate insufficient funds
mock.money().debit.mockRejectedValue({
type: 'https://api.septemcore.com/problems/insufficient-funds',
status: 400,
title: 'Insufficient Funds',
detail: 'Available balance (0 cents) is less than requested amount (5000 cents)',
});

// Simulate Kafka unavailability
mock.events().publish.mockRejectedValue({
type: 'https://api.septemcore.com/problems/events-broker-unavailable',
status: 503,
title: 'Event Bus Unavailable',
});

// Simulate slow data layer (test loading states)
mock.data().list.mockImplementation(() =>
new Promise(resolve => setTimeout(() => resolve(paginatedContactsFixture()), 1500))
);

Test Utilities

UtilityDescription
mockKernel()Replace all SDK methods with vi.fn() / jest.fn()
resetKernel()Restore real kernel (call in afterEach)
createTestContext(overrides?)Build server-side request context with JWT fields
renderWithKernel(ui, options?)RTL render with full kernel mock + ThemeProvider + QueryClient
waitForAudit(mock, matcher)Assert audit record emitted — waits up to 5s
waitForEvent(mock, matcher)Assert platform event published — waits up to 5s
paginatedFixture(items, overrides?)Wrap fixture array in { data, meta }
errorFixture(type, status, detail)Build RFC 9457 PlatformError object