第七回:マイクロフロントエンドのテスト戦略
「個別のアプリのテストはパスするのに、統合すると動かない...」
「E2Eテストをどう書けばいいの?」
「テストの実行時間が長くなってきた...」
マイクロフロントエンドのテストは、従来のフロントエンドテストとは少し違った考え方が必要です。今回は、効果的なテスト戦略を具体的に見ていきましょう。
テストのレイヤー
マイクロフロントエンドでは、3つのレイヤーでテストを考える必要があります:
テストピラミッド:
/ \
/ E2E \
/ 統合テスト \
/ ユニットテスト \
1. ユニットテスト(各アプリ個別のテスト)
// 商品一覧コンポーネントのテスト
import { render, screen } from '@testing-library/react';
import { ProductList } from './ProductList';
describe('ProductList', () => {
test('商品一覧が正しく表示される', () => {
const products = [
{ id: 1, name: '商品A', price: 1000 },
{ id: 2, name: '商品B', price: 2000 }
];
render(<ProductList products={products} />);
// 商品名が表示されているか確認
expect(screen.getByText('商品A')).toBeInTheDocument();
expect(screen.getByText('商品B')).toBeInTheDocument();
});
test('カートに追加できる', async () => {
render(<ProductList products={[{ id: 1, name: '商品A', price: 1000 }]} />);
// カートに追加ボタンをクリック
await userEvent.click(screen.getByText('カートに追加'));
// イベントが発火されたか確認
expect(mockEventBus.emit).toHaveBeenCalledWith(
'addToCart',
expect.any(Object)
);
});
});
2. 統合テスト(アプリ間の連携テスト)
// カートと商品一覧の連携テスト
describe('商品一覧とカートの連携', () => {
test('商品をカートに追加できる', async () => {
// 両方のアプリをマウント
render(
<>
<ProductApp />
<CartApp />
</>
);
// 商品一覧から商品を追加
await userEvent.click(screen.getByText('カートに追加'));
// カートに商品が追加されたか確認
expect(screen.getByTestId('cart-count')).toHaveTextContent('1');
});
test('カートの合計金額が正しく計算される', async () => {
// テストコード
});
});
3. E2Eテスト(全体の流れのテスト)
// Cypressを使ったE2Eテスト
describe('買い物フロー', () => {
it('商品を購入できる', () => {
// トップページに遷移
cy.visit('/');
// 商品を選択
cy.get('[data-testid="product-item"]').first().click();
// カートに追加
cy.get('[data-testid="add-to-cart"]').click();
// カートに遷移
cy.get('[data-testid="cart-link"]').click();
// 購入手続きへ
cy.get('[data-testid="checkout-button"]').click();
// 購入完了を確認
cy.get('[data-testid="confirmation"]')
.should('contain', '購入完了');
});
});
テストの効率化のコツ
1. モック戦略
// 共有イベントバスのモック
const mockEventBus = {
emit: jest.fn(),
on: jest.fn()
};
// テストでの使用
jest.mock('../shared/eventBus', () => ({
eventBus: mockEventBus
}));
test('イベントが正しく発行される', () => {
// テストコード
expect(mockEventBus.emit)
.toHaveBeenCalledWith('addToCart', expect.any(Object));
});
2. テスト環境の構築
// テスト用の設定ファイル(jest.config.js)
module.exports = {
// 各アプリのテストをまとめて実行
projects: [
'<rootDir>/apps/*/jest.config.js'
],
// 共通の設定
setupFilesAfterEnv: [
'<rootDir>/test/setupTests.js'
],
// モジュールのモック
moduleNameMapper: {
// スタイルファイルのモック
'\\.(css|less|scss)$': 'identity-obj-proxy',
// 画像ファイルのモック
'\\.(jpg|jpeg|png|gif)$': '<rootDir>/test/__mocks__/fileMock.js'
}
};
3. CIへの組み込み
# GitHub Actionsの例
name: Test
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
# ユニットテスト
- name: Unit Tests
run: npm test
# 統合テスト
- name: Integration Tests
run: npm run test:integration
# E2Eテスト
- name: E2E Tests
run: npm run test:e2e
よくある課題と解決策
1. テストの実行時間が長い
// 並列実行の設定
module.exports = {
maxWorkers: 4, // 並列実行数
// アプリごとに別々のワーカーで実行
projects: [
{
displayName: 'product-app',
testMatch: ['<rootDir>/apps/product/**/*.test.js']
},
{
displayName: 'cart-app',
testMatch: ['<rootDir>/apps/cart/**/*.test.js']
}
]
};
2. フラッキーテスト(不安定なテスト)への対応
// リトライロジックの実装
const retry = (fn, retries = 3) => async () => {
for (let i = 0; i < retries; i++) {
try {
await fn();
return;
} catch (error) {
if (i === retries - 1) throw error;
await new Promise(r => setTimeout(r, 1000));
}
}
};
test('不安定なテスト', retry(async () => {
// テストコード
}));
まとめ
マイクロフロントエンドのテストでは:
各レイヤーに適したテスト方法を選ぶ
テストの効率化を工夫する
CI/CDへの組み込みを考える
が重要です。
次回は、マイクロフロントエンドのデプロイメント戦略について詳しく見ていきます。