Writing Your First Cypress Component Test
Writing your first Cypress component test is simpler than you might think: import your component, call mount() with test props, query the rendered DOM with cy.get() or cy.contains(), interact with elements using Cypress commands, and assert on the result. In under 50 lines of code, you can verify that a component renders correctly, handles user events, and updates its state.
Let me walk you through a realistic example inspired by components I've tested in production applications over the past several years. By the end of this article, you'll have a fully working test that you can extend for your own components.
Setting Up Your First Test
Create a simple Button component in your React project:
// src/components/Button.jsx
export default function Button({ label, onClick, disabled = false, variant = 'primary' }) {
return (
<button
onClick={onClick}
disabled={disabled}
className={`btn btn--${variant}`}
data-testid="button-element"
>
{label}
</button>
)
}
This component is intentionally simple: it accepts a label, an onClick handler, a disabled flag, and a variant for styling. This mirrors real-world button components you'll encounter.
Now create a test file in the same directory:
// src/components/Button.cy.js
import { mount } from 'cypress/react'
import Button from './Button'
describe('Button Component', () => {
it('renders with the provided label', () => {
// Mount the component with test props
mount(<Button label="Click me" onClick={() => {}} />)
// Query the button and verify its text
cy.get('[data-testid="button-element"]')
.should('contain', 'Click me')
})
})
Run this test with npx cypress open --component, click on the test file, and you should see the button rendered and the assertion pass.
Testing Props and Conditional Rendering
Component testing is primarily about verifying that different props produce different DOM outputs. Let's expand the Button test to cover variants and disabled states:
// src/components/Button.cy.js
import { mount } from 'cypress/react'
import Button from './Button'
describe('Button Component', () => {
it('renders with the provided label', () => {
mount(<Button label="Click me" onClick={() => {}} />)
cy.get('[data-testid="button-element"]').should('contain', 'Click me')
})
it('applies the variant class correctly', () => {
// Test the secondary variant
mount(<Button label="Cancel" variant="secondary" onClick={() => {}} />)
cy.get('[data-testid="button-element"]')
.should('have.class', 'btn--secondary')
})
it('disables the button when disabled prop is true', () => {
mount(<Button label="Disabled" disabled onClick={() => {}} />)
cy.get('[data-testid="button-element"]')
.should('be.disabled')
})
it('is enabled by default', () => {
mount(<Button label="Enabled" onClick={() => {}} />)
cy.get('[data-testid="button-element"]')
.should('not.be.disabled')
})
})
Notice that we test each prop scenario separately. This is a key principle: one test = one scenario. This makes failures easier to diagnose because a single assertion failure tells you exactly which behavior is broken.
Testing User Interactions and Event Handlers
The true power of component testing is verifying that your component responds correctly to user interactions. Here's how to test click handlers:
it('calls the onClick handler when clicked', () => {
// Create a stub (a fake function that tracks calls)
const handleClick = cy.stub()
mount(<Button label="Click me" onClick={handleClick} />)
// User clicks the button
cy.get('[data-testid="button-element"]').click()
// Verify the handler was called
cy.wrap(handleClick).should('have.been.called')
})
it('calls onClick with correct event object', () => {
const handleClick = cy.stub()
mount(<Button label="Click me" onClick={handleClick} />)
cy.get('[data-testid="button-element"]').click()
// Verify the handler was called once with a MouseEvent
cy.wrap(handleClick).should('have.been.calledOnce')
cy.wrap(handleClick).should('have.been.calledWith', Cypress.sinon.match.object)
})
it('does not call onClick when disabled', () => {
const handleClick = cy.stub()
mount(<Button label="Click me" disabled onClick={handleClick} />)
// Attempt to click the disabled button
cy.get('[data-testid="button-element"]').click({ force: true })
// The click should not trigger the handler
cy.wrap(handleClick).should('not.have.been.called')
})
The key here is cy.stub(), which creates a fake function that records calls. You pass it as the onClick prop, then assert that it was called the expected number of times.
Note: When a button is disabled, Cypress won't click it by default (it mimics real browser behavior). Use { force: true } if you want to simulate that edge case, but your component shouldn't allow it anyway.
Testing Component State and Effects
Let's test a more complex component that has internal state:
// src/components/Counter.jsx
import { useState } from 'react'
export default function Counter({ initialValue = 0, max = 10 }) {
const [count, setCount] = useState(initialValue)
const increment = () => {
if (count < max) setCount(count + 1)
}
const decrement = () => {
setCount(count - 1)
}
return (
<div>
<p>Count: {count}</p>
<button onClick={increment}>Increment</button>
<button onClick={decrement}>Decrement</button>
{count >= max && <p>Max reached!</p>}
</div>
)
}
Here's the component test:
// src/components/Counter.cy.js
import { mount } from 'cypress/react'
import Counter from './Counter'
describe('Counter Component', () => {
it('displays the initial count', () => {
mount(<Counter initialValue={5} />)
cy.contains('Count: 5').should('exist')
})
it('increments count when increment button is clicked', () => {
mount(<Counter initialValue={0} />)
cy.contains('Increment').click()
cy.contains('Count: 1').should('exist')
})
it('decrements count when decrement button is clicked', () => {
mount(<Counter initialValue={5} />)
cy.contains('Decrement').click()
cy.contains('Count: 4').should('exist')
})
it('does not increment beyond max', () => {
mount(<Counter initialValue={9} max={10} />)
cy.contains('Increment').click()
cy.contains('Count: 10').should('exist')
// Try to increment again (should not go to 11)
cy.contains('Increment').click()
cy.contains('Count: 10').should('exist')
})
it('shows "Max reached!" message when count equals max', () => {
mount(<Counter initialValue={10} max={10} />)
cy.contains('Max reached!').should('exist')
})
})
This test verifies:
- Initial state rendering (initialValue prop)
- State updates in response to clicks
- Boundary conditions (max value)
- Conditional rendering (the "Max reached!" message)
Using cy.get() vs. cy.contains()
Cypress provides two main ways to query the DOM:
cy.get(selector)— Find elements by CSS selector, data attribute, or role. Use this for precise targeting.cy.contains(text)— Find elements by visible text content. Use this for user-centric assertions (users see text, not selectors).
Best practice: use data-testid attributes for reliable selectors that won't break when styling changes:
// Reliable: won't break if CSS class changes
cy.get('[data-testid="button-element"]').click()
// Also good: semantic and accessible
cy.get('button').contains('Click me').click()
// User-centric: based on visible text
cy.contains('Click me').click()
Organizing Tests and Cleanup
As your test suite grows, organize tests with beforeEach for setup and shared assertions:
describe('Button Component', () => {
let handleClick
beforeEach(() => {
// Reset before each test
handleClick = cy.stub()
})
it('renders', () => {
mount(<Button label="Test" onClick={handleClick} />)
cy.get('button').should('exist')
})
it('handles click', () => {
mount(<Button label="Test" onClick={handleClick} />)
cy.get('button').click()
cy.wrap(handleClick).should('have.been.called')
})
})
The beforeEach hook runs before each test, ensuring a clean state.
Key Takeaways
- Mount a component with
mount(<Component {...props} />)and query the rendered DOM withcy.get()andcy.contains(). - Test one scenario per test (one behavior, one assertion failure reason) to make debugging faster.
- Use stubs (
cy.stub()) to mock event handlers and verify they're called correctly. - Verify props change the DOM (variant classes, disabled states, conditional rendering).
- Verify state updates in response to user interactions (clicks, form submissions).
- Use
data-testidattributes for reliable, maintainable selectors.
Frequently Asked Questions
How do I test a component that uses hooks like useContext or useReducer?
Mount the component inside a provider or reducer context wrapper. For example, wrap the component in a Context.Provider before mounting, or use a custom mount command (see article 8) that applies providers automatically.
Can I test multiple interactions in a single test?
Yes, and it's common. You can chain Cypress commands: cy.get('button').click() then cy.get('input').type('text') then cy.get('form').submit(). The key is that each step should be testing one logical flow, not multiple independent behaviors.
What's the difference between cy.get() and cy.contains()?
cy.get() uses CSS selectors or attribute selectors. cy.contains() finds elements by visible text. Both are valid; cy.get() is more precise, cy.contains() is more user-centric. Mix them based on context.
How do I test async behavior like useEffect with API calls?
In component tests, you'll stub the API call with Cypress intercept (covered in article 5). Mount the component, which triggers the effect, then assert on the rendered result after Cypress waits for the mocked response.
Should I test CSS styling in component tests?
Test that CSS classes are applied (e.g., .should('have.class', 'btn--secondary')), but not the computed styles themselves. CSS testing is better done with visual regression or E2E tests. Focus component tests on behavior, not pixel-perfect styling.