πͺ’ Shared Resources
Share state, servers, database connections, and more between parallel test files β no duplicated setup, no conflicts.
PluginHistory
| Version | Changes |
|---|---|
| v1.1.1 | isolation: none by forcing in-process mode and skipping IPC wiring. |
| v1.1.0 | |
| v3.0.3-canary.68e71482 | |
| v3.0.3-canary.5b54775d | |
| v3.0.3-canary.ffab4562 | |
| v3.0.3-canary.60ff5ce2 |
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.
Installβ
npm i -D @pokujs/shared-resources
Enabling the Pluginβ
Add the plugin to your Poku configuration file:
// poku.config.ts
import { sharedResources } from '@pokujs/shared-resources';
import { defineConfig } from 'poku';
export default defineConfig({
plugins: [sharedResources()],
});
You can also register custom codecs at plugin setup time:
// poku.config.ts
import { sharedResources } from '@pokujs/shared-resources';
import { defineConfig } from 'poku';
export default defineConfig({
plugins: [
sharedResources({
codecs: [
// custom ArgCodec values
],
}),
],
});
Basic Usageβ
1. Define a Resource Contextβ
Create a resource file using resource.create():
// counter.ts
import { resource } from '@pokujs/shared-resources';
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 { resource } from '@pokujs/shared-resources';
import { assert, test } 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();
});
On isolation: 'none', the plugin automatically switches to in-process mode.
In this mode, methods are still exposed as async functions, but calls execute directly in the same process (no IPC).
Execution Modesβ
The plugin chooses the execution mode from Poku isolation settings:
isolation: 'process'(or other process-based isolation) uses IPC mode.isolation: 'none'uses in-process mode and skips IPC wiring.
This means Shared Resources work consistently in both modes without additional configuration.
Real-World Exampleβ
Here's a complete example of two tests sharing a counter:
counter.ts
import { resource } from '@pokujs/shared-resources';
export const CounterContext = resource.create(() => ({
count: 0,
increment() {
this.count++;
return this.count;
},
getCount() {
return this.count;
},
}));
test-a.test.ts
import { resource } from '@pokujs/shared-resources';
import { assert, test } 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 { resource } from '@pokujs/shared-resources';
import { assert, test, 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 { resource } from '@pokujs/shared-resources';
import { createConnection } from 'mysql2/promise';
export const DatabaseContext = resource.create(
() =>
createConnection({
host: 'localhost',
user: 'root',
database: 'test',
}),
{
onDestroy: (connection) => connection.end(),
}
);
import type { RowDataPacket } from 'mysql2/promise';
import { resource } from '@pokujs/shared-resources';
import { assert, test } 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 through a codec pipeline.
Built-in codecs support:
undefinedbigintDateMapSet- Arrays and objects (including nested combinations)
Class instances are serialized as own enumerable data properties by default. When values cross process boundaries, prototype chains are not reconstructed automatically.
If you need custom reconstruction behavior, register an ArgCodec.
Custom Codecs (ArgCodec)β
Use codecs for custom types that cannot be represented faithfully by default JSON semantics.
import type { ArgCodec } from '@pokujs/shared-resources';
import { resource } from '@pokujs/shared-resources';
class Token {
constructor(public value: string) {}
}
const tokenCodec: ArgCodec<Token> = {
tag: Symbol.for('example:Token'),
is: (v): v is Token => v instanceof Token,
encode: (v) => ({ value: v.value }),
decode: (v) => new Token((v as { value: string }).value),
};
resource.configure({
codecs: [tokenCodec],
});
resource.configure({ codecs }) is the safest place to register codecs because resource modules are evaluated in both parent and child processes.
sharedResources({ codecs }) configures codecs in the parent process.
Use both only when your setup requires it.
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.