टेस्ट आइसोलेशन और पैरलल एक्ज़ीक्यूशन

Test isolation का मतलब है कि test अकेले चले, किसी भी क्रम में चले, कई बार चले या दूसरे tests के साथ एक ही पल में चले, उसका result एक जैसा रहे, क्योंकि वह अपनी ज़रूरत की state खुद बनाता है और किसी दूसरे test की छोड़ी हुई चीज़ पर निर्भर नहीं होता। Parallel execution का मतलब है कि test runner एक साथ कई tests चलाता है: एक मशीन पर अलग-अलग workers में, या कई मशीनों पर बँटे shards में। जो checkout suite एक worker के साथ 24 मिनट लेता है, वह चार workers के साथ लगभग उसके एक-चौथाई समय में पूरा हो सकता है, बशर्ते कोई दो tests एक ही पल में एक ही account, cart या file इस्तेमाल न करें।

Isolation ही parallelism को सुरक्षित बनाता है। Test data, fixtures और hooks वाले lesson ने दिखाया कि एक test अपना data कैसे set up और clean up करता है; यह lesson पूरे suite को देखता है: tests अनजाने में क्या share करते हैं, runners tests को workers और मशीनों में कैसे बाँटते हैं, और parallel run से उजागर होने वाली dependencies को कैसे ठीक करें।

यह क्यों ज़रूरी है

एक-दूसरे पर निर्भर tests ऐसे results देते हैं जिन पर कोई भरोसा नहीं कर सकता। कोई test पूरे run में सिर्फ़ इसलिए pass होता है क्योंकि पहले वाले test ने वह order बनाया जिसे वह पढ़ता है, और जब कोई उसे अकेले चलाता है या worker बढ़ाता है तो वह fail हो जाता है; दो tests एक ही पल में एक ही user को edit करते हैं और उनमें से एक हर बीस runs में एक बार fail होता है। ऐसे failures product defects जैसे दिखते हैं, जाँच में घंटों खा जाते हैं और team को लाल builds पढ़ने की बजाय दोबारा चलाना सिखा देते हैं। Martin Fowler isolation की कमी को non-deterministic tests का एक आम कारण बताते हैं।

Speed दूसरा पहलू है। एक घंटे वाला suite कम बार चलता है और तब report करता है जब developer किसी और काम पर जा चुका होता है। Parallel execution बढ़ते suite को इतना तेज़ रखता है कि वह हर change पर चल सके, और यह तभी काम करता है जब tests isolated हों। Isolation bug report में भी मदद करता है: जो test अकेले, हर बार, अपने data के साथ fail होता है, वह developer के लिए तैयार reproduction है।

Isolation और shared state की समस्याएँ

Isolated test अपनी शुरुआती state खुद नियंत्रित करता है: वह अपनी ज़रूरत की चीज़ें बनाता है या fixture से पाता है, और सिर्फ़ उसी data पर assert करता है जो उसने बनाया या जिसे कोई test नहीं बदलता। Playwright की best practices कहती हैं कि हर test अपने storage, cookies और data के साथ स्वतंत्र रूप से चले; Cypress पिछले tests की state पर निर्भर tests को anti-pattern मानता है।

Shared state वह सब कुछ है जिसे एक test बदल सकता है और दूसरा test देख सकता है। Read-only data, जैसे ऐसा product catalogue जिसे कोई test edit नहीं करता, share करना सुरक्षित है; shared mutable state नहीं। इलाज यह है कि हर test को, या कम से कम हर worker को, जो वह बदलता है उसकी अपनी copy दें, और वह state test की शुरुआत में बनाएँ। Martin Fowler cleanup पर भरोसा करने की बजाय शुरुआती state दोबारा बनाने को बेहतर मानते हैं: cleanup fail हो तो कोई दूसरा test fail होता है, bug से दूर। Shared mutable state पाँच जगह छिपती है:

  • Server data: कई tests का इस्तेमाल किया एक account, cart, order या coupon। एक test वह cart खाली कर देता है जिसे दूसरा जाँच रहा है; single-use coupon सिर्फ़ उस test के लिए काम करता है जो पहले पहुँचे।
  • Order dependencies: test B वह order पढ़ता है जो test A ने बनाया। B सामान्य क्रम में pass होता है, और अकेले, shuffled क्रम में या किसी दूसरे worker पर चलने पर fail होता है।
  • In-memory state: module-level variable, singleton (पूरे program में shared एक object), cache, mocked clock या कोई "current user" जिसे एक test set करता है और कभी reset नहीं करता। Worker processes memory share नहीं करते, लेकिन एक ही worker के tests करते हैं, और JUnit 5 के parallel threads भी करते हैं।
  • Browser state: cookies, local storage और signed-in session। सिर्फ़ पढ़ने वाले tests के लिए saved sign-in ठीक है; जो test sign out करता है या profile edit करता है, उसे अपना session चाहिए।
  • मशीन और environment के resources: downloads/report.csv जैसा fixed file path, fixed port, एक shared mailbox, rate-limited sandbox API, site-wide feature flag।

आज़माएँ: फ़ेक ईमेल जनरेटर

Runners tests को parallel में कैसे चलाते हैं

Playwright Test, Jest और pytest-xdist में worker operating system का एक अलग process है जो tests को एक के बाद एक चलाता है। चार workers हों तो एक ही पल में चार tests चलते हैं। उनकी memory अलग होती है, लेकिन test environment, उसका database, disk पर files और network ports अलग नहीं होते। Runners इसमें अलग हैं कि वे default रूप से क्या बाँटते हैं, और हर worker को उसके अपने resources कैसे दिए जाते हैं:

  • Playwright Test test files को parallel में चलाता है; एक file के अंदर के tests उसी worker में क्रम से चलते हैं, जब तक आप fullyParallel: true या test.describe.configure({ mode: 'parallel' }) set न करें। संख्या config में workers से या --workers से तय करें। हर test को नया browser context मिलता है, यानी fresh private window जैसा isolated browser profile, जिसकी अपनी cookies और storage होती हैं, और किसी failure के बाद Playwright worker को नए process से बदल देता है।
  • pytest को pytest-xdist plugin चाहिए: pytest -n 4, या हर CPU पर एक worker के लिए -n auto। worker_id fixture gw0, gw1 आदि लौटाता है।
  • Jest test files को worker processes में बाँटता है और एक file के tests क्रम से चलाता है; --runInBand सब कुछ एक process में serially चलाता है।
  • JUnit 5 default रूप से tests को एक thread में क्रम से चलाता है। Parallel execution opt-in है: configuration parameter junit.jupiter.execution.parallel.enabled=true इसे चालू करता है, लेकिन tests तब भी एक के बाद एक चलते हैं जब तक आप उन्हें concurrent न बनाएँ, जैसे junit.jupiter.execution.parallel.mode.default=concurrent से। Parallel tests एक ही JVM के threads होते हैं, इसलिए वे static fields और singletons share करते हैं। @ResourceLock बताता है कि tests ऐसा shared resource इस्तेमाल करते हैं जिसे synchronised access चाहिए।
  • Per-worker resources: जो चीज़ हर test के लिए नहीं बन सकती, वह हर worker को अलग दें। Playwright का testInfo.parallelIndex 0 से workers − 1 तक जाता है, इसलिए worker 2 run से पहले तैयार pool से account 2 ले सकता है।
  • Serial अपवाद: जो test कोई global चीज़ बदलता है, जैसे VAT rate या site-wide feature flag, वह दूसरे tests के साथ नहीं चल सकता। पूरे suite को serial बनाने की बजाय उसे --workers=1 वाले छोटे अलग run में ले जाएँ, और उसे ऐसे schedule करें कि वह उसी environment पर main run के साथ-साथ कभी न चले।

Sharding: suite को कई मशीनों में बाँटना

Workers एक ही मशीन का CPU और memory share करते हैं, इसलिए मशीन व्यस्त हो जाने के बाद और workers जोड़ने से फ़ायदा नहीं होता। Sharding suite को कई मशीनों में बाँटती है: हर CI job एक shard चलाता है, यानी tests का एक तय हिस्सा, और हर shard फिर भी कई workers इस्तेमाल कर सकता है। npx playwright test --shard=2/4 चार में से दूसरा हिस्सा चलाता है; Jest और Vitest भी यही --shard option लेते हैं।

फ़ायदा balance तय करता है, क्योंकि run तब ख़त्म होता है जब सबसे धीमा shard ख़त्म होता है। fullyParallel के बिना Playwright पूरी files shards को देता है, इसलिए एक लंबी file एक shard को व्यस्त रख सकती है जबकि बाक़ी इंतज़ार करते हैं; इसके साथ अलग-अलग tests बाँटे जाते हैं। हर shard सिर्फ़ अपने tests की report देता है: Playwright हर shard के लिए blob report लिखता है, और आख़िरी job उन्हें एक report में merge करता है, उसके बाद ही कोई run पर फ़ैसला करे।

Shards isolation का अपना ख़तरा भी लाते हैं। वे अलग मशीनों पर चलते हैं लेकिन आम तौर पर एक ही test environment पर, और worker indexes हर shard में दोहराए जाते हैं। इसलिए qa+w0@example.com जैसा नाम, या pool का account 0, एक साथ चार workers इस्तेमाल करते हैं; shard number जोड़ें, या UUID जैसा random हिस्सा इस्तेमाल करें।

# Four CI jobs start at the same time, one shard each
npx playwright test --shard=1/4 --reporter=blob
npx playwright test --shard=2/4 --reporter=blob
npx playwright test --shard=3/4 --reporter=blob
npx playwright test --shard=4/4 --reporter=blob

# A final job downloads the four blob reports into one folder and merges them
npx playwright merge-reports --reporter html ./all-blob-reports

आज़माएँ: UUID जनरेटर

Worked example: तीन tests जो सिर्फ़ parallel में fail होते हैं

120 browser tests वाला एक checkout suite एक worker के साथ 24 मिनट लेता है। Team fullyParallel: true और चार workers set करती है; run 7 मिनट का हो जाता है, लेकिन अब तीन tests कुछ runs में fail होते हैं। पूरा suite --workers=1 के साथ अब भी हर बार pass होता है, इसलिए संभावित कारण tests के बीच टकराव है, product नहीं।

हर test qa.shared@example.com से sign in करता है। "Cart total" 150.00 और 80.00 के items जोड़कर 230.00 की उम्मीद करता है, लेकिन कभी-कभी 0.00 देखता है, क्योंकि दूसरे worker पर चल रहा "empty cart" वही cart खाली कर देता है। "Order history" उम्मीद करता है कि list में तीन orders दिखें, वही जो उसकी file के तीन ordering tests बनाते हैं; यह सिर्फ़ इसलिए pass होता था कि यह उसी worker में उनके बाद चलता था, और अकेले चलाने पर भी fail होता है। "Download invoice" downloads/invoice.pdf में save करता है, जहाँ किसी दूसरे worker की invoice उसे overwrite कर सकती है।

दो workers "cart total" और "empty cart" एक ही पल में चलाते हैं: एक shared account पर cart test के बीच में खाली हो जाता है और 230.00 की जगह 0.00 आता है; हर worker के अपने account के साथ दोनों tests pass होते हैं।
  • एक shared account

    • Worker 1 test "cart total"
    • Test environment सभी tests के लिए एक cart
    • Worker 2 test "empty cart"
    1. 1 Worker 1 से Test environment तक: 150.00 और 80.00 के items जोड़ें qa.shared@example.com
    2. 2 Worker 2 से Test environment तक: उसी पल cart खाली करें Account वही, इसलिए cart भी वही qa.shared@example.com
    3. 3 Worker 2 आंतरिक रूप से: Assert: cart खाली है पास
    4. 4 Worker 1 से Test environment तक: Cart खोलकर total पढ़ें
    5. 5 Test environment से Worker 1 तक: Total 0.00 दिखाता है जवाब
    6. 6 Worker 1 आंतरिक रूप से: Assert: total 230.00 है फ़ेल

    परिणाम: सिर्फ़ तब fail होता है जब दोनों tests समय में टकराते हैं; अकेले और एक worker के साथ हर एक pass होता है। फ़ेल

  • हर worker का अपना नया account

    • Worker 1 test "cart total"
    • Test environment हर worker का अलग account और cart
    • Worker 2 test "empty cart"
    1. 1 Worker 1 से Test environment तक: 150.00 और 80.00 के items जोड़ें qa+3f9a…@example.com
    2. 2 Worker 2 से Test environment तक: उसी पल अपना cart खाली करें अलग account, इसलिए अलग cart qa+c41e…@example.com
    3. 3 Worker 2 आंतरिक रूप से: Assert: cart खाली है पास
    4. 4 Worker 1 से Test environment तक: Cart खोलकर total पढ़ें
    5. 5 Test environment से Worker 1 तक: Total 230.00 दिखाता है जवाब
    6. 6 Worker 1 आंतरिक रूप से: Assert: total 230.00 है पास

    परिणाम: Timing वही, carts अलग: दोनों tests pass होते हैं, उनके steps चाहे जैसे भी आपस में मिलें। पास

  • हर worker worker-scoped fixture में अपना नया account बनाता है, email में UUID के साथ, ताकि shards भी न टकराएँ।
  • एक worker के tests उस account को फिर भी एक के बाद एक share करते हैं, इसलिए beforeEach hook हर test से पहले cart खाली करता है, पिछले test पर भरोसा करने की बजाय।
  • "Order history" API से अपने तीन orders बनाता है और उनकी ids पर assert करता है, कुल गिनती पर नहीं।
  • Invoice test.info().outputPath() में save होती है, जो हर test का अपना folder है।
  • Fix की पुष्टि चार workers पर --repeat-each=10 से होती है, क्योंकि एक green run बहुत कम साबित करता है।
// 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'));
});

आज़माएँ: फ़ेक यूज़र जनरेटर

आम गलतियाँ

  • Tests क्या share करते हैं यह पहले जाँचे बिना parallel execution चालू करना, फिर उजागर हुए failures के लिए runner को दोष देना।
  • Order dependency को क्रम थोपकर ठीक करना, serial mode या numbered file names से, बजाय इसके कि हर test अपनी state खुद बनाए।
  • Worker index को नाम का अकेला unique हिस्सा बनाना, जिससे हर shard का worker 0 एक ही account इस्तेमाल करता है।
  • Shared environment में "list में तीन orders हैं" जैसी global गिनती पर assert करना, बजाय test के बनाए records पर।
  • Sharded run को सभी shards की merged report की बजाय एक shard के result से आँकना।
  • टकराव से हुए failures को pass होने तक retry करना, जो shared state को हटाने की बजाय छिपा देता है।

QA का नज़रिया

Isolation ऐसी चीज़ है जिसे tester जाँच सकता है और माँग सकता है। Suite को parallel करने से पहले, और जब भी आप कोई automated test review करें, चार checks करें: क्या test अकेले, shuffled क्रम में, लगातार दो बार और कई workers के साथ pass होता है? Jest में --randomize है, pytest-randomly plugin pytest runs का क्रम बदलता है, और Playwright में --repeat-each है। फिर पूछें कि हर test क्या बदलता है और वह बदलाव और कौन देख सकता है। कई fixes के लिए developers या platform team चाहिए: test accounts बनाने का API, cart reset करने का तरीका, हर run के लिए अलग tenant, और ऐसा environment जो चार workers का load झेल सके।

जब कोई test सिर्फ़ parallel में fail हो, तो वह evidence दर्ज करें जो टकराव को product defect से अलग करता है: वह किस worker और shard पर चला, उसने कौन-सा account और कौन-सी record ids इस्तेमाल कीं, उसी समय और क्या चल रहा था, और क्या वह अकेले और एक worker के साथ pass होता है। पुष्टि हुआ टकराव test suite का defect है जिसका owner और fix होना चाहिए, दोबारा चलाने लायक noise नहीं।

अभ्यास

Settings suite को parallel के लिए सुरक्षित बनाएँ

आपका काम

नीचे दिया suite एक worker के साथ काम करता है। Team इसे चार workers और दो shards के साथ चलाना चाहती है। हर test के लिए बताएँ कि वह दूसरे tests के साथ क्या share करता है, आप उसे कैसे isolate करेंगे, और कौन-से tests जैसे हैं वैसे रह सकते हैं और किन्हें अलग चलना होगा। फिर लिखें कि दो 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
चरण
  1. हर test के लिए लिखें कि वह क्या पढ़ता है और क्या बदलता है।
  2. हर उस बदलाव को चिह्नित करें जिसे कोई दूसरा test भी पढ़ता या बदलता है।
  3. हर एक के लिए fix चुनें: अपना data, अपनी file, per-worker resource, या अलग serial run।
  4. जाँचें कि दो shards क्या जोड़ते हैं, यह ध्यान में रखते हुए कि दोनों एक ही staging environment इस्तेमाल करते हैं।
मॉडल उत्तर दिखाएँ
मॉडल उत्तर

Tests 1 और 2: test 2 इस पर निर्भर है कि test 1 उसी account पर पहले चल चुका हो। fullyParallel के साथ वह test 1 से पहले या किसी दूसरे worker पर चल सकता है। Fix: हर test को fixture से नया account मिले, और test 2 header जाँचने से पहले API से नाम खुद set करे।

Test 3: shared account पर two-factor sign-in चालू करना बाक़ी सभी tests का sign-in तोड़ देता है, सिर्फ़ पढ़ने वाले test 6 का भी। उसे अपना नया account चाहिए, जिसे वह बाद में delete कर सके।

Test 4: दो workers एक ही समय downloads/export.zip लिख सकते हैं। इसकी बजाय test.info().outputPath('export.zip') में save करें।

Test 5: site-wide setting को account के हिसाब से isolate नहीं किया जा सकता। उसे --workers=1 वाले अलग run में ले जाएँ जो एक बार चले, shard jobs से पहले या बाद में, उनके साथ-साथ कभी नहीं, क्योंकि सब एक ही staging environment इस्तेमाल करते हैं। वह test शुरुआत में अपनी अपेक्षित theme set करे और teardown में default theme वापस लाए। Test 6 सिर्फ़ shared reference data पढ़ता है और अपने या read-only account से sign in करने के बाद जैसा है वैसा रह सकता है।

Shards: दोनों shards एक ही staging environment इस्तेमाल करते हैं, इसलिए account names सभी shards में unique होने चाहिए, जैसे UUID से, क्योंकि अकेला worker index दोहराया जाता है। Run पर फ़ैसला करने से पहले दोनों shard reports को एक में merge करें, और चार workers पर कई repeated runs से fixes की पुष्टि करें।

अक्सर पूछे जाने वाले सवाल

क्या parallel execution tests को flaky बना देता है?

ज़्यादातर यह उन समस्याओं को उजागर करता है जो पहले से थीं: shared state और order dependencies, जिन्हें तय क्रम और timing ने छिपा रखा था। यह एक असली कारण जोड़ सकता है, load: चार workers एक छोटे test environment को इतना धीमा कर सकते हैं कि waits का timeout हो जाए। अगर कम workers पर failures गायब हो जाएँ और fail हुए test का data किसी दूसरे test ने नहीं छुआ, तो shared state की बजाय load पर शक करें।

Workers और shards में क्या अंतर है?

Workers एक मशीन पर parallel processes हैं (JUnit 5 में threads); shards suite के हिस्से हैं जिन्हें अलग मशीनें या CI jobs चलाते हैं। दोनों साथ इस्तेमाल होते हैं: चार shards, हर एक में चार workers, एक साथ सोलह tests तक चलाते हैं। Workers एक मशीन का CPU और disk share करते हैं; shards सिर्फ़ test environment share करते हैं और उनकी reports merge करनी पड़ती हैं।

क्या हर test का अपना account होना चाहिए?

जब accounts बनाना सस्ता हो, तो हर test के लिए नया account सबसे सुरक्षित विकल्प है। हर worker के लिए एक account आम समझौता है: एक worker के tests एक के बाद एक चलते हैं, इसलिए यह तब काम करता है जब हर test अपनी शुरुआत में वह reset करे जो वह बदलता है। Read-only data, जैसे catalogue जिसे कोई test edit नहीं करता, सब tests share कर सकते हैं।

संदर्भ

  • Eradicating Non-Determinism in Tests (Martin Fowler)martinfowler.com · 2011flaky टेस्ट सूट का मूल्य क्यों नष्ट करते हैं, उनके आम कारण (isolation, asynchrony, remote services, समय, resource leak) और जवाब के रूप में quarantine।
  • Playwright DocumentationMicrosoft (Playwright)Playwright का आधिकारिक दस्तावेज़ — Chromium, WebKit और Firefox चलाने वाला end-to-end टेस्ट फ़्रेमवर्क।
  • Best Practices — PlaywrightMicrosoft (Playwright)यूज़र को दिखने वाला व्यवहार टेस्ट करें, टेस्ट अलग-थलग रखें, मज़बूत locator और web-first assertions अपनाएँ, और trace से डिबग करें।
  • Best Practices — CypressCypress.ioसमर्पित एट्रिब्यूट से एलिमेंट चुनने, फ़िक्स्ड wait से बचने और टेस्ट को स्वतंत्र रखने पर Cypress की गाइडेंस।

Mutqan tools से अभ्यास करें

UUID v4 (रैंडम) और v7 (समय-क्रमित) पहचानकर्ता थोक में बनाएँ और मौजूदा UUID वैलिडेट करें।

यूटिलिटीज़

सुरक्षित रिज़र्व्ड डोमेन (example.com, test) और plus-addressing वैरिएंट के साथ टेस्ट ईमेल पते जनरेट करें।

टेस्ट डेटा

टेस्ट अकाउंट के लिए फ़ेक यूज़र रिकॉर्ड (नाम, username, ईमेल, पासवर्ड, avatar seed, role) जनरेट करें।

टेस्ट डेटा

सिंथेटिक टेस्ट डेटा की एक ही जगह पर फ़ैक्टरी: users, customers, companies, पते, संख्याएँ, तारीख़ें और स्ट्रिंग — JSON, CSV, SQL या XML में।

टेस्ट डेटा

और गहराई से

इस topic में आपकी प्रगति

QA रोडमैप का हिस्सा