Concurrency testing helps detect race conditions and timing-dependent bugs in asynchronous code by controlling the execution order of promises and async operations.
Schedulers allow you to:
Generate schedulers that control async execution order.
/**
* Generate schedulers for testing asynchronous code with various execution orderings
* @param constraints - Constraints for scheduler behavior
* @returns Arbitrary generating schedulers
*/
function scheduler<TMetaData = unknown>(constraints?: SchedulerConstraints): Arbitrary<Scheduler<TMetaData>>;
interface SchedulerConstraints {
act?: SchedulerAct;
}
type SchedulerAct = (f: () => Promise<void>) => Promise<unknown>;Create schedulers with predefined execution order for deterministic replay.
/**
* Create custom scheduler with predefined resolution order
* @param customOrderingOrConstraints - Execution order array or constraints
* @param constraintsOrUndefined - Optional constraints when first param is ordering
* @returns Scheduler or template tag function for creating schedulers
*/
function schedulerFor<TMetaData = unknown>(
customOrderingOrConstraints?: number[] | SchedulerConstraints,
constraintsOrUndefined?: SchedulerConstraints
): Scheduler<TMetaData> | ((strs: TemplateStringsArray, ...ordering: number[]) => Scheduler<TMetaData>);/**
* Interface for controlling promise execution order
*/
interface Scheduler<TMetaData = unknown> {
/**
* Wrap an async operation for scheduled execution
* @param task - Async function to schedule
* @returns Promise that resolves according to schedule
*/
schedule<T>(task: Promise<T>, label?: string, metadata?: TMetaData): Promise<T>;
/**
* Wrap multiple async operations
* @param tasks - Array of promises to schedule
* @returns Promise resolving to array of results
*/
scheduleSequence(tasks: SchedulerSequenceItem<TMetaData>[]): Promise<unknown[]>;
/**
* Get number of scheduled tasks
*/
count(): number;
/**
* Wait for all scheduled tasks to complete
*/
waitAll(): Promise<void>;
/**
* Wait for one scheduled task to complete
*/
waitOne(): Promise<void>;
/**
* Get execution report
*/
report(): SchedulerReportItem<TMetaData>[];
}
interface SchedulerSequenceItem<TMetaData> {
builder: () => Promise<unknown>;
label: string;
metadata?: TMetaData;
}
interface SchedulerReportItem<TMetaData> {
status: 'resolved' | 'rejected' | 'pending';
value?: unknown;
label: string;
metadata?: TMetaData;
}Test async operations with different execution orders:
import { scheduler, property, assert } from 'fast-check';
// Function to test
async function transferMoney(
scheduler: Scheduler,
from: Account,
to: Account,
amount: number
): Promise<void> {
const fromBalance = await scheduler.schedule(from.getBalance(), 'get-from');
const toBalance = await scheduler.schedule(to.getBalance(), 'get-to');
if (fromBalance < amount) {
throw new Error('Insufficient funds');
}
await scheduler.schedule(from.setBalance(fromBalance - amount), 'set-from');
await scheduler.schedule(to.setBalance(toBalance + amount), 'set-to');
}
// Test with different execution orders
assert(
property(scheduler(), async (s) => {
const from = new Account(100);
const to = new Account(50);
await transferMoney(s, from, to, 30);
await s.waitAll();
// Verify final state
const fromFinal = await from.getBalance();
const toFinal = await to.getBalance();
return fromFinal === 70 && toFinal === 80;
})
);Test concurrent operations to find race conditions:
import { scheduler, property, assert } from 'fast-check';
class Counter {
private value = 0;
async increment(s: Scheduler): Promise<void> {
const current = await s.schedule(Promise.resolve(this.value), 'read');
await s.schedule(Promise.resolve(), 'delay'); // Simulate async work
this.value = current + 1;
await s.schedule(Promise.resolve(), 'write');
}
getValue(): number {
return this.value;
}
}
// This test will likely find a race condition
assert(
property(scheduler(), async (s) => {
const counter = new Counter();
// Two concurrent increments
const p1 = counter.increment(s);
const p2 = counter.increment(s);
await Promise.all([p1, p2]);
await s.waitAll();
// Should be 2, but might be 1 due to race condition
return counter.getValue() === 2;
})
);Reproduce specific scenarios with custom orderings:
import { schedulerFor } from 'fast-check';
// Define specific execution order
const s = schedulerFor([2, 1, 3]); // Tasks resolve in order: 2nd, 1st, 3rd
async function testScenario() {
const result1 = s.schedule(asyncOp1(), 'op1'); // Executes 2nd
const result2 = s.schedule(asyncOp2(), 'op2'); // Executes 1st
const result3 = s.schedule(asyncOp3(), 'op3'); // Executes 3rd
await s.waitAll();
// Verify behavior with this specific ordering
const report = s.report();
console.log(report);
}
// Using template literal syntax
const s2 = schedulerFor`${2}${1}${3}`;Combine schedulers with model-based testing:
import {
scheduler,
commands,
scheduledModelRun,
property,
assert,
constant,
} from 'fast-check';
class AsyncCommand implements AsyncCommand<Model, Real, true> {
async check(m: Readonly<Model>): Promise<boolean> {
return true;
}
async run(m: Model, r: Real): Promise<void> {
// Async operations on model and real system
await m.performOperation();
await r.performOperation();
}
}
// Test with scheduler to find race conditions
assert(
property(
scheduler(),
commands([constant(new AsyncCommand())], { maxCommands: 10 }),
async (s, cmds) => {
await scheduledModelRun(s, setup, cmds);
}
)
);Use scheduler reports to understand execution order:
import { schedulerFor } from 'fast-check';
const s = schedulerFor();
async function debug() {
await s.schedule(asyncOp1(), 'operation-1', { priority: 'high' });
await s.schedule(asyncOp2(), 'operation-2', { priority: 'low' });
await s.schedule(asyncOp3(), 'operation-3', { priority: 'medium' });
await s.waitAll();
const report = s.report();
report.forEach((item) => {
console.log(`${item.label}: ${item.status}`, item.metadata);
});
}Wrap scheduled tasks with custom behavior:
import { scheduler } from 'fast-check';
// Custom act function that logs all scheduled operations
const customAct = async (f: () => Promise<void>) => {
console.log('Starting scheduled operation...');
await f();
console.log('Completed scheduled operation');
};
const s = scheduler({ act: customAct });
// Use scheduler with custom act function
assert(
property(s, async (scheduler) => {
// All scheduled operations will be logged
await scheduler.schedule(asyncOp(), 'my-op');
await scheduler.waitAll();
})
);type SchedulerAct = (f: () => Promise<void>) => Promise<unknown>;