Parallel Testing with @fast-check/worker
Relevant source files
The following files were used as context for generating this wiki page:
The @fast-check/worker package provides functionality to run property-based tests within dedicated worker threads. Its primary purpose is to enable fast-check to test synchronous code that may contain infinite loops or other blocking operations. By isolating the predicate execution in a separate process, the main test runner can safely terminate it if it exceeds a specified timeout, and then proceed with shrinking the failing case.
This mechanism is crucial because most JavaScript test runners cannot interrupt a synchronous piece of code once it has started executing. @fast-check/worker solves this by offloading the execution, making tests more robust against such failures. It is designed to be integrated with test runner-specific packages, such as @fast-check/jest.
Sources: packages/worker/README.md, packages/worker/CHANGELOG.md:162-164
Architecture
The core architecture of @fast-check/worker revolves around a main run function that orchestrates the execution of predicates in a pool of worker threads. When a property is asserted, instead of running the predicate in the main thread, it is passed to the worker environment.
High-Level Execution Flow
The following diagram illustrates the process when a fast-check assertion uses the worker runner.
This diagram shows how the test runner's call to fc.assert is delegated to the @fast-check/worker's run function, which manages predicate execution in isolated worker threads.
Sources: packages/worker/src/main.ts, packages/jest/CHANGELOG_1.X.md:79-81
Integration with Test Runners
@fast-check/worker is intended to be used as a building block for other packages, not directly by end-users. For instance, @fast-check/jest declares it as an optional peer dependency to provide a worker-based runner. This allows users of @fast-check/jest to opt-in to running their properties in isolated workers.
Sources: packages/jest/package.json:62-67, packages/jest/CHANGELOG_1.X.md:79-81
Key Components
The run function
The main entry point of the package is the run function. It is responsible for setting up the worker environment and executing the provided predicate.
function run<Ts extends any[]>(
workerFile: string,
predicate: (...args: Ts) => boolean | void | Promise<boolean | void>,
options?: WorkerPropertyOptions,
): IAsyncProperty<Ts>;
This function takes the path to a worker file, the predicate to be tested, and an optional configuration object. It returns an IAsyncProperty that fast-check can execute.
Sources: packages/worker/src/main.ts:103-108
Worker Pool
To manage worker threads efficiently, the package uses a worker pool. An internal IWorkerPool interface defines the contract for acquiring and releasing workers. The default implementation is BasicPool, which manages a set of workers and handles their lifecycle.
The interaction with the worker pool is shown in the sequence diagram below.
This diagram shows the lifecycle of a worker: it is acquired from the pool, runs a task, and is then released back to the pool, which may terminate it based on the configured isolation level.
Sources: packages/worker/src/internals/worker-pool/BasicPool.ts, packages/worker/CHANGELOG.md:124-125
Isolation Levels
@fast-check/worker supports different isolation levels to control the lifecycle of worker threads. This allows for a trade-off between performance (re-using workers) and isolation (ensuring no state is shared between test runs). This feature was introduced in version 0.0.7.
| Level | Description |
|---|---|
file | A single worker is created and reused for all the properties of a single test file. |
property | A new worker is created for each property. It is reused across all the runs of this property. |
run | A new worker is created for each single run of a property. |
The default isolation level is run if not specified.
Sources: packages/worker/README.md, packages/worker/CHANGELOG.md:121-122
Configuration
The run function can be configured via the WorkerPropertyOptions object.
| Option | Type | Description |
|---|---|---|
timeout | number | Timeout in milliseconds for the predicate execution. |
isolationLevel | 'file' | 'property' | 'run' | Defines the lifecycle of the workers. Defaults to run. |
maxWorkers | number | The maximum number of parallel workers. Defaults to os.cpus().length. |
pool | IWorkerPool | An optional custom worker pool implementation. |
extra | unknown | Extra data to be passed to the worker during initialization. |
Sources: packages/worker/src/main.ts:32-60, packages/worker/README.md
Feature Evolution
The @fast-check/worker package has evolved over time to add more features and improve its robustness.
- v0.0.1: Initial release, providing the core ability to run synchronous code in a worker to prevent infinite loops from blocking the test runner.
- v0.0.4: Added the ability to terminate workers early if the property they are running has timed out.
- v0.0.7: Introduced isolation levels (
file,property,run) to give users more control over worker lifecycles. - v0.3.0: Added support for non-serializable data in workers, increasing flexibility.
- v0.4.0: Changed worker respawn logic to only occur on a crash, not on a simple test failure. Also added support for
preconditions in properties. - v0.5.0: Dropped support for
fast-checkv3 and now requiresfast-checkv4 or higher.
Sources: packages/worker/CHANGELOG.md, packages/worker/package.json:44-46
Conclusion
@fast-check/worker is a powerful extension to the fast-check ecosystem that significantly enhances its capability to test synchronous code. By isolating property executions in worker threads, it provides a safety net against infinite loops and other blocking operations, ensuring that the test runner remains responsive and can report failures correctly. Its configurable nature, particularly through isolation levels, allows developers to balance performance and test purity according to their needs.