Component Testing
What it is
Section titled “What it is”A component test mounts a single UI component (button, form, card) or a small cluster of them and asserts on what the user sees and does — text, roles, interactions — without booting the whole application, real network or a real browser.
It sits between unit and integration: real rendering, but a scoped surface. In the testing trophy it is part of the wide middle layer.
When to use it / when to avoid it
Section titled “When to use it / when to avoid it”Use it for
- Interactive components: forms, dropdowns, modals, tables
- Conditional rendering, empty/loading/error states
- Accessibility basics: roles, labels, keyboard interaction
Avoid it when
- The component has no behaviour (pure presentational markup) → a visual regression or snapshot is enough
- You need routing, data fetching and several pages → that is end-to-end
Frameworks & tools
Section titled “Frameworks & tools”| Tool | Ecosystem | Notes |
|---|---|---|
| React Testing Library | React | Query by role/text, user-centric |
| Testing Library (Vue/Svelte/Solid/Angular) | Multi | Same guiding principles |
| Storybook | Multi | Develop and catalog components in isolation |
| Storybook Test Runner | Multi | Run stories as tests |
| Chromatic / Percy | Multi | Visual + interaction review of stories |
| Vitest browser mode | Multi | Real browser, in-process |
| Cypress Component Testing | Multi | Component tests in a real browser |
| Playwright CT | React/Vue/Svelte | Experimental component testing |
| Vue Test Utils, Angular TestBed | per-framework | Official mounting helpers |
Quick setup
Section titled “Quick setup”npm install -D vitest @testing-library/react @testing-library/jest-dom jsdomnpm install -D vitest @vue/test-utils @testing-library/vue jsdomnpm install -D vitest @testing-library/svelte jsdomnpx storybook@latest initnpm install -D @storybook/test-runnernpm install -D cypressnpx cypress open --componentExample
Section titled “Example”import { render, screen } from '@testing-library/react';import userEvent from '@testing-library/user-event';import { Counter } from './Counter';
it('increments when the button is clicked', async () => { render(<Counter />); await userEvent.click(screen.getByRole('button', { name: /increment/i })); expect(screen.getByText('Count: 1')).toBeInTheDocument();});import { mount } from '@vue/test-utils';import Counter from './Counter.vue';
it('increments on click', async () => { const wrapper = mount(Counter); await wrapper.get('button').trigger('click'); expect(wrapper.text()).toContain('Count: 1');});export const Increments: Story = { play: async ({ canvasElement }) => { const canvas = within(canvasElement); await userEvent.click(canvas.getByRole('button', { name: /increment/i })); await expect(canvas.getByText('Count: 1')).toBeInTheDocument(); },};Anti-patterns & best practices
Section titled “Anti-patterns & best practices”- Query like a user. Prefer
getByRole,getByLabelTextover test IDs and CSS classes — the queries themselves enforce accessibility. - Assert what the user sees, not component internals or state variables.
- Drive interaction with
userEvent, not by dispatching raw events. - Cover the states: empty, loading, error, disabled.
- Do not snapshot the whole tree. Large snapshots break on every styling change and nobody reads them.
Self-assessment quiz
Section titled “Self-assessment quiz”Self-assessment quiz
Pick one answer per question, then press Check answers.