Testing Material UI Components: From Unit to E2E Tests
Material UI (MUI) is one of the most popular React component libraries, offering a rich set of prebuilt, accessible, and themeable components. However, as your application grows, ensuring that these components behave correctly — both in isolation and within the broader user journey — becomes critical. This tutorial walks you through a complete testing strategy for Material UI components, covering unit tests, integration tests, and end-to-end (E2E) tests.
Why Testing MUI Components Matters
Material UI components encapsulate a lot of internal logic: theming, accessibility attributes, keyboard navigation, and responsive behavior. While MUI is well-tested upstream, the way you compose, theme, and wire these components into your application introduces your own risk surface. A robust test suite helps you:
- Catch regressions when upgrading MUI versions or customizing the theme.
- Verify that custom compositions (e.g., a Dialog with a Form) behave as expected.
- Ensure accessibility features like ARIA roles and keyboard focus are preserved.
- Validate full user flows that span multiple pages and API calls.
Setting Up the Testing Stack
For most React + MUI projects, the recommended stack is Jest (or Vitest) as the test runner, React Testing Library (RTL) for component-level tests, and Playwright or Cypress for E2E tests. Start by installing the necessary dependencies:
npm install --save-dev @testing-library/react @testing-library/jest-dom @testing-library/user-event @testing-library/dom
npm install --save-dev @playwright/test
Make sure your test setup file imports the custom matchers from @testing-library/jest-dom:
// src/test/setup.js
import '@testing-library/jest-dom';
// Optional: mock matchMedia for components using useMediaQuery
window.matchMedia = window.matchMedia || function (query) {
return {
matches: false,
media: query,
onchange: null,
addListener: () => {},
removeListener: () => {},
addEventListener: () => {},
removeEventListener: () => {},
dispatchEvent: () => false,
};
};
Unit Testing Individual MUI Components
Unit tests focus on a single component in isolation. The golden rule with React Testing Library is to test behavior, not implementation details. Instead of querying by class names or internal MUI structure, query by accessible roles, labels, and text.
Example: Testing a Button Click Handler
// src/components/SubmitButton.test.jsx
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { ThemeProvider, createTheme } from '@mui/material/styles';
import SubmitButton from './SubmitButton';
const renderWithTheme = (ui) => {
const theme = createTheme();
return render(<ThemeProvider theme={theme}>{ui}</ThemeProvider>);
};
describe('SubmitButton', () => {
it('calls onSubmit when clicked', async () => {
const user = userEvent.setup();
const onSubmit = jest.fn();
renderWithTheme(<SubmitButton onSubmit={onSubmit} />);
const button = screen.getByRole('button', { name: /submit/i });
await user.click(button);
expect(onSubmit).toHaveBeenCalledTimes(1);
});
it('is disabled when loading prop is true', () => {
renderWithTheme(<SubmitButton loading />);
expect(screen.getByRole('button')).toBeDisabled();
});
});
Notice that we wrap the component in a ThemeProvider. Many MUI components rely on the theme context, so a small helper like renderWithTheme keeps your tests clean and consistent.
Testing Form Inputs and Validation
MUI text fields render native inputs under the hood, which makes them straightforward to test with RTL. The key is to interact as a real user would: focus, type, blur, and observe.
// src/components/EmailField.test.jsx
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import EmailField from './EmailField';
describe('EmailField', () => {
it('shows an error message for invalid email', async () => {
const user = userEvent.setup();
render(<EmailField />);
const input = screen.getByRole('textbox', { name: /email/i });
await user.type(input, 'not-an-email');
await user.tab();
expect(await screen.findByText(/enter a valid email/i)).toBeInTheDocument();
});
it('accepts a valid email without errors', async () => {
const user = userEvent.setup();
render(<EmailField />);
const input = screen.getByRole('textbox', { name: /email/i });
await user.type(input, 'user@example.com');
await user.tab();
expect(screen.queryByText(/enter a valid email/i)).not.toBeInTheDocument();
});
});
Integration Testing MUI Compositions
Integration tests verify that several components work together. A common scenario is a Dialog containing a form, where focus management, backdrop clicks, and form submission all need to cooperate.
Example: Testing a Dialog Form
// src/components/SettingsDialog.test.jsx
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import SettingsDialog from './SettingsDialog';
describe('SettingsDialog', () => {
it('submits the form with entered values', async () => {
const user = userEvent.setup();
const onSave = jest.fn();
render(<SettingsDialog open onSave={onSave} />);
expect(screen.getByRole('dialog')).toBeInTheDocument();
const nameInput = screen.getByLabelText(/display name/i);
await user.type(nameInput, 'Ada Lovelace');
const saveButton = screen.getByRole('button', { name: /save/i });
await user.click(saveButton);
expect(onSave).toHaveBeenCalledWith({ name: 'Ada Lovelace' });
});
it('closes when the cancel button is clicked', async () => {
const user = userEvent.setup();
const onClose = jest.fn();
render(<SettingsDialog open onClose={onClose} />);
await user.click(screen.getByRole('button', { name: /cancel/i }));
expect(onClose).toHaveBeenCalled();
});
});
Because MUI Dialog uses portals, RTL's screen queries still find the content since they query document.body by default. This means you rarely need special handling for portals.
Testing Theme and Styling
When you customize the MUI theme, you may want to verify that certain styles are applied. While you should generally avoid asserting on CSS classes, sometimes checking computed styles is justified — for example, ensuring a custom palette color is applied.
// src/theme.test.jsx
import { render } from '@testing-library/react';
import { ThemeProvider, createTheme } from '@mui/material/styles';
import Button from '@mui/material/Button';
import { Box } from '@mui/material';
describe('Custom theme', () => {
it('applies the primary color to buttons', () => {
const theme = createTheme({
palette: { primary: { main: '#ff5722' } },
});
const { container } = render(
<ThemeProvider theme={theme}>
<Button variant="contained" color="primary">Click</Button>
</ThemeProvider>
);
const button = container.querySelector('button');
const styles = window.getComputedStyle(button);
// Note: jsdom may not compute all styles; prefer visual regression for color checks.
expect(button).toBeInTheDocument();
});
});
For true visual verification, consider adding a visual regression tool like Chromatic or Playwright screenshot comparisons, since jsdom does not fully compute styles.
End-to-End Testing with Playwright
E2E tests run against a real browser and a running dev server. They validate complete user journeys, including routing, API calls, and MUI's interactive components like Select dropdowns and DataGrids.
Example: E2E Test for a Login Flow
// e2e/login.spec.js
import { test, expect } from '@playwright/test';
test('user can log in with valid credentials', async ({ page }) => {
await page.goto('http://localhost:3000/login');
// MUI TextField renders a native input; target by label
await page.getByLabel(/email/i).fill('user@example.com');
await page.getByLabel(/password/i).fill('correct-horse-battery-staple');
await page.getByRole('button', { name: /sign in/i }).click();
// Wait for navigation to the dashboard
await expect(page).toHaveURL(/\/dashboard$/);
await expect(page.getByText(/welcome back/i)).toBeVisible();
});
test('shows error for invalid credentials', async ({ page }) => {
await page.goto('http://localhost:3000/login');
await page.getByLabel(/email/i).fill('user@example.com');
await page.getByLabel(/password/i).fill('wrong-password');
await page.getByRole('button', { name: /sign in/i }).click();
await expect(page.getByText(/invalid credentials/i)).toBeVisible();
});
Handling MUI Selects in E2E Tests
MUI Select uses a portal-rendered listbox, which can complicate selectors. Playwright handles this gracefully if you interact by role and option text.
// e2e/settings.spec.js
import { test, expect } from '@playwright/test';
test('user can change language preference', async ({ page }) => {
await page.goto('http://localhost:3000/settings');
await page.getByRole('combobox', { name: /language/i }).click();
await page.getByRole('option', { name: /français/i }).click();
await expect(page.getByRole('combobox', { name: /language/i })).toHaveText(/français/i);
});
Best Practices
- Query by role and accessible name rather than test IDs or CSS classes. This keeps tests aligned with how users and assistive technology interact with your UI.
- Wrap components in ThemeProvider in unit tests to avoid context errors and to mirror production behavior.
- Mock
matchMediain jsdom environments, since MUI'suseMediaQueryhook depends on it. - Use
userEventoverfireEventfor realistic interactions that trigger focus, blur, and keyboard events in the correct order. - Avoid testing MUI internals like internal class names or component structure, which can change between versions.
- Keep E2E tests focused on critical paths like authentication, checkout, or settings — they are slower and more brittle than unit tests.
- Run accessibility checks with
jest-axein unit tests and@axe-core/playwrightin E2E tests to ensure MUI customizations do not break a11y. - Snapshot test sparingly; prefer behavioral assertions. If you do snapshot, exclude volatile MUI-generated class names.
Adding Accessibility Assertions
// src/components/NavMenu.test.jsx
import { render } from '@testing-library/react';
import { axe, toHaveNoViolations } from 'jest-axe';
import NavMenu from './NavMenu';
expect.extend(toHaveNoViolations);
describe('NavMenu accessibility', () => {
it('has no a11y violations', async () => {
const { container } = render(<NavMenu />);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
});
Conclusion
Testing Material UI components effectively means meeting users where they are: interacting with roles, labels, and visible text rather than implementation details. Unit tests with React Testing Library validate individual components and their props, integration tests confirm that complex compositions like dialogs and forms cooperate correctly, and E2E tests with Playwright ensure the full user journey works in a real browser. By combining these layers with accessibility checks and thoughtful theme testing, you build a safety net that lets you upgrade MUI, refactor freely, and ship with confidence — knowing that both the smallest button and the longest user flow are covered.