Test Isolation and Parallel Execution
Test isolation means that a test gives the same result whether it runs alone, in any order, several times or at the same moment as other tests, because it builds the state it needs and depends on nothing another test leaves behind. Parallel execution means the test runner runs several tests at once: in separate workers on one machine, or in shards spread across several machines. A checkout suite that takes 24 minutes with one worker can finish in roughly a quarter of that time with four, provided no two tests use the same account, cart or file at the same moment.
Isolation is what makes parallelism safe. The lesson on test data, fixtures and hooks showed how one test sets up and cleans up its own data; this lesson looks at the whole suite: what tests share by accident, how runners divide tests between workers and machines, and how to fix the dependencies a parallel run exposes.
Why it matters
Tests that depend on each other produce results nobody can trust. A test passes in the full run only because an earlier test created the order it reads, then fails when someone runs it alone or adds a worker; two tests edit the same user at the same moment and one of them fails one run in twenty. Such failures look like product defects, cost hours of investigation and teach a team to re-run red builds instead of reading them. Martin Fowler names lack of isolation as a common cause of non-deterministic tests.
Speed is the other half. A suite that takes an hour runs less often and reports after the developer has moved on. Parallel execution keeps a growing suite fast enough to run on every change, and it works only when tests are isolated. Isolation also helps the bug report: a test that fails alone, every time, with its own data, is a ready reproduction for the developer.
How runners execute tests in parallel
In Playwright Test, Jest and pytest-xdist, a worker is a separate operating-system process that runs tests one after another. With four workers, four tests run at the same moment. Their memory is separate, but the test environment, its database, the files on disk and the network ports are not. Runners differ in what they split by default, and in how you give each worker its own resources:
- Playwright Test runs test files in parallel; the tests inside one file run in order in the same worker unless you set
fullyParallel: trueortest.describe.configure({ mode: 'parallel' }). Set the number withworkersin the config or--workers. Every test gets a new browser context, an isolated browser profile like a fresh private window, with its own cookies and storage, and after a failure Playwright replaces the worker with a fresh process. - pytest needs the pytest-xdist plugin:
pytest -n 4, or-n autofor one worker per CPU. Theworker_idfixture returnsgw0,gw1and so on. - Jest spreads test files across worker processes and runs the tests in one file in order;
--runInBandruns everything serially in one process. - JUnit 5 runs tests sequentially in a single thread by default. Parallel execution is opt-in: the configuration parameter
junit.jupiter.execution.parallel.enabled=trueswitches it on, but tests still run one after another until you also make them concurrent, for example withjunit.jupiter.execution.parallel.mode.default=concurrent. The parallel tests are threads in one JVM, so they share static fields and singletons.@ResourceLockdeclares that tests use a shared resource that needs synchronised access. - Per-worker resources: when something cannot be created per test, give each worker its own. Playwright's
testInfo.parallelIndexruns from 0 to workers − 1, so worker 2 can take account 2 from a pool prepared before the run. - Serial exceptions: a test that changes something global, such as the VAT rate or a site-wide feature flag, cannot run beside other tests. Move it into a small separate run with
--workers=1, scheduled so that it never overlaps with the main run in the same environment, instead of making the whole suite serial.
Worked example: three tests that fail only in parallel
A checkout suite of 120 browser tests takes 24 minutes with one worker. The team sets fullyParallel: true and four workers; the run drops to 7 minutes, but three tests now fail in some runs. The whole suite still passes every time with --workers=1, so the likely cause is interference between tests, not the product.
Every test signs in as qa.shared@example.com. "Cart total" adds items of 150.00 and 80.00 and expects 230.00, but sometimes sees 0.00, because "empty cart", running on another worker, clears the same cart. "Order history" expects the list to show three orders, the ones placed by the three ordering tests in its file; it passed only because it used to run after them in the same worker, and it fails even when run alone. "Download invoice" saves to downloads/invoice.pdf, where another worker's invoice can overwrite it.
One shared account
- Worker 1 test "cart total"
- Test environment one cart for every test
- Worker 2 test "empty cart"
- 1 From Worker 1 To Test environment: Add items of 150.00 and 80.00
qa.shared@example.com - 2 From Worker 2 To Test environment: Empty the cart, at the same moment Same account, so the same cart
qa.shared@example.com - 3 Worker 2 internally: Assert: the cart is empty Pass
- 4 From Worker 1 To Test environment: Open the cart and read the total
- 5 From Test environment To Worker 1: Shows a total of 0.00 reply
- 6 Worker 1 internally: Assert: the total is 230.00 Fail
Outcome: Fails only when the two tests overlap in time; each passes alone and with one worker. Fail
One fresh account per worker
- Worker 1 test "cart total"
- Test environment a separate account and cart per worker
- Worker 2 test "empty cart"
- 1 From Worker 1 To Test environment: Add items of 150.00 and 80.00
qa+3f9a…@example.com - 2 From Worker 2 To Test environment: Empty its own cart, at the same moment A different account, so a different cart
qa+c41e…@example.com - 3 Worker 2 internally: Assert: the cart is empty Pass
- 4 From Worker 1 To Test environment: Open the cart and read the total
- 5 From Test environment To Worker 1: Shows a total of 230.00 reply
- 6 Worker 1 internally: Assert: the total is 230.00 Pass
Outcome: Same timing, separate carts: both tests pass however their steps interleave. Pass
- Each worker creates its own fresh account in a worker-scoped fixture, with a UUID in the email so shards cannot collide either.
- Tests in one worker still share that account one after another, so a
beforeEachhook empties the cart before every test instead of trusting the previous one. - "Order history" creates its own three orders through the API and asserts on their ids, not on a total count.
- The invoice is saved to
test.info().outputPath(), a folder unique to each test. - The fix is confirmed with
--repeat-each=10at four workers, because a single green run proves little.
// fixtures.ts: one fresh account per worker process
import { test as base } from '@playwright/test';
import { randomUUID } from 'node:crypto';
type Account = { id: string; email: string };
export const test = base.extend<{}, { account: Account }>({
account: [async ({ playwright }, use) => {
const api = await playwright.request.newContext({ baseURL: process.env.BASE_URL });
const email = `qa+${randomUUID()}@example.com`; // unique in every shard
const res = await api.post('/api/test-accounts', { data: { email } });
const account: Account = await res.json();
await use(account); // shared by this worker's tests
await api.delete(`/api/test-accounts/${account.id}`);
await api.dispose();
}, { scope: 'worker' }],
});
// cart.spec.ts
import { test } from './fixtures';
test.beforeEach(async ({ request, account }) => {
await request.delete(`/api/test-accounts/${account.id}/cart`); // start empty
});
test('download invoice', async ({ page, account }) => {
// ...sign in as account.email and place an order...
const downloading = page.waitForEvent('download');
await page.getByRole('button', { name: 'Download invoice' }).click();
await (await downloading).saveAs(test.info().outputPath('invoice.pdf'));
});Common mistakes
- Switching on parallel execution without first checking what tests share, then blaming the runner for the failures it exposes.
- Fixing an order dependency by forcing an order, with serial mode or numbered file names, instead of letting each test build its own state.
- Using the worker index as the only unique part of a name, so worker 0 of every shard uses the same account.
- Asserting on global counts such as "the list has three orders" in a shared environment instead of on the records the test created.
- Judging a sharded run by one shard's result instead of the merged report of all shards.
- Retrying interference failures until they pass, which hides the shared state instead of removing it.
The QA perspective
Isolation is something a tester can check and ask for. Before a suite goes parallel, and whenever you review an automated test, run four checks: does it pass alone, in a shuffled order, twice in a row and with several workers? Jest has --randomize, the pytest-randomly plugin shuffles pytest runs, and Playwright has --repeat-each. Then ask what each test changes and who else can see that change. Many fixes need the developers or the platform team: an API to create test accounts, a way to reset a cart, a separate tenant per run, an environment that can take the load of four workers.
When a test fails only in parallel, record what separates interference from a product defect: the worker and shard it ran on, the account and record ids it used, what else was running at the same time, and whether it passes alone and with one worker. A confirmed interference is a defect in the test suite with an owner and a fix, not noise to re-run.
Practice exercise
Make a settings suite safe to run in parallel
The suite below works with one worker. The team wants to run it with four workers and two shards. For each test, say what it shares with other tests, how you would isolate it, and which tests can stay as they are or must run separately. Then list what changes because of the two shards.
account-settings.spec.ts (Playwright)
Today: workers: 1
Planned: workers: 4, fullyParallel: true
Planned CI: --shard=1/2 and --shard=2/2, both against the same staging environment
Every test signs in as qa.settings@example.com
1 change name sets the display name to "Sara A."
2 name in header expects "Sara A." in the page header
3 enable 2FA turns on two-factor sign-in for the account
4 export data saves the export to downloads/export.zip, then opens it
5 default theme sets the site-wide default theme to dark in the admin panel
6 language list reads the list of supported languages and changes nothing- For each test, write down what it reads and what it changes.
- Mark every change that another test also reads or changes.
- Choose a fix for each: own data, own file, a per-worker resource, or a separate serial run.
- Check what the two shards add, given that both use the same staging environment.
Show the model answer
Tests 1 and 2: test 2 depends on test 1 having run first on the same account. With fullyParallel it can run before test 1 or on another worker. Fix: each test gets a fresh account from a fixture, and test 2 sets the name itself through the API before checking the header.
Test 3: turning on two-factor sign-in for the shared account breaks the sign-in of every other test, including the read-only test 6. It needs its own fresh account, which it can delete afterwards.
Test 4: two workers can write downloads/export.zip at the same time. Save to test.info().outputPath('export.zip') instead.
Test 5: a site-wide setting cannot be isolated per account. Move it to a separate run with --workers=1 that runs once, before or after the shard jobs and never at the same time, because they all use the same staging environment. It sets the theme it expects at its start and restores the default in teardown. Test 6 only reads shared reference data and can stay as it is once it signs in with its own or a read-only account.
Shards: both shards use the same staging environment, so account names must be unique across shards, for example with a UUID, because the worker index alone repeats. Merge the two shard reports into one before judging the run, and confirm the fixes with several repeated runs at four workers.
Frequently asked questions
Does parallel execution make tests flaky?
Mostly it exposes problems that were already there: shared state and order dependencies that a fixed order and timing kept hidden. It can add one real cause, load: four workers can slow a small test environment until waits time out. If failures disappear with fewer workers and no other test touched the failing test's data, suspect load rather than shared state.
What is the difference between workers and shards?
Workers are parallel processes on one machine (threads, in JUnit 5); shards are parts of the suite run by separate machines or CI jobs. They combine: four shards with four workers each run up to sixteen tests at once. Workers share one machine's CPU and disk; shards share only the test environment, and their reports must be merged.
Must every test have its own account?
A fresh account per test is the safest choice when accounts are cheap to create. One account per worker is a common compromise: tests in one worker run one after another, so it works if each test resets what it changes at its start. Read-only data, such as a catalogue no test edits, can be shared by all tests.
References
- Eradicating Non-Determinism in Tests (Martin Fowler)Why flaky tests destroy a suite's value, their common causes (isolation, asynchrony, remote services, time, resource leaks) and quarantine as a response.
- Playwright DocumentationOfficial documentation for Playwright, an end-to-end test framework that drives Chromium, WebKit and Firefox.
- Best Practices — PlaywrightTest user-visible behaviour, keep tests isolated, prefer resilient locators and web-first assertions, and debug with traces.
- Best Practices — CypressCypress guidance on selecting elements with dedicated attributes, avoiding fixed waits and keeping tests independent.
Practise with Mutqan tools
Generate UUID v4 (random) and v7 (time-ordered) identifiers in bulk, and validate existing ones.
Generate test email addresses with safe reserved domains (example.com, test) and plus-addressing variants.
Generate fake user records (name, username, email, password, avatar seed, role) for test accounts.
One-stop factory for synthetic test data: users, customers, companies, addresses, numbers, dates and strings in JSON, CSV, SQL or XML.
Go deeper
Your progress on this topic
Not started
Mark the topic complete when you have studied it and done the exercise.
Sign in to track your progress
Mark topics complete, continue where you left off and unlock specialisation exams.