📚 5 min read
Testing Library Guide
Testing Library is a family of packages that help you test UI components in a way that resembles how users interact with your app. It encourages better testing practices by focusing on testing behavior rather than implementation details.
Key Features
- User-centric testing approach
- Framework agnostic (works with React, Vue, Angular, etc.)
- Built-in accessibility checks
- Powerful querying capabilities
- Async utilities
- Event simulation
- Semantic queries
Getting Started
bash
# For React
npm install --save-dev @testing-library/react
# For Vue
npm install --save-dev @testing-library/vue
# For Angular
npm install --save-dev @testing-library/angular
Basic Test Structure
javascript
import { render, screen, fireEvent } from '@testing-library/react';
import { Button } from './Button';
test('calls onClick handler when clicked', () => {
const handleClick = jest.fn();
render(<Button onClick={handleClick}>Click Me</Button>);
const button = screen.getByText('Click Me');
fireEvent.click(button);
expect(handleClick).toHaveBeenCalledTimes(1);
});
Common Queries
javascript
// By role (preferred)
const button = screen.getByRole('button', { name: 'Submit' });
// By label text
const input = screen.getByLabelText('Username');
// By placeholder
const search = screen.getByPlaceholderText('Search...');
// By text content
const element = screen.getByText('Hello, World');
// By test ID (last resort)
const container = screen.getByTestId('custom-element');
User Events
javascript
import userEvent from '@testing-library/user-event';
test('typing in an input field', async () => {
const user = userEvent.setup();
render(<Input />);
const input = screen.getByRole('textbox');
await user.type(input, 'Hello, World');
expect(input).toHaveValue('Hello, World');
});
Advanced Queries
Priority Order
javascript
// Best to worst query methods:
getByRole('button', { name: 'Submit' }); // 1. Accessible Roles
getByLabelText('Username'); // 2. Labels
getByPlaceholderText('Enter username'); // 3. Placeholder
getByText('Submit'); // 4. Text Content
getByDisplayValue('John'); // 5. Form Values
getByAltText('Profile picture'); // 6. Alt Text
getByTitle('Close'); // 7. Title Attribute
getByTestId('submit-button'); // 8. Test IDs
Query Variants
javascript
// Single Element
getBy... // Throws error if not found or multiple found
queryBy... // Returns null if not found
findBy... // Returns promise, waits for element
// Multiple Elements
getAllBy... // Throws error if none found
queryAllBy... // Returns empty array if none found
findAllBy... // Returns promise, waits for elements
Testing Patterns
Form Testing
javascript
test('form submission', async () => {
const handleSubmit = jest.fn();
render(<LoginForm onSubmit={handleSubmit} />);
await userEvent.type(screen.getByLabelText('Email'), 'user@example.com');
await userEvent.type(screen.getByLabelText('Password'), 'password123');
await userEvent.click(screen.getByRole('button', { name: /submit/i }));
expect(handleSubmit).toHaveBeenCalledWith({
email: 'user@example.com',
password: 'password123',
});
});
Async Operations
javascript
test('loads and displays data', async () => {
render(<UserProfile userId="1" />);
// Wait for loading to finish
expect(await screen.findByText('User Profile')).toBeInTheDocument();
// Verify loaded content
expect(screen.getByText('John Doe')).toBeInTheDocument();
expect(screen.getByText('john@example.com')).toBeInTheDocument();
});
Event Handling
javascript
test('menu toggle', async () => {
render(<Dropdown />);
const button = screen.getByRole('button', { name: /open menu/i });
const menu = screen.getByRole('menu', { hidden: true });
// Initial state
expect(menu).not.toBeVisible();
// Click to open
await userEvent.click(button);
expect(menu).toBeVisible();
// Click to close
await userEvent.click(button);
expect(menu).not.toBeVisible();
});
Framework Integration
React Testing
javascript
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
test('counter increments', async () => {
render(<Counter initialCount={0} />);
const button = screen.getByRole('button', { name: /increment/i });
const count = screen.getByText('Count: 0');
await userEvent.click(button);
expect(screen.getByText('Count: 1')).toBeInTheDocument();
});
Vue Testing
javascript
import { render, fireEvent } from '@testing-library/vue';
test('emits update event', async () => {
const { emitted, getByRole } = render(CustomInput);
const input = getByRole('textbox');
await fireEvent.update(input, 'new value');
expect(emitted()['update:modelValue'][0]).toEqual(['new value']);
});
Angular Testing
typescript
import { render, screen, fireEvent } from '@testing-library/angular';
test('button click', async () => {
await render(ButtonComponent, {
componentProperties: {
label: 'Click Me',
onClick: () => {},
},
});
const button = screen.getByRole('button');
await fireEvent.click(button);
expect(screen.getByText('Clicked!')).toBeInTheDocument();
});
Custom Queries
Creating Custom Queries
javascript
const getByDataCy = (container, id) => getByAttribute('data-cy', id, container);
const queryByDataCy = (container, id) =>
queryByAttribute('data-cy', id, container);
const getAllByDataCy = (container, id) =>
getAllByAttribute('data-cy', id, container);
const queryAllByDataCy = (container, id) =>
queryAllByAttribute('data-cy', id, container);
const findByDataCy = async (container, id) =>
findByAttribute('data-cy', id, container);
const findAllByDataCy = async (container, id) =>
findAllByAttribute('data-cy', id, container);
const customQueries = {
getByDataCy,
queryByDataCy,
getAllByDataCy,
queryAllByDataCy,
findByDataCy,
findAllByDataCy,
};
const customRender = (ui, options) =>
render(ui, { queries: { ...queries, ...customQueries }, ...options });
Best Practices
1. Query Selection
javascript
// ❌ Avoid
getByTestId('submit-button');
getByClassName('submit-btn');
container.querySelector('.submit-btn');
// ✅ Prefer
getByRole('button', { name: /submit/i });
getByLabelText('Submit form');
2. Async Operations
javascript
// ❌ Avoid
await wait(() => {
expect(getByText('Loaded')).toBeInTheDocument();
});
// ✅ Prefer
expect(await findByText('Loaded')).toBeInTheDocument();
3. User Interactions
javascript
// ❌ Avoid
fireEvent.change(input, { target: { value: 'test' } });
// ✅ Prefer
await userEvent.type(input, 'test');
4. Accessibility Testing
javascript
test('form is accessible', async () => {
const { container } = render(<Form />);
// Check for ARIA attributes
expect(screen.getByRole('form')).toHaveAttribute('aria-label');
// Check for proper labels
expect(screen.getByLabelText('Email')).toBeInTheDocument();
// Check focus management
const submitButton = screen.getByRole('button');
submitButton.focus();
expect(submitButton).toHaveFocus();
});
Debugging
Screen Debug
javascript
test('debugging example', () => {
render(<Component />);
// Print current DOM state
screen.debug();
// Print specific element
screen.debug(screen.getByRole('button'));
// Print with depth limit
screen.debug(undefined, 2);
});
Logging Queries
javascript
import { prettyDOM } from '@testing-library/dom';
test('logging example', () => {
const { container } = render(<Component />);
// Log specific element
console.log(prettyDOM(container.querySelector('.element')));
// Log with maxLength
console.log(prettyDOM(container, 10000));
});
Error Messages
javascript
test('error handling', () => {
render(<Component />);
// Will show helpful error message with suggestions
expect(() => screen.getByRole('button', { name: /submit/i })).toThrow();
});