πͺ’ Shared Resources
History
| Version | Changes |
|---|---|
| v3.0.3-canary.5b54775d | |
| v3.0.3-canary.ffab4562 | |
| v3.0.3-canary.60ff5ce2 |
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 setmoduleexplicitly 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);
});
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.