How to Test Material UI Components
Test Material UI through the DOM and behavior users can observe. Set up React Testing Library, choose accessible queries, handle interactions and async work, and troubleshoot common failures.
Test Material UI components by rendering them as part of your application and asserting on the DOM and behavior a user can observe. Use React Testing Library queries based on accessible roles, names, labels, and visible text; use user-event for supported interactions. Avoid depending on Material UI component instances, React internals, or private state.
Material UI’s guidance is direct: “It’s generally recommended to test your application without tying the tests too closely to Material UI.” Material UI testing guide. This keeps tests useful when you change component implementations while preserving the same user-facing behavior.
1. Choose a test runner and install the testing tools
React Testing Library is a testing utility, not a test runner. It can be used with different runners and DOM environments. The example below uses Vitest with JSDOM; adapt the runner setup if your project already uses Jest or another supported runner.
npm install --save-dev vitest jsdom @testing-library/react @testing-library/user-event @testing-library/jest-dom
Add a test script to package.json:
{
"scripts": {
"test": "vitest"
}
}
Configure Vitest to use JSDOM and load jest-dom matchers. For example, create vitest.config.js:
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
environment: 'jsdom',
setupFiles: './src/test/setup.js',
},
});
Create src/test/setup.js:
import '@testing-library/jest-dom/vitest';
With Jest, use its JSDOM environment and the corresponding jest-dom setup import. Keep the environment and matcher setup consistent with your runner.
2. Render a component and test its accessible interface
Suppose the component is a Material UI text field and button. Query the textbox by its label and the button by its accessible name. Assert the result the user sees after entering a value and clicking.
// GreetingForm.jsx
import { useState } from 'react';
import Button from '@mui/material/Button';
import TextField from '@mui/material/TextField';
export function GreetingForm() {
const [name, setName] = useState('');
const [greeting, setGreeting] = useState('');
return (
<form onSubmit={(event) => {
event.preventDefault();
setGreeting(`Hello, ${name}`);
}}>
<TextField
label="Your name"
value={name}
onChange={(event) => setName(event.target.value)}
/>
<Button type="submit">Greet</Button>
{greeting && <p role="status">{greeting}</p>}
</form>
);
}
// GreetingForm.test.jsx
import { describe, expect, it } from 'vitest';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { GreetingForm } from './GreetingForm';
describe('GreetingForm', () => {
it('shows a greeting for the entered name', async () => {
const user = userEvent.setup();
render(<GreetingForm />);
await user.type(screen.getByRole('textbox', { name: /your name/i }), 'Sam');
await user.click(screen.getByRole('button', { name: /greet/i }));
expect(screen.getByRole('status')).toHaveTextContent('Hello, Sam');
});
});
The example uses screen to query the rendered document. A labeled Material UI TextField exposes a textbox users and assistive technology can identify. If a query fails, first inspect whether the control has an accessible name and the expected role.
3. Select queries that match what users can identify
Prefer queries in this order, choosing the most meaningful query for the interface:
| Query | Use it for | Example |
|---|---|---|
| Role and accessible name | Buttons, links, headings, dialogs, checkboxes, and other semantic controls | getByRole('button', { name: 'Save' }) |
| Label text | Form controls with visible labels | getByLabelText('Email address') |
| Visible text | Content or text that identifies an item | getByText('Settings saved') |
| Placeholder | A last resort when no better label or role query applies | getByPlaceholderText('Search') |
| Test ID | A last resort for an element with no useful user-facing selector | getByTestId('chart-canvas') |
Use getBy* when the element should already exist, queryBy* when checking that it is absent, and findBy* when it should appear asynchronously. Scope repeated controls with within rather than relying on a generated class or broad selector:
import { screen, within } from '@testing-library/react';
const billingRow = screen.getByRole('row', { name: /billing/i });
await user.click(within(billingRow).getByRole('button', { name: /edit/i }));
Do not query MUI-generated class names or inspect component instances to assert what a user can observe. A query such as container.querySelector('.MuiButton-root') couples the test to styling and markup details without proving the button works.
4. Test interactions with user-event
The current user-event documentation describes v14. Create a userEvent.setup() instance in each test before rendering, and await interactions. It models fuller user interactions than dispatching one event directly. Use fireEvent when you need a low-level event detail or interaction that user-event does not express.
it('disables save until the form is valid', async () => {
const user = userEvent.setup();
render(<ProfileForm />);
const save = screen.getByRole('button', { name: /save/i });
expect(save).toBeDisabled();
await user.type(screen.getByRole('textbox', { name: /email/i }), 'dev@example.com');
expect(save).toBeEnabled();
});
Test keyboard behavior when it is part of the expected interface. For example, exercise a dialog’s close button or Escape handling and assert that the dialog is no longer present. Avoid calling an internal handler directly: interact through the control or key the user would use.
5. Test asynchronous states and errors
For a component that loads data, assert the loading state and then await the resulting accessible content. Async queries wait for an element to appear; they are generally clearer than arbitrary sleeps.
render(<AccountSummary />);
expect(screen.getByRole('progressbar')).toBeInTheDocument();
expect(await screen.findByRole('heading', { name: /account summary/i })).toBeInTheDocument();
For an element that should disappear, wait for removal:
import { waitForElementToBeRemoved } from '@testing-library/react';
const loading = screen.getByRole('progressbar');
await waitForElementToBeRemoved(loading);
Use the query that matches the state: getByRole for an immediate element, findByRole for a later one, and queryByRole to assert absence. Avoid fixed timeouts; they slow the suite and can still race.
6. Mock network requests with MSW
When a component communicates with an API, the Testing Library example recommends Mock Service Worker (MSW) for declarative request mocking. This lets the component use its normal request path while the test supplies controlled responses. Define handlers for the success, empty, and error cases relevant to the UI, and reset handlers between tests so one test cannot leak configuration into another.
import { http, HttpResponse } from 'msw';
import { setupServer } from 'msw/node';
const server = setupServer(
http.get('/api/account', () => HttpResponse.json({ name: 'Sam' }))
);
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
Then render the component and assert its visible state:
render(<AccountSummary />);
expect(await screen.findByText('Sam')).toBeInTheDocument();
Set up MSW according to its documentation and your application’s request URL. A test that only replaces a component’s internal fetch function may miss integration behavior between the UI and request layer.
7. Render components that need providers
Some components depend on application context, such as a theme, router, localization provider, or state store. Render the real provider configuration needed by the component. A small helper can keep setup consistent:
import { render } from '@testing-library/react';
import { ThemeProvider, createTheme } from '@mui/material/styles';
const theme = createTheme();
function renderWithTheme(ui) {
return render(<ThemeProvider theme={theme}>{ui}</ThemeProvider>);
}
Add only providers the component actually needs. If your application has a shared provider tree, use a wrapper around that tree so tests behave like the application without duplicating setup in every case.
8. Snapshots, styles, and browser limits
Material UI does not recommend snapshot testing as the default approach. Snapshots can be secondary, but they do not replace assertions that a user can find and operate the control or see the expected result. Prefer focused behavioral tests.
DOM-based tests in JSDOM are useful for component behavior, but they are not proof of every browser-specific visual detail. Testing Library notes that DOM Testing Library can run in simulated DOM environments or a real browser; user-event also documents that ordinary programmatic tests cannot generate trusted browser UI events and uses workarounds. Use a real browser check for behavior that depends on browser rendering or trusted events.
9. Common failures and fixes
| Symptom | Likely cause | Fix |
|---|---|---|
| Unable to find a label or role | The control has no accessible label, the accessible name differs, or it has not appeared yet. | Check the rendered accessible name and role; add a proper label if needed; use findByRole for async content. |
| Multiple elements match the query | The same text or control appears more than once. | Query the semantic container, then use within; make the UI’s accessible names distinct where appropriate. |
| Click or typing assertion runs too early | An interaction was not awaited, or an async result was asserted synchronously. | Await each user-event interaction and use findBy* for elements that appear later. |
| Component throws about missing context | A required theme, router, localization, or state provider is absent. | Render with the provider configuration that the component needs. |
Matcher such as toBeInTheDocument is undefined |
jest-dom was not loaded, or its runner-specific setup import is wrong. | Load the jest-dom setup file in the test runner configuration and use the import for that runner. |
| Test passes alone but fails in the suite | Handlers, mocks, or shared state may be leaking between tests. | Reset MSW handlers and restore or clear mocks and mutable state after each test. |
| Tests fail after a harmless MUI update | Assertions depend on generated markup, CSS classes, or snapshots of implementation details. | Use stable roles, labels, names, and visible outcomes instead. |
| JSDOM behavior differs from a real browser | The behavior depends on browser rendering, layout, or trusted UI events. | Keep DOM tests for component behavior and verify the browser-specific case in a real browser. |
10. Keep the suite fast and reliable
- Render only the component or small integration surface needed for the behavior under test.
- Prefer accessible queries and explicit outcomes over broad snapshots and fragile selectors.
- Await asynchronous interactions and use async queries instead of fixed delays.
- Mock network boundaries declaratively when a test needs deterministic responses.
- Keep tests isolated: reset handlers and shared mocks, and avoid depending on test order.
- Use JSDOM for routine DOM behavior and reserve real-browser coverage for browser-specific behavior.
Tests that wait for stable visible conditions and isolate external responses are less prone to timing failures. A real browser can provide additional confidence for layout-sensitive cases, though it is a separate test environment with its own setup and runtime cost.
11. Or skip the browser setup
If the task is to capture a rendered website that contains your Material UI interface, you can request a screenshot through ScreenshotNeo. This is separate from component assertions: it gives you a rendered image or PDF of a page, rather than a React DOM test. See the ScreenshotNeo API documentation.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get(
"https://api.screenshotneo.com/v1/shot",
params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"},
timeout=90,
)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
ScreenshotNeo removes cookie banners, newsletter popups, and chat widgets before capture. Bot checks, blank pages, and failed loads are never billed. Its MCP server lets AI agents take screenshots. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000. Sign up for free.
12. FAQ
Should I test Material UI internals directly?
No. Test the accessible DOM and user-visible behavior of the application that uses the component.
Does React Testing Library require Jest?
No. It is not a test runner and can work with different runners and DOM environments.
Should I use fireEvent or user-event?
Use user-event v14 for interactions it supports; use fireEvent for low-level event details or interactions user-event does not yet express.
Are component tests enough to verify visual appearance?
No. DOM tests cover behavior and structure exposed to the DOM; browser-specific appearance and trusted-event behavior may need real-browser coverage.


