Cypress Anti-Patterns to Avoid
Learn why Cypress tests become flaky and how to fix brittle selectors, fixed waits, hidden state, and other common anti-patterns.
A Cypress test that passes only after another test, breaks when a CSS class changes, or needs a three-second sleep is often showing an anti-pattern: the test does not express its real prerequisites or wait condition clearly. Prefer independent tests, selectors that reflect test intent, retryable assertions, and deliberate application state.
Cypress describes some of these practices as anti-patterns and others as best practices. The distinction matters: for example, a data attribute is often a durable selector, but user-visible text can be the right choice when the text itself is what the test should verify.
1. Tests that depend on earlier tests
Each test should pass by itself and in any order. A test that succeeds only because a previous test logged in, created data, or left the browser on a particular page has hidden setup. It may fail when run alone, reordered, retried, or selected with .only().
Cypress says end-to-end test isolation is enabled by default. Before each test, it resets the page to about:blank, cookies across domains, localStorage, and sessionStorage. IndexedDB and other browser storage mechanisms are not cleared by that list, so set up and clean up any state your application stores there when it matters.
describe('account settings', () => {
beforeEach(() => {
cy.loginByApi();
cy.visit('/account/settings');
});
it('shows the current email address', () => {
cy.get('[data-cy="account-email"]').should('have.value', 'dev@example.test');
});
it('lets the user update notification preferences', () => {
cy.get('[data-cy="weekly-digest"]').check();
cy.get('[data-cy="save-settings"]').click();
cy.get('[data-cy="save-status"]').should('contain', 'Saved');
});
});
cy.loginByApi() is an example project helper, not a built-in Cypress command. Implement it using your application’s supported test or authentication setup. Keep shared hooks focused on prerequisites; one test should not be responsible for making another test runnable.
2. Disabling test isolation as a blanket speed fix
Setting testIsolation: false for end-to-end tests can retain browser state and may help a particular suite, but it also lets tests affect one another. It is not a general-purpose fix for a slow suite. First make setup explicit and verify that tests pass alone. Use retained state only when the suite’s design justifies the trade-off and the state dependencies are controlled.
// cypress.config.js
const { defineConfig } = require('cypress');
module.exports = defineConfig({
e2e: {
baseUrl: 'http://localhost:3000',
testIsolation: true,
},
});
Isolation behavior also affects cy.session(). With isolation enabled, visit the app after setting up or restoring a session when the test needs an application page. Component tests reset the rendered component and the documented cookie and storage categories; Cypress does not let component testing configure the isolation behavior in the same way.
3. Selectors coupled to styling or implementation
A selector such as .blue-button encodes a styling decision. A selector based on a deep DOM structure encodes implementation details. Either can break during a visual or structural refactor even if the user behavior is unchanged.
// Fragile when styling or markup changes
cy.get('.btn.btn-primary:nth-child(2)').click();
// Stable when this attribute is reserved for test targeting
cy.get('[data-cy="submit-order"]').click();
Cypress recommends purpose-built data-* selectors when appropriate. Use a selector that expresses the behavior under test when that is the point of the test: for example, a role and accessible name can make sense when checking that a user can find and activate a named button. The goal is deliberate coupling, not a rule that every test must use the same selector type.
4. Fixed waits and timing guesses
cy.wait(3000) waits three seconds whether the page is ready in 100 milliseconds or still loading after three seconds. It adds delay without proving that the condition your test needs is true. Cypress recommends retryable queries and assertions for UI state, and aliased requests when a particular network response matters.
// Avoid guessing how long the request takes
cy.wait(3000);
cy.get('[data-cy="results"]').should('be.visible');
// Wait for the specific request and then assert the rendered result
cy.intercept('GET', '/api/search*').as('search');
cy.get('[data-cy="search-input"]').type('invoice{enter}');
cy.wait('@search').its('response.statusCode').should('eq', 200);
cy.get('[data-cy="results"]').should('contain', 'invoice');
Register the intercept before the action that triggers the request. If the test is checking a UI condition rather than a request, the assertion is usually the meaningful synchronization point:
cy.get('[data-cy="loading"]').should('not.exist');
cy.get('[data-cy="results"]').should('be.visible');
cy.visit() resolves when the page’s load event fires, and cy.request() resolves when its response arrives. An extra sleep after either is generally unnecessary. For client-rendered content, assert on the specific element or state the test needs.
5. UI login for every test and uncontrolled external sites
Repeatedly typing credentials through the UI can make setup slower and couple many tests to the login screen. Cypress recommends programmatic login where appropriate. Choose a setup that matches the authentication system and environment; use a supported test endpoint, API, or session helper rather than assuming one recipe works everywhere.
Tests that visit a third-party site your team does not control depend on that site’s availability, content, and behavior. Keep the test focused on your application. When appropriate, exercise the third-party integration through its API using cy.request(), or use a controlled test double for the external boundary.
6. Page objects that hide test intent
Cypress discourages sharing page objects as a default organizational pattern and recommends organizing tests around features and user flows. A broad page-object layer can make it harder to see what a test does or which state it requires. This is Cypress guidance, not a claim that every abstraction is harmful: small helpers can still reduce genuine duplication when they keep behavior and prerequisites clear.
// A flow-oriented test makes the behavior visible
it('submits an order and shows its confirmation', () => {
cy.visit('/checkout');
cy.get('[data-cy="address-line-1"]').type('10 Market Street');
cy.get('[data-cy="place-order"]').click();
cy.get('[data-cy="order-confirmation"]').should('be.visible');
});
7. End-to-end tests with only one assertion
A user flow often has several related outcomes worth checking. Cypress identifies a single-assertion-only end-to-end approach as an anti-pattern and says not to worry about adding multiple assertions. Keep assertions tied to the same behavior, though; do not combine unrelated scenarios just to reduce the test count.
it('saves a profile change and confirms the result', () => {
cy.visit('/profile');
cy.get('[data-cy="display-name"]').clear().type('Sam Lee');
cy.get('[data-cy="save-profile"]').click();
cy.get('[data-cy="save-status"]').should('contain', 'Saved');
cy.get('[data-cy="profile-heading"]').should('contain', 'Sam Lee');
});
8. Hard-coded secrets in test files
Do not put real credentials, tokens, or other secrets in test source or expose sensitive values to browser-side test code. Store secrets with the secret-handling mechanism appropriate to your CI and local environment, and limit credentials to the permissions the test needs. A test-only environment does not make a committed secret safe.
9. Repeating full URLs instead of configuring baseUrl
Cypress lists calling cy.visit() without a configured baseUrl as an anti-pattern. A base URL avoids repeating an environment-specific origin and makes switching environments easier.
// cypress.config.js
const { defineConfig } = require('cypress');
module.exports = defineConfig({
e2e: {
baseUrl: 'http://localhost:3000',
},
});
// In a spec
cy.visit('/account');
Configure the URL for the environment running the suite. Avoid baking a production origin into a test that is intended to run against a local or staging application.
10. Starting Cypress before the app is ready
Starting cypress run at the same time as the development server does not guarantee the server has booted when Cypress begins. A guessed shell sleep has the same weakness as a fixed wait in a browser test. Use a readiness check or a CI action that waits for the server before launching the test command.
A practical review checklist
- Can each test pass alone, in a different order, and when retried?
- Does each test establish the data and authentication state it needs?
- Do selectors target test intent or user-visible meaning rather than incidental styling?
- Does every wait express a condition, such as an assertion or aliased request?
- Are external services controlled or isolated from tests of your own application?
- Do hooks and helpers clarify shared setup without hiding prerequisites?
- Are secrets kept out of test source and browser context?
- Does CI confirm the application is ready before Cypress runs?
Troubleshooting common Cypress failures
| Symptom | Likely cause | What to change |
|---|---|---|
| Passes in the full suite but fails alone | A previous test or shared browser state supplies an unstated prerequisite. | Run the test alone or with .only(); move setup into that test or a focused hook. |
| Fails after a CSS or markup change | The selector depends on styling or DOM structure that was not part of the intended behavior. | Use a purpose-built data-* attribute or an appropriate user-facing semantic selector. |
| Still flaky after adding a sleep | The guessed duration does not identify the UI condition or request completion. | Use a retryable assertion or intercept and wait for the specific request. |
cy.wait('@search') times out |
The intercept may be registered too late, its method or URL may not match, or the action may not send a request. | Register the intercept before the action; check the request method and matcher against the actual call. |
| App state leaks between tests | Isolation is disabled or state lives in storage not covered by the default reset. | Prefer default isolation; explicitly clear or seed additional storage your app uses. |
| Suite is slow after login | Each test may be repeating UI setup unnecessarily. | Consider programmatic login and Cypress session support where appropriate; preserve each test’s independence. |
| CI reports connection refused at startup | Cypress started before the application server was ready. | Add a readiness check or CI wait-for-server step instead of a guessed delay. |
| Tests fail against an unexpected host | URLs are duplicated in specs or environment configuration is inconsistent. | Set baseUrl and provide the correct environment origin to the run. |
Performance, reliability, and cost of fixes
Removing arbitrary waits can reduce wasted test time, but reliability comes from waiting for the condition that matters, not from simply making a test faster. Retryable assertions and request aliases make the synchronization intent visible. Programmatic setup can avoid repeating a long UI login path, while test isolation protects independence. Measure suite changes in your own CI; Cypress guidance does not establish a universal speedup.
Retaining state with isolation disabled may reduce setup work in some suites, but increases the risk that order or prior state affects outcomes. Treat it as a scoped trade-off and confirm tests pass individually. Do not estimate savings or flaky-test reductions without measurements from your own runs.
Or skip the browser setup
If your Cypress workflow needs a website screenshot for a visual record, bug report, or documentation artifact, ScreenshotNeo can return one from a single API request. See the ScreenshotNeo API documentation.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
ScreenshotNeo accepts cookie and consent banners as a visitor and removes more than 60 known consent platforms, newsletter popups, and chat widgets before capture; each cleanup step can be turned off. Bot checks and CAPTCHAs, blank pages, timeouts, failed loads, and cache hits cost nothing, and response headers say which page verdict and billing outcome applied. Its MCP server gives AI agents tools for screenshots, page information, and PDF capture. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000 shots. Every feature is on every plan. Learn more at ScreenshotNeo, or sign up free for 1,000 screenshots a month with no card.
FAQ
Does test isolation clear every kind of browser storage?
No. Cypress documents resets for cookies, localStorage, and sessionStorage; IndexedDB and other storage mechanisms are not included in that list.
Should every Cypress test use a data-cy selector?
No. Use one when it is the clearest durable test hook. A semantic or text-based selector can be more appropriate when the test is verifying what a user can perceive or access.
Can one end-to-end test make several assertions?
Yes. Related assertions about one user behavior are appropriate. Keep unrelated flows separate so a failure has a clear meaning.
Where can I learn Cypress from Cypress?
Cypress offers free official courses and examples through Real World Testing with Cypress.


