Skip to main content
Version: v4.x.x (Canary)

πŸͺ’ Shared Resources

History
VersionChanges
v3.0.3-canary.5b54775d
Refactored API with automatic cleanup, lazy loading, and type inference.
v3.0.3-canary.ffab4562
Improved serialization and Windows support.
v3.0.3-canary.60ff5ce2
Introduce Shared Resources.
1Experimental

Share state and methods between test files and processes, enabling advanced integration and end-to-end testing patterns.

What are Shared Resources?​

Shared Resources enable you to define a stateful resource once and access it from multiple test files and processes. Each test gets the same resource instance, allowing you to:

  • Share database connections, in-memory stores, or API clients across tests.
  • Coordinate state and assertions between parallel or sequential tests.
  • Manage setup and teardown logic in one place.

The resource system automatically handles inter-process communication, serialization, and cleanup β€” so you can focus on writing tests.

Enabling Shared Resources​

To use Shared Resources, pass the --sharedResources flag when running Poku:

poku --sharedResources

Or in a configuration file:

{
"sharedResources": true
}

Basic Usage​

1. Define a Resource Context​

Create a resource file using resource.create():

// counter.ts
import { resource } from 'poku';

export const CounterContext = resource.create(() => ({
count: 0,
increment() {
this.count++;
return this.count;
},
getCount() {
return this.count;
},
}));

resource.create() automatically detects the file path of your resource module. It takes a factory function as its first argument β€” a function that returns the resource object with methods.

Troubleshooting: If resource.create() fails to detect the module path automatically, you can set module explicitly as a fallback:

export const CounterContext = resource.create(
() => ({
/* ... */
}),
{ module: __filename } // or import.meta.url for ESM
);

2. Access the Resource in Tests​

Use resource.use() to get the resource:

import { test, assert, resource } from 'poku';
import { CounterContext } from './counter';

test('increment counter', async () => {
const counter = await resource.use(CounterContext);

const current = await counter.getCount();
assert.equal(current, 0);

await counter.increment();
assert.equal(await counter.getCount(), 1);
});

3. Methods are Remote Procedure Calls​

All methods on your resource become async RPCs that work across processes. State is automatically synchronized:

test('shared state across tests', async () => {
const counter = await resource.use(CounterContext);

// This gets the current state from the resource
const value = await counter.getCount();
assert.equal(value, 1); // reflects changes from other test

// This mutates the shared state
await counter.increment();
});

Real-World Example​

Here's a complete example of two tests sharing a counter:

counter.ts

import { resource } from 'poku';

export const CounterContext = resource.create(() => ({
count: 0,
increment() {
this.count++;
return this.count;
},
getCount() {
return this.count;
},
}));

test-a.test.ts

import { assert, test, resource } from 'poku';
import { CounterContext } from './counter';

test('Test A: Increment Counter', async () => {
const counter = await resource.use(CounterContext);

const current = await counter.getCount();
assert.strictEqual(current, 0, 'Should start at 0');

await counter.increment();
assert.strictEqual(
await counter.getCount(),
1,
'Should be 1 after increment'
);
});

test-b.test.ts

import { assert, test, resource, waitForExpectedResult } from 'poku';
import { CounterContext } from './counter';

test('Test B: Verify Counter State', async () => {
const counter = await resource.use(CounterContext);

// If you need the outcome of Test A to be ready before proceeding,
// It is recommended to wait for the expected state to avoid flakiness.
await waitForExpectedResult(counter.getCount, 1);

await counter.increment();
assert.strictEqual(
await counter.getCount(),
2,
'Should be 2 after increment'
);
});

When these tests run, they share the same counter instance. Test B sees the state changes made by Test A.

Advanced Usage​

Resource Lifecycle​

To perform an action when the resource is destroyed, use the onDestroy option:

import { createConnection } from 'mysql2/promise';
import { resource } from 'poku';

export const DatabaseContext = resource.create(
() =>
createConnection({
host: 'localhost',
user: 'root',
database: 'test',
}),
{
onDestroy: (connection) => connection.end(),
}
);
import type { RowDataPacket } from 'mysql2/promise';
import { test, assert, resource } from 'poku';
import { DatabaseContext } from './db.js';

type QueryResult = RowDataPacket & {
total: number;
};

await test('Connection', async () => {
const connection = await resource.use(DatabaseContext);
const [rows] = await connection.execute<QueryResult[]>(
'SELECT 1 + 1 AS total'
);

assert.strictEqual(rows[0].total, 2);
});
tip

Resources can be initialized asynchronously.

Important Notes​

Serialization​

Arguments and return values are serialized using JSON when communicated between processes. Circular references and non-serializable values (such as sockets, streams, and Buffers) are not supported. Class instances are reduced to their enumerable properties, losing their prototype chain. Keep your resource methods simple with basic types: strings, numbers, booleans, arrays, and plain objects.

Lazy Loading​

Resources are created on-demand the first time you call resource.use(). If a test never accesses a resource, it's never created.