MUI ButtonをReact Testing Libraryでテストする方法を解説します。
基本的なクリックイベントから、disabled状態、各種プロパティ、非同期処理中の二重クリック防止まで、実務で必要なパターンを網羅しています。
動作環境
- @mui/material v6
- @testing-library/react v16
- @testing-library/user-event v14
- vitest v3
基本的なテスト
ボタンの取得
getByRole('button')でボタンを取得します。nameオプションでボタンのテキストを指定できます。
import Button from '@mui/material/Button';
import {render, screen, waitFor} from '@testing-library/react';
test('ボタンがレンダリングされる', () => {
render(<Button>送信する</Button>);
const button = screen.getByRole('button', {name: '送信する'});
expect(button).toBeInTheDocument();
});data-testidで取得することもできますが、ユーザーが実際に認識する方法(ロールやテキスト)で取得する方が推奨されます。
// 非推奨:data-testidでの取得
render(<Button data-testid="submit-button">送信</Button>);
const button = screen.getByTestId('submit-button');
// 推奨:ロールでの取得
const button = screen.getByRole('button', {name: '送信'});クリックイベント
userEvent.click()でクリックをシミュレートします。
import userEvent from '@testing-library/user-event';
test('クリック時にonClickが呼ばれる', async () => {
const user = userEvent.setup();
const onClick = vi.fn();
render(<Button onClick={onClick}>クリック</Button>);
const button = screen.getByRole('button', {name: 'クリック'});
await user.click(button);
expect(onClick).toHaveBeenCalledTimes(1);
});
test('複数回クリックできる', async () => {
const user = userEvent.setup();
const onClick = vi.fn();
render(<Button onClick={onClick}>クリック</Button>);
const button = screen.getByRole('button', {name: 'クリック'});
await user.click(button);
await user.click(button);
await user.click(button);
expect(onClick).toHaveBeenCalledTimes(3);
});フォーム送信ボタン
type属性によってフォームの挙動が変わります。
test('type="submit" でフォーム送信がトリガーされる', async () => {
const user = userEvent.setup();
const onSubmit = vi.fn(e => e.preventDefault());
render(
<form onSubmit={onSubmit}>
<Button type="submit">送信</Button>
</form>
);
await user.click(screen.getByRole('button', {name: '送信'}));
expect(onSubmit).toHaveBeenCalledTimes(1);
});
test('type="button" はフォーム送信をトリガーしない', async () => {
const user = userEvent.setup();
const onSubmit = vi.fn(e => e.preventDefault());
render(
<form onSubmit={onSubmit}>
<Button type="button">キャンセル</Button>
</form>
);
await user.click(screen.getByRole('button', {name: 'キャンセル'}));
expect(onSubmit).not.toHaveBeenCalled();
});
test('type="reset" でフォームをリセットできる', async () => {
const user = userEvent.setup();
render(
<form>
<input defaultValue="初期値" data-testid="input" />
<Button type="reset">リセット</Button>
</form>
);
const input = screen.getByTestId('input') as HTMLInputElement;
await user.clear(input);
await user.type(input, '変更後');
expect(input.value).toBe('変更後');
await user.click(screen.getByRole('button', {name: 'リセット'}));
expect(input.value).toBe('初期値');
});disabled状態のテスト
toBeDisabled() / toBeEnabled() を使う
test('disabled=true でボタンが無効化される', () => {
render(<Button disabled={true}>ボタン</Button>);
const button = screen.getByRole('button', {name: 'ボタン'});
expect(button).toBeDisabled();
});
test('disabled=false でボタンが有効化される', () => {
render(<Button disabled={false}>ボタン</Button>);
const button = screen.getByRole('button', {name: 'ボタン'});
expect(button).toBeEnabled();
});toHaveAttribute(‘disabled’) は避ける
// ❌ これは失敗する
expect(button).toHaveAttribute('disabled', 'true');
// ✅ こちらを使う
expect(button).toBeDisabled();HTMLのboolean属性は値を持たないため、toBeDisabled()を使いましょう。
disabled時はクリックイベントが発火しない
userEventでは、disabledのボタンをクリックできないので、注意が必要です。
test('disabled時はクリックできない', async () => {
const user = userEvent.setup();
const onClick = vi.fn();
render(<Button disabled onClick={onClick}>ボタン</Button>);
await user.click(screen.getByRole('button', {name: 'ボタン'})); // ❌ これは失敗する
expect(onClick).not.toHaveBeenCalled();
});プロパティのテスト
MUIのButtonはvariant、color、sizeなどのプロパティに応じてクラスが付与されます。
必要があれば、toHaveClassでチェック可能です。
variant
test('variant="contained" が適用される', () => {
render(<Button variant="contained">Contained</Button>);
const button = screen.getByRole('button', {name: 'Contained'});
expect(button).toHaveClass('MuiButton-contained');
});
test('variant="outlined" が適用される', () => {
render(<Button variant="outlined">Outlined</Button>);
const button = screen.getByRole('button', {name: 'Outlined'});
expect(button).toHaveClass('MuiButton-outlined');
});
test('variant="text" が適用される(デフォルト)', () => {
render(<Button>Text</Button>);
const button = screen.getByRole('button', {name: 'Text'});
expect(button).toHaveClass('MuiButton-text');
});
color
test('color="primary" が適用される', () => {
render(<Button color="primary">Primary</Button>);
const button = screen.getByRole('button', {name: 'Primary'});
expect(button).toHaveClass('MuiButton-colorPrimary');
});
test('color="secondary" が適用される', () => {
render(<Button color="secondary">Secondary</Button>);
const button = screen.getByRole('button', {name: 'Secondary'});
expect(button).toHaveClass('MuiButton-colorSecondary');
});
test('color="error" が適用される', () => {
render(<Button color="error">Error</Button>);
const button = screen.getByRole('button', {name: 'Error'});
expect(button).toHaveClass('MuiButton-colorError');
});size
test('size="small" が適用される', () => {
render(<Button size="small">Small</Button>);
const button = screen.getByRole('button', {name: 'Small'});
expect(button).toHaveClass('MuiButton-sizeSmall');
});
test('size="medium" が適用される(デフォルト)', () => {
render(<Button size="medium">Medium</Button>);
const button = screen.getByRole('button', {name: 'Medium'});
expect(button).toHaveClass('MuiButton-sizeMedium');
});
test('size="large" が適用される', () => {
render(<Button size="large">Large</Button>);
const button = screen.getByRole('button', {name: 'Large'});
expect(button).toHaveClass('MuiButton-sizeLarge');
});fullWidth
test('fullWidth が適用される', () => {
render(<Button fullWidth>Full Width</Button>);
const button = screen.getByRole('button', {name: 'Full Width'});
expect(button).toHaveClass('MuiButton-fullWidth');
});
startIcon / endIcon
test('startIconが表示される', () => {
const Icon = () => <span data-testid="start-icon">🚀</span>;
render(<Button startIcon={<Icon />}>Button</Button>);
expect(screen.getByTestId('start-icon')).toBeInTheDocument();
});
test('endIconが表示される', () => {
const Icon = () => <span data-testid="end-icon">→</span>;
render(<Button endIcon={<Icon />}>Button</Button>);
expect(screen.getByTestId('end-icon')).toBeInTheDocument();
});非同期処理のテスト
API呼び出しなど非同期処理中はボタンを無効化し、二重クリックを防ぐのが一般的です。
テスト対象のコンポーネント
const AsyncButton: React.FC = () => {
const [loading, setLoading] = useState(false);
const [result, setResult] = useState<string | null>(null);
const handleClick = async () => {
setLoading(true);
await new Promise(resolve => setTimeout(resolve, 100));
setResult('完了');
setLoading(false);
};
return (
<div>
<Button disabled={loading} onClick={handleClick}>
{loading ? '処理中...' : '実行'}
</Button>
{result && <div data-testid="result">{result}</div>}
</div>
);
};テストコード
test('処理中はボタンが無効化される', async () => {
const user = userEvent.setup();
render(<AsyncButton />);
const button = screen.getByRole('button', {name: '実行'});
expect(button).toBeEnabled();
await user.click(button);
expect(button).toBeDisabled();
});
test('処理完了後、ボタンが再度有効化される(findByを使う)', async () => {
const user = userEvent.setup();
render(<AsyncButton />);
await user.click(screen.getByRole('button', {name: '実行'}));
// findByは要素が見つかるまで待機する
await screen.findByTestId('result');
expect(screen.getByRole('button', {name: '実行'})).toBeEnabled();
});
test('処理完了後、ボタンが再度有効化される(waitForを使う)', async () => {
const user = userEvent.setup();
render(<AsyncButton />);
await user.click(screen.getByRole('button', {name: '実行'}));
// waitForは条件が満たされるまで待機する
await waitFor(() => {
expect(screen.getByRole('button', {name: '実行'})).toBeEnabled();
});
});
test('処理中は二重クリックされない', async () => {
const user = userEvent.setup();
const onClick = vi.fn(async () => {
await new Promise(resolve => setTimeout(resolve, 100));
});
const TestButton: React.FC = () => {
const [loading, setLoading] = useState(false);
const handleClick = async () => {
setLoading(true);
await onClick();
setLoading(false);
};
return (
<Button disabled={loading} onClick={handleClick}>
{loading ? '処理中...' : '送信'}
</Button>
);
};
render(<TestButton />);
await user.click(screen.getByRole('button', {name: '送信'}));
// 処理中はdisabledなので再クリック不可
expect(screen.getByRole('button', {name: '処理中...'})).toBeDisabled();
expect(onClick).toHaveBeenCalledTimes(1);
});findByは要素が見つかるまで、waitForは条件が満たされるまで待機します。どちらも非同期処理の完了を待つのに使えます。
まとめ
この記事では、MUI ButtonをReact Testing Libraryでテストする方法を解説しました。
テストパターン一覧
| パターン | 使用するマッチャー/クエリ |
|---|---|
| ボタンの取得 | getByRole('button', {name: 'テキスト'}) |
| クリックイベント | userEvent.click() + toHaveBeenCalled() |
| disabled状態 | toBeDisabled() / toBeEnabled() |
| クラスの確認 | toHaveClass('MuiButton-xxx') |
| 非同期処理の完了待ち | findByTestId() / findByRole() |
ポイント
- ボタンは
getByRole('button')で取得する - disabled状態は
toBeDisabled()を使う(toHaveAttributeは避ける) - 非同期処理の完了待ちには
findByクエリを使う - プロパティのテストはMUIのクラス名に依存するため、必要な場合のみ実施する

コメント