June 2nd, 2024 according to npmtrends. Until early 2024, Cypress was the most downloaded e2e framework. Since then, steep decline in Cypress popularity while Playwright became most downloaded, marking significant shift in testing framework preferences. By 2025, Playwright maintains dominant position with 23 releases in 2024 alone (nearly 2 updates/month), showing strong Microsoft backing and active development. Playwright's cross-browser support, multi-language APIs, and native parallel execution drove adoption.
Modern Testing FAQ & Answers
30 expert Modern Testing answers researched from official documentation. Every answer cites authoritative sources you can verify.
unknown
30 questionsCross-browser (Chromium, Firefox, WebKit vs Cypress's Chromium-only), multi-language (JS/TS/Python/C#/Java vs JS/TS only), native parallel execution without paid plans, advanced network interception, multi-context testing for simultaneous sessions. Playwright 1.45+ auto-waits for 6 actionability checks (attached, visible, stable, enabled, editable, receives events) eliminating flaky timeouts. Example: await page.locator('button').click() auto-waits vs Cypress needs manual waits. Playwright's out-of-process architecture enables multi-user scenarios and headless cloud execution.
getBy: synchronously returns element or throws error if not found, for immediately present elements. queryBy: synchronously returns element or null (no error), for asserting non-existence. findBy: returns Promise, async waits up to 1000ms (configurable), for elements appearing after events/API calls. Example: screen.getByRole('button') for immediate elements, screen.queryByText('Loading...') to verify loading finished, await screen.findByText('Success') after async action. Use screen object (pre-bound to document.body) per Kent C. Dodds 2024+ recommendations for better autocomplete and maintainability.
Vitest 2-3x faster in watch mode, only reruns affected tests with Vite HMR. Cold runs show 4x improvement with proper config: 50K-line codebase uses 800 MB vs Jest's 1.2 GB. Example benchmark: 25 suites/100 tests - Jest 15.5s, Vitest 3.8s. Vitest 3 (Jan 2025) adds suite-level shuffling for better parallelism. Full ESM support out-of-box vs Jest's experimental ESM requiring extra setup. Config crucial: use HappyDOM over JSDOM, enable threads. Example: vitest.config.ts with { test: { environment: 'happy-dom', threads: true } } for optimal performance.
Playwright auto-waits for 6 actionability checks before actions: 1) Attached to DOM, 2) Visible (has bounding box, not CSS hidden), 3) Stable (not animating), 4) Enabled (not disabled attribute), 5) Editable (for inputs, not readonly), 6) Receives events (not obscured by other elements). Applies to click(), fill(), expect() operations. Fails with TimeoutError if checks don't pass within timeout (default 30s). Example: await page.locator('.submit').click() waits until button is visible, stable, and enabled. Eliminates arbitrary waits like await page.waitForTimeout(1000) which create flaky tests.
Spies and stubs network requests/responses. Next-gen successor to deprecated cy.route() with more flexibility. Supports fetch, page loads, resource loads (images, stylesheets), XHR. Features URL matching, HTTP method filtering, regex, response manipulation. Example: cy.intercept('POST', '/api/users', { statusCode: 201, body: { id: 1 } }).as('createUser') then cy.wait('@createUser'). Introduced in Cypress 6.0, enhanced in Cypress 13+ with better asset response capturing for Test Replay. Use for API mocking, request verification, response stubbing. Note: only works client-side, cannot intercept SSR calls in Next.js getServerSideProps.
jest.fn() creates standalone mock function from scratch, replaces entire implementation, doesn't call original. jest.spyOn() wraps existing object method, tracks calls, calls original by default unless mockImplementation() used. Only spyOn supports mockRestore() to restore original. Example: const mock = jest.fn(() => 42) creates new mock, while jest.spyOn(obj, 'method').mockReturnValue(42) wraps existing obj.method. Use jest.fn() for callbacks and dependencies, jest.spyOn() for monitoring real methods. Compatible with Jest 29+ and Vitest (vi.fn(), vi.spyOn() with same API).
Always use screen (Kent C. Dodds recommendation 2024+). Benefits: no destructuring updates when render changes, better IDE autocomplete, simpler maintenance, pre-bound to document.body. Example: screen.getByRole('button') instead of const { getByRole } = render(<App />); getByRole('button'). Only use container for rare edge cases: rerender(), baseElement customization, or legacy codebases. ESLint rule testing-library/prefer-screen-queries enforces this pattern. Screen works with all query types: getBy, queryBy, findBy, getAllBy. Reduces boilerplate and prevents common destructuring mistakes.
Use explicit waits when auto-wait insufficient: waiting for loader to disappear, API call completion, background process, data loading while element remains visible, specific network response. Methods: await page.locator('.spinner').waitFor({ state: 'hidden' }) for element disappearance, await page.waitForResponse(res => res.url().includes('/api/data') && res.status() === 200) for API calls. Never use page.waitForTimeout(1000) in production - unreliable, slows tests, creates flakiness. Example: await page.locator('.loading').waitFor({ state: 'detached' }) then await page.locator('.content').click() ensures loading finished.
Superior developer experience with GUI Test Runner featuring time-travel debugging - hover over commands to see DOM snapshots at each step, enabling real-time issue spotting. Simpler learning curve with strong community (est. 2014 vs Playwright 2020). In-browser architecture provides native DOM/network access ideal for frontend debugging. Component testing built-in for React/Vue/Angular. Example: Cypress GUI shows network XHRs, command log, and DOM state visually vs Playwright's CLI-first approach. Fewer lines of code for same functionality due to automatic retries and waiters. Best for teams prioritizing DX over cross-browser coverage.
1000ms (1 second) default timeout, configurable via timeout option: await screen.findByText('Loaded', { timeout: 3000 }). findBy queries return Promise that resolves when element found or rejects after timeout with error. Internally uses waitFor with retry interval ~50ms. Useful for async elements appearing after button clicks, API calls, state updates. Example: await screen.findByRole('alert') after userEvent.click(submitButton). For global timeout config, set in test setup: configure({ asyncUtilTimeout: 2000 }). Prefer findBy over getBy + waitFor for cleaner async tests. Works with all query variants: findByRole, findByText, findByLabelText.
No, cy.intercept() only intercepts client-side browser requests (fetch, XHR, resource loads). Cannot intercept server-side Node.js calls in Next.js getServerSideProps(), getStaticProps(), API routes, or similar SSR methods running in Node runtime. For server-side mocking, use tools like nock, msw/node, or Mock Service Worker in Node.js mode. Example: client-side cy.intercept('GET', '/api/data') works, but SSR data fetching bypasses browser network stack. Workaround: test SSR pages after hydration when client-side fetches occur, or use Playwright with route.fulfill() for both client and service worker interception.
Replaces implementation of mock or spy with custom function. Works with jest.fn() or jest.spyOn(). mockImplementationOnce() overrides only first call, subsequent calls use original mock or next mockImplementationOnce. Example: const mock = jest.fn().mockImplementation(() => 42) or jest.spyOn(obj, 'method').mockImplementation(() => 'mocked'). Chain multiple: mock.mockImplementationOnce(() => 1).mockImplementationOnce(() => 2) for different return values per call. Allows testing error paths, edge cases, conditional logic. Also available in Vitest as vi.mockImplementation(). Combine with mockReturnValue() for simple values or mockResolvedValue() for Promises.
Playwright supports Chromium (Chrome, Edge), Firefox, and WebKit (Safari engine) across Windows, macOS, Linux. Cypress primarily supports Chromium-based browsers (Chrome, Edge, Electron), with limited Firefox support (beta) and no WebKit/Safari. This makes Playwright essential for cross-browser compatibility testing including Safari. Example: playwright.config.ts with projects: [{ name: 'chromium' }, { name: 'firefox' }, { name: 'webkit' }] runs tests on all engines. Playwright 1.45+ maintains WebKit updates for macOS 12+ ensuring latest Safari features. Critical for teams needing full browser coverage.
getBy throws error when element not found, causing test failure even if non-existence is expected behavior. Use queryBy instead - returns null for non-existent elements, allowing assertion expect(screen.queryByText('Error')).toBeNull(). Example: after successful form submit, verify error message gone: expect(screen.queryByRole('alert')).not.toBeInTheDocument(). Using getBy would fail test with "Unable to find" error. getBy designed for positive assertions of existence, queryBy for negative assertions. ESLint rule testing-library/prefer-presence-queries helps catch this. Also applies to getAllBy (throws if 0 results) vs queryAllBy (returns empty array).
Vitest 3 (Jan 2025) introduces suite-level test shuffling for better concurrent execution and improved parallelism. Combined with Vite's HMR (Hot Module Replacement), enables dramatically faster watch mode - only reruns affected tests, not entire files. Better than Jest's file-level rerunning. Example config: vitest.config.ts with { test: { sequence: { shuffle: true }, poolOptions: { threads: { singleThread: false } } } } maximizes parallelism. Additional improvements: better browser mode, workspace support, enhanced coverage. Benchmark: Fortune 500 e-commerce reduced regression testing from 4 hours to 1 hour with Vitest 3 + proper config.
Six checks must pass: 1) Attached to DOM (element exists in document), 2) Visible (has bounding box, no CSS display:none/visibility:hidden), 3) Stable (not animating, position unchanged for 2 consecutive animation frames), 4) Enabled (no disabled attribute), 5) Editable (for inputs, not readonly), 6) Receives events (not obscured by other elements, z-index check). All checks auto-retry until timeout (default 30s). Example: await page.locator('button').click() waits for all 6 checks. Fails with "Element is not visible" or "Element is outside of the viewport" if checks don't pass. Configurable: { timeout: 10000, force: true } to override.
No native parallel execution without Cypress Cloud (paid). Free tier requires workarounds: community tools like cypress-parallel (reduces runtime up to 40% on same machine), CI-level parallelization with manual test splitting, or split specs across multiple CI jobs. Example: cypress-parallel -s cypress run -n 4 runs 4 parallel threads locally. In contrast, Playwright natively supports parallel execution, sharding, and orchestration without third-party tools: npx playwright test --workers=4 or dynamic sharding in CI. Playwright's out-of-process architecture enables true parallelism. Cypress 13+ improved but still recommends Cloud for best parallel experience.
Jest design philosophy: spies observe behavior without changing it. Unlike Sinon/Jasmine which stub by default, Jest spyOn() calls original implementation unless mockImplementation() used. This allows monitoring calls while preserving behavior for integration-style tests. Example: const spy = jest.spyOn(db, 'query') tracks calls but still executes real query. To stub: spy.mockImplementation(() => mockData) or spy.mockReturnValue(result). Use mockRestore() to return to original: spy.mockRestore(). Vitest vi.spyOn() follows same pattern. Pattern useful for partial mocking - spy on some methods, mock others.
Prefer queries matching how users interact, prioritized by Testing Library: 1) getByRole (most accessible, includes ARIA roles), 2) getByLabelText (form inputs), 3) getByPlaceholderText, 4) getByText, 5) getByDisplayValue. Avoid getByTestId, querySelector - breaks confidence in user interactions, harder to read, more fragile. Example: screen.getByRole('button', { name: 'Submit' }) instead of screen.getByTestId('submit-btn'). Role queries enforce accessibility: missing ARIA roles fail tests, encouraging better HTML. Query priority reflects real user behavior - screen readers use roles, sighted users see text. ESLint rules enforce: testing-library/prefer-screen-queries, testing-library/no-node-access.
Comprehensive coverage: fetch API calls, XMLHttpRequests (XHR), page loads (navigation), resource loads (images, stylesheets, fonts, scripts). Introduced in Cypress 6.0, enhanced in Cypress 13+ with better asset response capturing. Supports URL matching (string, glob, regex), HTTP method filtering, RouteMatcher objects, with .as() aliasing for cy.wait(). Example: cy.intercept('GET', '/api/**', { fixture: 'data.json' }).as('getData') then cy.wait('@getData'). Also intercepts redirects, CORS preflight, and can modify request headers/body. Unlike cy.route() (deprecated), works with fetch. Note: client-side only, cannot intercept server-side Node.js requests.
JavaScript, TypeScript, Python, C#, and Java with official first-party libraries. Multi-language support major advantage over Cypress (JS/TS only). Enables teams with diverse language expertise to adopt without learning new language. Example: Python playwright.sync_api for pytest integration, C# Playwright.NUnit for .NET projects, Java with JUnit. All languages share same API design - page.locator('button').click() syntax consistent across languages. Community bindings exist for Go, Ruby. TypeScript provides best IDE support with auto-complete. Python popular for data science teams, Java for enterprise QA. Choose language matching your team's expertise.
Performance results vary dramatically with configuration. Reports range from Vitest 4x faster to Jest faster depending on config. Critical settings: environment (HappyDOM 30% faster than JSDOM), threads (enable for parallelism), isolate (false for speed, true for isolation), coverage provider (v8 faster than istanbul). Example optimal config: { test: { environment: 'happy-dom', threads: true, isolate: false, coverage: { provider: 'v8' } } }. ESM vs CommonJS matters - Vitest optimized for ESM, Jest for CommonJS. Pool strategy affects performance: poolOptions.threads.singleThread: false enables parallelism. Benchmark before/after config changes. Config more important than framework choice.
Out-of-process architecture allows automating multiple browser instances, contexts, and pages simultaneously. Can simulate multi-user scenarios, test collaborative features, handle isolated sessions with separate cookies/storage. Example: const context1 = await browser.newContext(); const context2 = await browser.newContext() creates two isolated sessions - test admin and user simultaneously. Each context has independent storage, permissions, geolocation. Useful for testing chat apps, real-time collaboration, session management. Cypress's in-browser architecture limits this - runs inside browser, single context at a time. Playwright also supports parallel test execution across contexts, reducing test runtime.
Only for debugging purposes, never in production tests. Fixed timeouts are unreliable (break in different environments - slow CI, network latency), slow tests unnecessarily, and create flaky tests. Use deterministic waits instead: await page.locator('.data').waitFor() for element appearance, await page.waitForResponse(url) for API calls, or rely on auto-wait for actions. Example: BAD: await page.waitForTimeout(5000); await page.click('button'). GOOD: await page.locator('button').click() (auto-waits for actionability). If debugging, add timeout temporarily then replace with proper wait. Playwright's auto-wait eliminates 90% of timeout needs.
MSW 2.0 intercepts network requests at the Service Worker level (browser) or http/https modules (Node.js), enabling API mocking without changing application code. Works seamlessly with Playwright, Vitest, Jest. Example browser setup: worker.start() then http.get('/api/user', () => HttpResponse.json({ id: 1 }). Node setup for Vitest: server.listen() in setupFiles. MSW + Playwright is the "bulletproof combo for 2025" - test real network conditions without backend. Advantages: mocks at network layer (works with any request library), same handlers for dev and test, supports REST and GraphQL. MSW 2.0 adds TypeScript-first API and improved performance.
Testcontainers provides lightweight, throwaway Docker container instances for integration testing. Start real databases, message queues, or services in tests, then dispose after. Combines with Playwright for full-stack testing. Example: const postgres = await new PostgreSQLContainer().start() creates real Postgres, run migrations, test API with Playwright, then await postgres.stop(). Available for Node.js, Python, Java. Benefits: test against real dependencies (no mocks), isolated environments, consistent across dev/CI. Pattern: Testcontainers + Playwright + MSW = complete testing stack. Fortune 500 teams report 75% reduction in production bugs with this approach.
AAA pattern (Arrange, Act, Assert) structures tests for clarity: 1) Arrange - set up test data and environment, 2) Act - execute the action being tested, 3) Assert - verify expected outcome. Example Vitest test: // Arrange: const user = { name: 'Alice' }; const mock = vi.fn(); // Act: const result = processUser(user, mock); // Assert: expect(result).toBe(true); expect(mock).toHaveBeenCalledWith(user). Benefits: readable, maintainable, single responsibility per test. Separate with blank lines or comments. Avoid multiple Acts in one test - split into separate tests. Also called Given-When-Then in BDD. Works with Jest, Vitest, Pytest, Go testing.
Test isolation ensures tests don't affect each other. Techniques: 1) Fresh environment per test - beforeEach() resets state, 2) Independent data - factories/fixtures generate unique data, 3) Mock cleanup - afterEach() calls mockRestore(), 4) Database transactions - rollback after test. Example Jest: beforeEach(() => { jest.clearAllMocks(); db.resetState(); }). Vitest isolate config: { test: { isolate: true } } runs each test file in separate environment. Playwright provides isolated context per test: test.use({ storageState: undefined }). Testcontainers restarts containers. Benefits: parallel execution, reproducible failures, no order dependencies. Anti-pattern: shared state across tests.
Test coverage measures percentage of code executed by tests. Types: line coverage (lines executed), branch coverage (if/else paths), function coverage, statement coverage. Tools: Istanbul/nyc for Jest, c8/v8 for Vitest, Coverage.py for Python. Example Vitest config: { test: { coverage: { provider: 'v8', reporter: ['text', 'html'], lines: 80, branches: 75 } } }. Good targets: 70-80% overall, 90%+ for critical paths, 50-60% for UI code. Don't chase 100% - diminishing returns. Focus on business logic, ignore boilerplate. Use coverage to find untested code, not as goal. Trend matters more than absolute number.