Parallelism
Parallelism in Testplane
Testplane runs tests in parallel by launching multiple browsers simultaneously, which are managed by one or several workers. Parallel test execution significantly reduces the overall test run time.
sessionsPerBrowser
In the configuration file, browser types are declared in the browsers field. For each browser type, you can independently configure how many instances can run simultaneously:
// .testplane.config.ts
export default {
browsers: {
chrome: {
sessionsPerBrowser: 5, // up to 5 parallel Chrome sessions
},
firefox: {
sessionsPerBrowser: 2, // up to 2 parallel Firefox sessions
},
},
};
The sessionsPerBrowser parameter is key for managing parallelism. It determines how many browsers of the same type Testplane will launch simultaneously. The default value is 1.
Workers
The workers parameter determines how many child worker processes will be launched. Architecturally, it works as follows:
- Master process — coordinates the launch, forms test queues and assigns tasks to workers;
- Worker processes — execute the tests directly.
Each worker is a separate thread, but within a single worker, tests run concurrently: when a test is waiting for a response from the browser (await), the worker doesn’t stay idle — it switches to the next test. Therefore, even a single worker can handle multiple parallel browser sessions.
workers and sessionsPerBrowser are independent limits. Increasing the number of workers won’t boost actual concurrency if sessionsPerBrowser remains unchanged.
testsPerSession
The testsPerSession parameter determines how many tests can be run sequentially in a single browser session. It limits session reuse to prevent test failures caused by browser degradation, and it’s not related to parallel test execution.
How it works
In this example, the sessionsPerBrowser parameter is set to 5:
const config = {
chrome: {
headless: true,
desiredCapabilities: {
browserName: "chrome",
},
sessionsPerBrowser: 5, // 5 parallel Chrome sessions
waitTimeout: 10000,
},
};
and to 2:
const config = {
firefox: {
headless: true,
desiredCapabilities: {
browserName: "firefox",
},
sessionsPerBrowser: 2, // 2 parallel Firefox sessions
waitTimeout: 10000,
},
};
The value of the workers:
const config = {
system: {
workers: 1,
},
};
Next, the tests:
describe("test examples", () => {
it("Find an element by data‑testid", async ({ browser }) => {
// test body
});
it("Find an element on the main page", async ({ browser }) => {
// test body
});
it("Find an element by ID on the main page", async ({ browser }) => {
// test body
});
it("Find an element by attribute type", async ({ browser }) => {
// test body
});
it("Find an element by text", async ({ browser }) => {
// test body
});
it("Find an element by attribute", async ({ browser }) => {
// test body
});
it("Find a button using the getByRole method", async ({ browser }) => {
// test body
});
});
How parallelism works in this example
For Chrome and Firefox, up to 5 and 2 browser windows are opened simultaneously, respectively. Each of them is a separate independent session with its own sessionId.
With the following parameter values:
const config = {
browsers: {
chrome: { sessionsPerBrowser: 3, testsPerSession: 1 },
firefox: { sessionsPerBrowser: 2, testsPerSession: 10 },
},
};
You’ll get the following result:
CH-* — Chrome sessions
FF-* — Firefox sessions
[n/m] = n out of m slots are occupied
Step 1
chrome [3/3]: windows CH‑1, CH‑2, CH‑3 are opened → run test1, test2, test3
firefox [2/2]: windows FF‑1, FF‑2 are opened → run test1, test2
Step 2
chrome [3/3]: CH‑1 completes test1 and closes; CH‑4 is created → test4 is launched
firefox [2/2]: FF‑1 completes test1 and is reused → test3 is launched
Step 3
chrome [2/3]: CH‑2 completes test2 and closes
firefox [2/2]: FF‑2 completes test2 and is reused → test4 is launched
Step 4
chrome [0/3]: CH‑3 and CH‑4 complete test3 and test4, then close
firefox [0/2]: FF‑1 and FF‑2 complete test3 and test4, then close
workers controls the number of Node.js processes, while sessionsPerBrowser controls the number of concurrent browser sessions within each worker. With workers: 1, all 7 sessions are managed by a single process.
Sharding
When you have thousands of tests, a single run can take an unacceptably long time — even with maximum concurrency. In such cases, sharding is used: the entire test suite is split into several independent parts (chunks) that are run in parallel on different machines or in separate CI-jobs.
@testplane/chunks plugin
The plugin splits the project’s test suite into deterministic fragments, making it easier to run a specific part.
Installation and setup
To install, run the following command:
npm install -D @testplane/chunks
And specify the parameters in the testplane.config.ts file:
module.exports = {
plugins: {
"@testplane/chunks": {
count: 7, // Split tests into 7 chunks
run: 1, // Run the first chunk
},
// other Testplane plugins...
},
// other Testplane settings...
};
As an example, the following test suite will be used:
project/
├── .testplane.conf.js
├── package.json
└── tests/
├── registration.testplane.js # 500 tests
├── payment.testplane.js # 500 tests
└── auth.testplane.js # 500 tests
// registration.testplane.js
describe("Registration", () => {
it("test 1 - empty email shows an error", async ({ browser }) => {});
it("test 2 - invalid email format", async ({ browser }) => {});
it("test 3 - empty password shows an error", async ({ browser }) => {});
// ...
// test 500
});
// payment.testplane.js
describe("Payment", () => {
it("test 1 - empty card number shows an error", async ({ browser }) => {});
it("test 2 - invalid card number", async ({ browser }) => {});
it("test 3 - expired card", async ({ browser }) => {});
// ...
// test 500
});
// auth.testplane.js
describe("Authorization", () => {
it("test 1 - empty email shows an error", async ({ browser }) => {});
it("test 2 - empty password shows an error", async ({ browser }) => {});
it("test 3 - incorrect password shows an error", async ({ browser }) => {});
// ...
// test 500
});
Splitting tests into chunks:
Chunk 0 → tests 1, 2, ..., 500 (≈ first part)
Chunk 1 → tests 1, 2, ..., 500 (≈ second part)
Chunk 2 → tests 1, 2, ..., 500 (≈ third part)
Running each chunk separately via the terminal using environment variables or command‑line arguments:
testplane_chunks_count=3 testplane_chunks_run=0 npx testplane
testplane_chunks_count=3 testplane_chunks_run=1 npx testplane
testplane_chunks_count=3 testplane_chunks_run=2 npx testplane
npx testplane --chunks-count 3 --chunks-run 0
npx testplane --chunks-count 3 --chunks-run 1
npx testplane --chunks-count 3 --chunks-run 2
How to merge reports
To combine several reports into one, use the merge-reports command. It accepts paths to report directories, database files, or databaseUrls.json files, and then creates a new html-report in the destination folder with data from all the provided reports.
Example of usage:
npx html-reporter merge-reports report-chunk-1/ report-chunk-2/ report-chunk-3/ -d merged-report
GitHub Actions example
Below is an example of running @testplane/chunks in GitHub Actions. The tests are split into 3 chunks — each chunk is executed in a separate matrix job. Then, the reports from all chunks are downloaded from S3 and merged into a single final report.
Code
name: Testplane
on:
pull_request:
permissions:
id-token: write
contents: read
jobs:
chunks:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
chunk: [1, 2, 3]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci
- run: npx testplane --chunks-count 3 --chunks-run ${{ matrix.chunk }}
- uses: aws-actions/configure-aws-credentials@v5
with:
role-to-assume: ${{ secrets.AWS_ROLE_ARN }}
aws-region: eu-central-1
- uses: jakejarvis/s3-sync-action@v0.5.1
with:
args: --follow-symlinks
env:
AWS_S3_BUCKET: ${{ secrets.S3_BUCKET }}
AWS_REGION: eu-central-1
SOURCE_DIR: testplane-report
DEST_DIR: testplane/${{ github.run_id }}/chunk-${{ matrix.chunk }}
merge-report:
runs-on: ubuntu-latest
needs: chunks
if: ${{ always() }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci
- uses: aws-actions/configure-aws-credentials@v5
with:
role-to-assume: ${{ secrets.AWS_ROLE_ARN }}
aws-region: eu-central-1
- run: |
mkdir -p reports/chunk-1 reports/chunk-2 reports/chunk-3
aws s3 cp s3://${{ secrets.S3_BUCKET }}/testplane/${{ github.run_id }}/chunk-1/ reports/chunk-1 --recursive
aws s3 cp s3://${{ secrets.S3_BUCKET }}/testplane/${{ github.run_id }}/chunk-2/ reports/chunk-2 --recursive
aws s3 cp s3://${{ secrets.S3_BUCKET }}/testplane/${{ github.run_id }}/chunk-3/ reports/chunk-3 --recursive
- run: |
npx testplane merge-reports \
reports/chunk-1 \
reports/chunk-2 \
reports/chunk-3 \
--output merged-report
- uses: jakejarvis/s3-sync-action@v0.5.1
with:
args: --follow-symlinks
env:
AWS_S3_BUCKET: ${{ secrets.S3_BUCKET }}
AWS_REGION: eu-central-1
SOURCE_DIR: merged-report
DEST_DIR: testplane/${{ github.run_id }}/merged
Recommended settings and their calculation
Workers
The recommended value is 8. Any other value should not exceed the number of CPU cores.
sessionsPerBrowser
Base the value on your hardware capabilities — for debugging or running individual tests, this value typically falls within the 1–5 range. When using local browsers in CI/Docker images, rely on a rough estimate: one LinuxDesktop-browser session consumes 1.2 CPU cores. If you’re using a remote grid, the provider usually specifies the number of available slots — for example, 400. Next, calculate the number of concurrent task runs in CI — say, up to 10. In this case, you can set the total sessionsPerBrowser value in the CI config:
// .testplane.config.ts
export default {
browsers: {
chrome: {
sessionsPerBrowser: 40,
},
},
};
Number of chunks
When calculating the number of chunks, you should take the following factors into account:
- the number of available browsers;
- the overhead for creating a chunk;
- the desired total run time.
Recommendation: aim for at least 500 tests per chunk.