Cypress Basics (Part 2): Writing Your First Tests
Once Cypress is installed and you've run a simple test, the next step is to write tests that interact with your app: find elements, click, type, and assert. This post covers selectors, the main commands you'll use every day, and how assertions work so your tests stay clear and stable.
Finding elements: selectors
Cypress uses the same selectors you know from the browser: CSS selectors, text, and data attributes. Prefer stable selectors so tests don't break when you change styling or copy.
cy.get() with CSS
cy.get('.submit-button')
cy.get('#login-form')
cy.get('[data-testid="user-menu"]')
Use data attributes (e.g. data-testid) when you can: they're meant for tests and change less often than classes or IDs.
cy.contains() for text
cy.contains('button', 'Submit')
cy.contains('Sign in')
cy.contains(selector, text) finds an element that matches the selector and has the given text. With one argument, Cypress finds any element containing that text. Prefer pairing a selector with text to avoid matching the wrong element.
Best practices for selectors
- Prefer
data-testid,data-cy, or similar over class names that are tied to styling. - Use
cy.contains(selector, text)to narrow by role and label (e.g.cy.contains('button', 'Save')). - Avoid brittle XPath or deeply nested CSS (e.g.
div > div > span) when a simple attribute or text will do.
Core commands
cy.visit()
Load a URL. Use it once at the start of a test (or in a beforeEach) for the page you're testing.
cy.visit('/')
cy.visit('https://example.com/login')
If you use a baseUrl in cypress.config.js, pass a path and Cypress will prepend it.
cy.get() and chaining
cy.get() returns a chainable that represents one or more elements. You can chain commands and assertions:
cy.get('[data-testid="email"]').type('user@example.com')
cy.get('[data-testid="password"]').type('secret')
cy.get('button[type="submit"]').click()
Cypress automatically waits for the element to exist and be actionable before typing or clicking, which reduces flakiness.
cy.click()
Clicks the element. Works on buttons, links, or any clickable element.
cy.get('button').click()
cy.contains('a', 'Dashboard').click()
cy.type() and cy.clear()
Type into inputs and clear them:
cy.get('#username').type('alice')
cy.get('#username').clear().type('bob')
Use { enter } to press Enter, useful for search or login:
cy.get('#search').type('cypress docs{ enter }')
Assertions with .should()
Cypress uses Chai-style assertions via .should(). They retry until they pass or time out, so you rarely need manual waits.
Common assertions
cy.get('h1').should('be.visible')
cy.get('[data-status]').should('have.attr', 'data-status', 'success')
cy.get('.count').should('contain', '42')
cy.get('form').should('have.length', 1)
You can chain multiple assertions:
cy.get('[data-testid="user-name"]')
.should('be.visible')
.and('contain', 'Alice')
Useful patterns
- Visibility:
should('be.visible'),should('not.exist')(after removal). - Content:
should('contain', 'text'),should('have.text', 'exact'). - State:
should('be.disabled'),should('have.class', 'active'). - Count:
should('have.length', 3).
Example: login flow
Putting it together, a minimal login test might look like:
describe('Login', () => {
beforeEach(() => {
cy.visit('/login')
})
it('logs in with valid credentials', () => {
cy.get('[data-testid="email"]').type('user@example.com')
cy.get('[data-testid="password"]').type('password123')
cy.get('button[type="submit"]').click()
cy.url().should('include', '/dashboard')
cy.contains('Welcome').should('be.visible')
})
})
beforeEach visits the login page; the test fills the form, clicks submit, then asserts the URL and a welcome message. Cypress waits for each step automatically.
Summary
- Use stable selectors: prefer
data-testidordata-cy, andcy.contains(selector, text)for buttons and links. - Core commands:
cy.visit(),cy.get(),cy.click(),cy.type(),cy.clear(); chain them and add.should()for assertions. - Assertions retry automatically; use
be.visible,contain,have.attr,have.length, etc., to keep tests readable and reliable.
In the next part we'll cover best practices: organizing specs, debugging, running in CI, and common anti-patterns to avoid.