@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.click(getByRole('button', { name: 'Save' }));
expect(mock.data().create).toHaveBeenCalledWith('contacts', {
name: 'Alice Johnson',
});
});
});
Mock API Coverage
mockKernel() mocks every SDK primitive:
| SDK | Mocked 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
| Fixture | Default 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
| Utility | Description |
|---|---|
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 |