微前端进阶(三)

微前端进阶(三)

从单元测试到 E2E,构建微前端架构下的完整质量保障体系


目录

  1. 微前端测试挑战与策略总览
  2. 单元测试:子应用独立测试
  3. [组件测试:UI 组件与共享组件](#组件测试:UI 组件与共享组件)
  4. 集成测试:跨子应用交互测试
  5. [E2E 测试:全链路用户场景](#E2E 测试:全链路用户场景)
  6. 契约测试:子应用接口契约
  7. [测试基础设施与 Mock 策略](#测试基础设施与 Mock 策略)
  8. [质量门禁与 CI 集成](#质量门禁与 CI 集成)

一、微前端测试挑战与策略总览

1.1 微前端测试的核心挑战

复制代码
┌─────────────────────────────────────────────────────────────┐
│                  微前端测试的核心挑战                         │
├─────────────────────────────────────────────────────────────┤
│  挑战 1:多技术栈                                            │
│  基座是 Vue3,子应用可能是 React/Angular,跨框架测试困难      │
├─────────────────────────────────────────────────────────────┤
│  挑战 2:运行时集成                                          │
│  子应用在运行时动态加载,无法在构建时静态分析全部交互路径      │
├─────────────────────────────────────────────────────────────┤
│  挑战 3:隔离环境                                            │
│  JS 沙箱和样式隔离使测试环境与生产环境存在差异                │
├─────────────────────────────────────────────────────────────┤
│  挑战 4:跨应用通信                                          │
│  子应用间通过 EventBus/GlobalState 通信,链路追踪困难         │
├─────────────────────────────────────────────────────────────┤
│  挑战 5:依赖共享                                            │
│  公共依赖通过 CDN/Module Federation 共享,版本一致性需验证    │
├─────────────────────────────────────────────────────────────┤
│  挑战 6:独立部署                                            │
│  各子应用独立发布版本,回归测试范围难以确定                   │
└─────────────────────────────────────────────────────────────┘

1.2 测试金字塔(微前端版)

复制代码
                    ┌──────────────────────┐
                    │     E2E 测试 (5%)     │  全链路用户场景
                    │  Cypress / Playwright │  跨子应用流程
                    └──────────┬───────────┘
                               │
                    ┌──────────▼───────────┐
                    │  集成测试 (15%)       │  跨应用交互
                    │  qiankun 沙箱内测试   │  通信机制
                    └──────────┬───────────┘
                               │
                    ┌──────────▼───────────┐
                    │  契约测试 (10%)       │  子应用接口契约
                    │  Pact / MSW          │  公共依赖兼容性
                    └──────────┬───────────┘
                               │
                    ┌──────────▼───────────┐
                    │  组件测试 (20%)       │  共享组件
                    │  Vitest / Testing Lib│  UI 组件
                    └──────────┬───────────┘
                               │
                    ┌──────────▼───────────┐
                    │  单元测试 (50%)       │  工具函数
                    │  Vitest / Jest       │  Store / Hooks
                    │                      │  API Service
                    └──────────────────────┘

1.3 各层测试责任矩阵

测试类型 测试对象 运行时机 执行环境 责任人
单元测试 工具函数、Store、API Service 每次 commit CI 子应用团队
组件测试 UI 组件、共享组件 每次 commit CI 子应用团队
契约测试 子应用接口、公共依赖 PR 时 CI 子应用团队
集成测试 基座+子应用交互、通信 每日构建 CI 平台团队
E2E 测试 全链路用户场景 预发布 Staging QA 团队

二、单元测试:子应用独立测试

2.1 测试环境搭建

bash 复制代码
# 使用 Vitest(推荐,与 Vite 原生集成)
pnpm add -D vitest @vue/test-utils jsdom
typescript 复制代码
// vite.config.ts(子应用)
/// <reference types="vitest" />
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';

export default defineConfig({
  plugins: [vue()],
  test: {
    // 测试环境
    environment: 'jsdom',
    // 全局 API 模拟
    globals: true,
    // 测试文件匹配
    include: ['src/**/*.{test,spec}.{ts,tsx}'],
    // 覆盖率
    coverage: {
      provider: 'v8',
      reporter: ['text', 'json', 'html'],
      include: ['src/**/*.{ts,vue}'],
      exclude: [
        'src/main.ts',
        'src/public-path.ts',
        'src/**/*.d.ts',
        'src/**/*.{test,spec}.ts'
      ],
      thresholds: {
        statements: 80,
        branches: 70,
        functions: 80,
        lines: 80
      }
    },
    // 全局 setup
    setupFiles: ['./src/__tests__/setup.ts'],
    // 模块别名
    resolve: {
      alias: {
        '@': '/src'
      }
    }
  }
});
typescript 复制代码
// src/__tests__/setup.ts
// 测试全局 setup:模拟 qiankun 环境变量
import { vi } from 'vitest';

// 模拟 qiankun 全局变量
Object.defineProperty(window, '__POWERED_BY_QIANKUN__', {
  value: false,
  writable: true
});

Object.defineProperty(window, '__INJECTED_PUBLIC_PATH_BY_QIANKUN__', {
  value: '',
  writable: true
});

// 模拟 matchMedia
Object.defineProperty(window, 'matchMedia', {
  value: vi.fn().mockImplementation((query: string) => ({
    matches: false,
    media: query,
    onchange: null,
    addListener: vi.fn(),
    removeListener: vi.fn(),
    addEventListener: vi.fn(),
    removeEventListener: vi.fn(),
    dispatchEvent: vi.fn()
  }))
});

// 模拟 IntersectionObserver
class MockIntersectionObserver {
  observe = vi.fn();
  unobserve = vi.fn();
  disconnect = vi.fn();
}
Object.defineProperty(window, 'IntersectionObserver', {
  value: MockIntersectionObserver
});

2.2 工具函数测试

typescript 复制代码
// src/utils/__tests__/helpers.test.ts
import { describe, it, expect } from 'vitest';
import { formatDate, debounce, deepClone, parseQueryString } from '../helpers';

describe('formatDate', () => {
  it('应正确格式化日期', () => {
    const date = new Date('2024-01-15 10:30:00');
    expect(formatDate(date, 'YYYY-MM-DD')).toBe('2024-01-15');
    expect(formatDate(date, 'YYYY/MM/DD HH:mm')).toBe('2024/01/15 10:30');
    expect(formatDate(date, 'MM-DD')).toBe('01-15');
  });

  it('应处理边界情况', () => {
    expect(formatDate(null, 'YYYY-MM-DD')).toBe('');
    expect(formatDate(undefined, 'YYYY-MM-DD')).toBe('');
    expect(formatDate(new Date('invalid'), 'YYYY-MM-DD')).toBe('');
  });
});

describe('debounce', () => {
  it('应在延迟后执行函数', async () => {
    vi.useFakeTimers();
    const fn = vi.fn();
    const debounced = debounce(fn, 100);

    debounced();
    debounced();
    debounced();

    expect(fn).not.toHaveBeenCalled();

    vi.advanceTimersByTime(100);
    expect(fn).toHaveBeenCalledTimes(1);

    vi.useRealTimers();
  });

  it('应传递正确的参数', async () => {
    vi.useFakeTimers();
    const fn = vi.fn();
    const debounced = debounce(fn, 100);

    debounced('arg1', 'arg2');
    vi.advanceTimersByTime(100);

    expect(fn).toHaveBeenCalledWith('arg1', 'arg2');
    vi.useRealTimers();
  });
});

describe('deepClone', () => {
  it('应深度克隆对象', () => {
    const original = { a: 1, b: { c: 2, d: [3, 4] } };
    const cloned = deepClone(original);

    expect(cloned).toEqual(original);
    expect(cloned).not.toBe(original);
    expect(cloned.b).not.toBe(original.b);
    expect(cloned.b.d).not.toBe(original.b.d);
  });

  it('应处理循环引用', () => {
    const obj: any = { a: 1 };
    obj.self = obj;
    expect(() => deepClone(obj)).not.toThrow();
  });
});

describe('parseQueryString', () => {
  it('应正确解析查询字符串', () => {
    const result = parseQueryString('?name=test&age=25&page=1');
    expect(result).toEqual({ name: 'test', age: '25', page: '1' });
  });

  it('应处理空值', () => {
    expect(parseQueryString('')).toEqual({});
    expect(parseQueryString('?')).toEqual({});
  });

  it('应解码 URL 编码', () => {
    const result = parseQueryString('?q=%E6%B5%8B%E8%AF%95&redirect=http%3A%2F%2Fexample.com');
    expect(result).toEqual({ q: '测试', redirect: 'http://example.com' });
  });
});

2.3 Store 测试

typescript 复制代码
// src/stores/__tests__/user.test.ts
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { setActivePinia, createPinia } from 'pinia';
import { useUserStore } from '../user';
import { request } from '@/utils/request';

// Mock axios 请求
vi.mock('@/utils/request', () => ({
  request: {
    get: vi.fn(),
    post: vi.fn()
  }
}));

describe('useUserStore', () => {
  beforeEach(() => {
    setActivePinia(createPinia());
    vi.clearAllMocks();
  });

  it('应初始化用户信息为 null', () => {
    const store = useUserStore();
    expect(store.userInfo).toBeNull();
    expect(store.token).toBe('');
    expect(store.isLoggedIn).toBe(false);
  });

  it('login 应更新 token 和用户信息', async () => {
    const mockLoginResult = {
      accessToken: 'mock-token-123',
      refreshToken: 'mock-refresh-token',
      expiresIn: 7200,
      tokenType: 'Bearer'
    };

    const mockUserInfo = {
      id: '1',
      name: '张三',
      email: 'zhangsan@company.com',
      roles: ['admin'],
      permissions: ['dashboard:view', 'report:export']
    };

    (request.post as any).mockResolvedValueOnce(mockLoginResult);
    (request.get as any).mockResolvedValueOnce(mockUserInfo);

    const store = useUserStore();
    await store.login({ username: 'admin', password: '123456' });

    expect(store.token).toBe('mock-token-123');
    expect(store.userInfo).toEqual(mockUserInfo);
    expect(store.isLoggedIn).toBe(true);
  });

  it('login 失败应抛出错误且不更新状态', async () => {
    (request.post as any).mockRejectedValueOnce(new Error('密码错误'));

    const store = useUserStore();
    await expect(
      store.login({ username: 'admin', password: 'wrong' })
    ).rejects.toThrow('密码错误');

    expect(store.token).toBe('');
    expect(store.userInfo).toBeNull();
    expect(store.isLoggedIn).toBe(false);
  });

  it('logout 应重置状态', () => {
    const store = useUserStore();
    store.$patch({
      token: 'mock-token',
      userInfo: { id: '1', name: '张三', roles: [] }
    });

    store.logout();

    expect(store.token).toBe('');
    expect(store.userInfo).toBeNull();
    expect(store.isLoggedIn).toBe(false);
  });
});

2.4 组合式函数(Composables)测试

typescript 复制代码
// src/composables/__tests__/usePermission.test.ts
import { describe, it, expect, beforeEach } from 'vitest';
import { setActivePinia, createPinia } from 'pinia';
import { usePermissionStore } from '@/stores/permission';
import { usePermission } from '../usePermission';

describe('usePermission', () => {
  beforeEach(() => {
    setActivePinia(createPinia());
    const store = usePermissionStore();
    store.permissions = ['user:view', 'report:export', 'system:config'];
    store.roles = ['editor'];
  });

  describe('hasPermission', () => {
    it('应返回 true 当用户有所需权限', () => {
      const { hasPermission } = usePermission();
      expect(hasPermission('report:export')).toBe(true);
    });

    it('应返回 false 当用户没有所需权限', () => {
      const { hasPermission } = usePermission();
      expect(hasPermission('user:manage')).toBe(false);
    });

    it('超级管理员应有所有权限', () => {
      const store = usePermissionStore();
      store.roles = ['super_admin'];
      const { hasPermission } = usePermission();
      expect(hasPermission('any:permission')).toBe(true);
    });
  });

  describe('hasAnyPermission', () => {
    it('有任一权限应返回 true', () => {
      const { hasAnyPermission } = usePermission();
      expect(hasAnyPermission(['user:manage', 'report:export'])).toBe(true);
    });

    it('没有任何权限应返回 false', () => {
      const { hasAnyPermission } = usePermission();
      expect(hasAnyPermission(['user:manage', 'admin:access'])).toBe(false);
    });
  });

  describe('filterMenusByPermission', () => {
    it('应过滤掉无权限的菜单', () => {
      const { filterMenusByPermission } = usePermission();
      const menus = [
        { path: '/users', title: '用户管理', permissions: ['user:view'] },
        { path: '/admin', title: '管理员', permissions: ['admin:access'] },
        { path: '/dashboard', title: '工作台' }
      ];

      const result = filterMenusByPermission(menus);

      expect(result).toHaveLength(2);
      expect(result.find(m => m.path === '/admin')).toBeUndefined();
    });
  });
});

三、组件测试:UI 组件与共享组件

3.1 Vue 组件测试

typescript 复制代码
// src/components/__tests__/ThemeToggle.test.ts
import { describe, it, expect, beforeEach } from 'vitest';
import { mount } from '@vue/test-utils';
import { setActivePinia, createPinia } from 'pinia';
import ThemeToggle from '@/components/ThemeToggle.vue';

describe('ThemeToggle', () => {
  beforeEach(() => {
    setActivePinia(createPinia());
  });

  it('应渲染主题切换按钮', () => {
    const wrapper = mount(ThemeToggle);
    expect(wrapper.find('.el-button').exists()).toBe(true);
  });

  it('点击应弹出主题选项', async () => {
    const wrapper = mount(ThemeToggle);
    await wrapper.find('.el-button').trigger('click');

    // 检查下拉菜单出现
    expect(document.body.querySelector('.el-dropdown-menu')).toBeTruthy();
  });

  it('选择主题色应触发 store 更新', async () => {
    const wrapper = mount(ThemeToggle);
    await wrapper.find('.el-button').trigger('click');

    // 模拟点击颜色选择
    const colorItems = document.body.querySelectorAll('.el-dropdown-menu .el-dropdown-menu__item');
    if (colorItems.length > 0) {
      (colorItems[1] as HTMLElement).click();
    }

    const themeStore = useThemeStore();
    expect(themeStore.color).toBe('green');
  });
});

3.2 React 组件测试

typescript 复制代码
// sub-app-react/src/components/__tests__/Button.test.tsx
import { describe, it, expect, vi } from 'vitest';
import { render, screen, fireEvent } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { Button } from '../Button';

describe('Button 组件', () => {
  it('应正确渲染文本', () => {
    render(<Button label="提交" />);
    expect(screen.getByText('提交')).toBeInTheDocument();
  });

  it('点击应触发 onClick', async () => {
    const onClick = vi.fn();
    render(<Button label="提交" onClick={onClick} />);
    await userEvent.click(screen.getByText('提交'));
    expect(onClick).toHaveBeenCalledTimes(1);
  });

  it('不同 variant 应应用不同样式', () => {
    const { rerender } = render(<Button label="测试" variant="primary" />);
    const button = screen.getByText('测试');
    expect(button.className).toContain('btn-primary');

    rerender(<Button label="测试" variant="danger" />);
    expect(button.className).toContain('btn-danger');
  });

  it('disabled 时不应触发 onClick', async () => {
    const onClick = vi.fn();
    render(<Button label="提交" disabled onClick={onClick} />);
    await userEvent.click(screen.getByText('提交'));
    expect(onClick).not.toHaveBeenCalled();
  });
});

3.3 共享组件测试

typescript 复制代码
// @company/ui/src/__tests__/DataTable.test.tsx
import { describe, it, expect, vi } from 'vitest';
import { render, screen, within } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { DataTable } from '../DataTable';

const mockColumns = [
  { key: 'name', title: '姓名' },
  { key: 'age', title: '年龄' },
  { key: 'email', title: '邮箱' }
];

const mockData = [
  { id: 1, name: '张三', age: 28, email: 'zhang@example.com' },
  { id: 2, name: '李四', age: 32, email: 'li@example.com' },
  { id: 3, name: '王五', age: 25, email: 'wang@example.com' }
];

describe('DataTable', () => {
  it('应正确渲染表头和数据行', () => {
    render(<DataTable columns={mockColumns} dataSource={mockData} />);

    // 检查表头
    expect(screen.getByText('姓名')).toBeInTheDocument();
    expect(screen.getByText('年龄')).toBeInTheDocument();
    expect(screen.getByText('邮箱')).toBeInTheDocument();

    // 检查数据行数
    const rows = screen.getAllByRole('row');
    expect(rows).toHaveLength(mockData.length + 1); // +1 for header
  });

  it('点击排序应触发 onSort 回调', async () => {
    const onSort = vi.fn();
    render(<DataTable columns={mockColumns} dataSource={mockData} onSort={onSort} />);

    await userEvent.click(screen.getByText('年龄'));
    expect(onSort).toHaveBeenCalledWith('age', 'ascend');

    await userEvent.click(screen.getByText('年龄'));
    expect(onSort).toHaveBeenCalledWith('age', 'descend');
  });

  it('空数据应显示空状态', () => {
    render(<DataTable columns={mockColumns} dataSource={[]} />);
    expect(screen.getByText('暂无数据')).toBeInTheDocument();
  });

  it('加载状态应显示骨架屏', () => {
    render(<DataTable columns={mockColumns} dataSource={[]} loading />);
    expect(document.querySelector('.skeleton-loader')).toBeTruthy();
  });
});

四、集成测试:跨子应用交互测试

4.1 qiankun 沙箱内测试

typescript 复制代码
// portal/src/__tests__/micro-integration.test.ts
import { describe, it, expect, beforeAll, afterAll, vi } from 'vitest';
import {
  registerMicroApps,
  start,
  loadMicroApp
} from 'qiankun';

// 模拟子应用入口
const mockReactApp = {
  bootstrap: vi.fn().mockResolvedValue(undefined),
  mount: vi.fn().mockResolvedValue(undefined),
  unmount: vi.fn().mockResolvedValue(undefined),
  update: vi.fn().mockResolvedValue(undefined)
};

describe('qiankun 集成测试', () => {
  beforeAll(() => {
    // 模拟子应用 UMD 入口
    (window as any).appReact = mockReactApp;
  });

  afterAll(() => {
    vi.restoreAllMocks();
  });

  it('应正确注册子应用', () => {
    const apps = [
      {
        name: 'app-react',
        entry: '//localhost:3001',
        container: '#micro-container',
        activeRule: '/react',
        props: { testMode: true }
      }
    ];

    registerMicroApps(apps);
    // 验证注册是否成功(通过 qiankun 内部 API)
    const registeredApps = (window as any).__QIANKUN_APPS__;
    expect(registeredApps).toBeDefined();
    expect(registeredApps).toHaveLength(1);
    expect(registeredApps[0].name).toBe('app-react');
  });

  it('加载子应用应调用生命周期', async () => {
    const app = loadMicroApp({
      name: 'app-react',
      entry: '//localhost:3001',
      container: '#micro-container',
      props: { testMode: true }
    });

    await app.mount();

    expect(mockReactApp.bootstrap).toHaveBeenCalled();
    expect(mockReactApp.mount).toHaveBeenCalled();

    await app.unmount();
    expect(mockReactApp.unmount).toHaveBeenCalled();
  });
});

4.2 全局状态通信测试

typescript 复制代码
// portal/src/__tests__/global-state.test.ts
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { initGlobalState } from 'qiankun';

describe('GlobalState 通信', () => {
  let actions: ReturnType<typeof initGlobalState>;

  beforeEach(() => {
    actions = initGlobalState({
      user: null,
      theme: 'light',
      token: null
    });
  });

  it('应初始化默认状态', () => {
    let capturedState: any;
    actions.onGlobalStateChange((state) => {
      capturedState = state;
    }, true);
    expect(capturedState).toEqual({
      user: null,
      theme: 'light',
      token: null
    });
  });

  it('更新状态应触发监听回调', () => {
    const callback = vi.fn();
    actions.onGlobalStateChange(callback, true);

    actions.setGlobalState({ theme: 'dark' });

    expect(callback).toHaveBeenCalledWith(
      expect.objectContaining({ theme: 'dark' }),
      expect.objectContaining({ theme: 'light' })
    );
  });

  it('子应用发送的消息应能通过全局状态广播', () => {
    const subApp1Callback = vi.fn();
    const subApp2Callback = vi.fn();

    actions.onGlobalStateChange(subApp1Callback);
    actions.onGlobalStateChange(subApp2Callback);

    actions.setGlobalState({
      messages: [{
        from: 'app-react',
        to: 'app-vue',
        type: 'data-request',
        payload: { id: 123 },
        timestamp: Date.now()
      }]
    });

    expect(subApp1Callback).toHaveBeenCalled();
    expect(subApp2Callback).toHaveBeenCalled();
  });
});

4.3 事件总线集成测试

typescript 复制代码
// portal/src/__tests__/event-bus.test.ts
import { describe, it, expect, vi } from 'vitest';
import { MicroEventBus } from '@/shared/eventBus';

describe('MicroEventBus 集成测试', () => {
  const bus = new MicroEventBus();

  it('应在不同应用间传递事件', () => {
    const fromReact = vi.fn();
    const fromVue = vi.fn();

    bus.on('micro:user:login', fromReact);
    bus.on('micro:user:login', fromVue);

    bus.emit('micro:user:login', { userId: '001', name: '张三' });

    expect(fromReact).toHaveBeenCalledWith({ userId: '001', name: '张三' });
    expect(fromVue).toHaveBeenCalledWith({ userId: '001', name: '张三' });
  });

  it('取消订阅后不应再收到事件', () => {
    const callback = vi.fn();
    const unsubscribe = bus.on('micro:test', callback);

    unsubscribe();
    bus.emit('micro:test', { data: 'test' });

    expect(callback).not.toHaveBeenCalled();
  });

  it('事件源信息应正确传递', () => {
    const callback = vi.fn();
    bus.on('micro:navigate', callback);

    window.dispatchEvent(new CustomEvent('micro:navigate', {
      detail: {
        source: 'app-react',
        target: 'app-vue',
        path: '/detail/123',
        timestamp: Date.now()
      }
    }));

    expect(callback).toHaveBeenCalledWith(
      expect.objectContaining({
        source: 'app-react',
        path: '/detail/123'
      })
    );
  });
});

五、E2E 测试:全链路用户场景

5.1 测试环境搭建

bash 复制代码
# 安装 Playwright
pnpm add -D @playwright/test
npx playwright install chromium
typescript 复制代码
// playwright.config.ts
import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
  testDir: './e2e',
  fullyParallel: true,
  forbidOnly: !!process.env.CI,
  retries: process.env.CI ? 2 : 0,
  workers: process.env.CI ? 1 : undefined,
  reporter: [
    ['html'],
    ['json', { outputFile: 'test-results/e2e-results.json' }]
  ],

  use: {
    baseURL: 'http://localhost:8080',
    trace: 'on-first-retry',
    screenshot: 'only-on-failure',
    video: 'retain-on-failure'
  },

  // 启动所有微前端应用
  webServer: [
    {
      command: 'pnpm run dev:portal',
      port: 8080,
      reuseExistingServer: !process.env.CI
    },
    {
      command: 'pnpm run dev:react-app',
      port: 3001,
      reuseExistingServer: !process.env.CI
    },
    {
      command: 'pnpm run dev:vue-app',
      port: 3002,
      reuseExistingServer: !process.env.CI
    }
  ],

  projects: [
    {
      name: 'chromium',
      use: { ...devices['Desktop Chrome'] }
    }
  ]
});

5.2 全链路用户场景测试

typescript 复制代码
// e2e/user-journey.spec.ts
import { test, expect } from '@playwright/test';

test.describe('用户核心流程', () => {
  test.beforeEach(async ({ page }) => {
    // 每次测试前登录
    await page.goto('/login');
    await page.fill('[data-testid="username"]', 'admin');
    await page.fill('[data-testid="password"]', 'password123');
    await page.click('[data-testid="login-btn"]');
    await page.waitForURL('/dashboard');
  });

  test('完整用户流程:仪表盘 → React 子应用 → Vue 子应用', async ({ page }) => {
    // 1. 验证基座首页
    await expect(page.locator('.header-bar')).toBeVisible();
    await expect(page.locator('.sidebar')).toBeVisible();
    await expect(page.locator('[data-testid="welcome-message"]')).toContainText('欢迎回来');

    // 2. 导航到 React 子应用
    await page.click('[data-micro-app="app-react"]');
    await page.waitForSelector('#micro-app-container', { state: 'visible' });

    // 等待子应用加载
    await page.waitForSelector('[data-testid="react-app-loaded"]', { timeout: 10000 });
    await expect(page.locator('[data-testid="react-title"]')).toContainText('React 子应用');

    // 3. React 子应用内操作:按钮点击
    await page.click('[data-testid="react-notify-btn"]');
    await expect(page.locator('.el-message')).toContainText('通知已发送');

    // 4. 切换到 Vue 子应用
    await page.click('[data-micro-app="app-vue"]');
    await page.waitForSelector('[data-testid="vue-app-loaded"]', { timeout: 10000 });
    await expect(page.locator('[data-testid="vue-title"]')).toContainText('Vue 子应用');

    // 5. 验证跨应用通信:Vue 发送消息给基座
    await page.click('[data-testid="vue-send-to-main"]');
    await expect(page.locator('[data-testid="global-notification"]')).toContainText('来自 Vue 的消息');

    // 6. 验证主题切换
    await page.click('[data-testid="theme-toggle"]');
    await page.click('text=暗黑模式');
    await expect(page.locator('html')).toHaveClass(/dark/);

    // 7. 验证国际化
    await page.click('[data-testid="lang-toggle"]');
    await page.click('text=English');
    await expect(page.locator('[data-testid="dashboard-title"]')).toContainText('Dashboard');
  });

  test('子应用加载失败应显示降级 UI', async ({ page }) => {
    // 模拟子应用不可用
    await page.route('**/localhost:3001/**', route => {
      route.abort('connectionrefused');
    });

    await page.click('[data-micro-app="app-react"]');
    await page.waitForSelector('[data-testid="micro-fallback"]', { timeout: 10000 });
    await expect(page.locator('[data-testid="micro-fallback"]')).toContainText('子应用加载失败');
    await expect(page.locator('[data-testid="retry-btn"]')).toBeVisible();
  });

  test('无权限时访问子应用应被阻止', async ({ page }) => {
    // 使用只有 viewer 角色的用户
    await page.goto('/login');
    await page.fill('[data-testid="username"]', 'viewer');
    await page.fill('[data-testid="password"]', 'viewer123');
    await page.click('[data-testid="login-btn"]');

    // viewer 无权限访问 React 子应用
    await page.goto('/react/dashboard');
    await expect(page.locator('.error-boundary')).toContainText('无权限访问此应用');
  });
});

test.describe('性能与稳定性测试', () => {
  test('子应用切换应在 2 秒内完成', async ({ page }) => {
    await page.goto('/dashboard');

    const startTime = Date.now();
    await page.click('[data-micro-app="app-react"]');
    await page.waitForSelector('[data-testid="react-app-loaded"]', { timeout: 10000 });
    const elapsed = Date.now() - startTime;

    expect(elapsed).toBeLessThan(2000);
  });

  test('快速切换子应用不应导致页面崩溃', async ({ page }) => {
    await page.goto('/dashboard');

    // 快速多次切换
    for (let i = 0; i < 5; i++) {
      await page.click('[data-micro-app="app-react"]');
      await page.waitForTimeout(500);
      await page.click('[data-micro-app="app-vue"]');
      await page.waitForTimeout(500);
    }

    // 验证页面没有崩溃
    await expect(page.locator('.main-layout')).toBeVisible();
    const errors = await page.evaluate(() => {
      return (window as any).__testErrors__ || [];
    });
    expect(errors).toHaveLength(0);
  });
});

5.3 自定义测试工具

typescript 复制代码
// e2e/helpers/micro-app.ts
import { Page } from '@playwright/test';

export class MicroAppHelper {
  constructor(private page: Page) {}

  // 等待子应用加载完成
  async waitForMicroApp(appName: string, timeout = 10000) {
    await this.page.waitForFunction(
      (name: string) => {
        const el = document.querySelector(`[data-micro-app-loaded="${name}"]`);
        return !!el;
      },
      appName,
      { timeout }
    );
  }

  // 获取子应用内的文本
  async getMicroAppText(selector: string): Promise<string> {
    return this.page.evaluate((sel: string) => {
      const container = document.getElementById('micro-app-container');
      if (!container) return '';
      const el = container.querySelector(sel);
      return el?.textContent || '';
    }, selector);
  }

  // 在子应用内执行操作
  async actInMicroApp(selector: string, action: 'click' | 'hover') {
    await this.page.evaluate(
      ({ sel, act }: { sel: string; act: string }) => {
        const container = document.getElementById('micro-app-container');
        if (!container) return;
        const el = container.querySelector(sel) as HTMLElement;
        if (act === 'click') el?.click();
        else if (act === 'hover') el?.dispatchEvent(new MouseEvent('mouseenter'));
      },
      { sel: selector, act: action }
    );
  }

  // 获取子应用控制台日志
  async getMicroAppLogs(): Promise<string[]> {
    return this.page.evaluate(() => {
      return (window as any).__MICRO_APP_LOGS__ || [];
    });
  }

  // 检查子应用内存泄漏
  async checkMemoryLeaks(appName: string): Promise<boolean> {
    return this.page.evaluate((name: string) => {
      const detector = (window as any).__MEMORY_DETECTOR__;
      return detector?.checkUnmount(name);
    }, appName);
  }
}

六、契约测试:子应用接口契约

6.1 为什么需要契约测试

复制代码
┌─────────────────────────────────────────────────────────────┐
│                   无契约测试的问题                             │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  后端 /api/users 接口返回 { id, name, email }               │
│                                                             │
│  子应用 A 期望: { id, name, email }          兼容         │
│  子应用 B 期望: { id, name, avatar }         ❌ 字段缺失     │
│  子应用 C 期望: { userId, userName, email }  ❌ 字段名不同   │
│                                                             │
│  → 后端修改接口后,子应用 B/C 会出问题                       │
│  → 契约测试可以在变更时提前发现不兼容                         │
│                                                             │
└─────────────────────────────────────────────────────────────┘

6.2 使用 MSW 进行接口契约测试

typescript 复制代码
// src/__tests__/mocks/handlers.ts
import { http, HttpResponse } from 'msw';

export const handlers = [
  // 用户相关
  http.get('/api/user/info', () => {
    return HttpResponse.json({
      id: '001',
      name: '张三',
      email: 'zhangsan@company.com',
      avatar: '/avatars/001.png',
      roles: ['admin'],
      permissions: ['dashboard:view', 'report:export']
    });
  }),

  // 菜单接口
  http.get('/api/menus', () => {
    return HttpResponse.json([
      {
        path: '/dashboard',
        title: '工作台',
        icon: 'Odometer'
      },
      {
        path: '/react',
        title: 'React 应用',
        icon: 'Coin',
        microApp: 'app-react',
        permissions: ['micro-app:app-react']
      },
      {
        path: '/vue',
        title: 'Vue 应用',
        icon: 'Coin',
        microApp: 'app-vue',
        permissions: ['micro-app:app-vue']
      }
    ]);
  }),

  // 子应用配置接口
  http.get('/api/micro-apps/configs', () => {
    return HttpResponse.json([
      {
        name: 'app-react',
        entry: 'http://localhost:3001',
        activeRule: '/react',
        platform: 'react'
      },
      {
        name: 'app-vue',
        entry: 'http://localhost:3002',
        activeRule: '/vue',
        platform: 'vue'
      }
    ]);
  })
];
typescript 复制代码
// src/__tests__/contract/api-contract.test.ts
import { describe, it, expect } from 'vitest';
import { http, HttpResponse } from 'msw';
import { setupServer } from 'msw/node';
import { request } from '@/utils/request';

// 契约定义:接口返回的数据结构必须符合此形状
interface UserInfoContract {
  id: string;
  name: string;
  email: string;
  avatar?: string;
  roles: string[];
  permissions: string[];
}

interface MenuContract {
  path: string;
  title: string;
  icon?: string;
  children?: MenuContract[];
  microApp?: string;
  permissions?: string[];
}

interface MicroAppConfigContract {
  name: string;
  entry: string;
  activeRule: string;
  platform: 'react' | 'vue' | 'angular';
}

const server = setupServer();

describe('API 契约测试', () => {
  beforeAll(() => server.listen({ onUnhandledRequest: 'error' }));
  afterEach(() => server.resetHandlers());
  afterAll(() => server.close());

  it('用户信息接口必须满足契约', async () => {
    server.use(
      http.get('/api/user/info', () => {
        return HttpResponse.json({
          id: '001',
          name: '张三',
          email: 'zhangsan@company.com',
          roles: ['admin'],
          permissions: ['dashboard:view']
        });
      })
    );

    const data = await request.get<UserInfoContract>('/api/user/info');

    // 契约校验
    expect(data).toHaveProperty('id');
    expect(typeof data.id).toBe('string');
    expect(data).toHaveProperty('name');
    expect(typeof data.name).toBe('string');
    expect(data).toHaveProperty('email');
    expect(typeof data.email).toBe('string');
    expect(data).toHaveProperty('roles');
    expect(Array.isArray(data.roles)).toBe(true);
    expect(data).toHaveProperty('permissions');
    expect(Array.isArray(data.permissions)).toBe(true);
  });

  it('菜单接口必须满足契约', async () => {
    server.use(
      http.get('/api/menus', () => {
        return HttpResponse.json([
          { path: '/dashboard', title: '工作台' }
        ]);
      })
    );

    const data = await request.get<MenuContract[]>('/api/menus');

    data.forEach(menu => {
      expect(menu).toHaveProperty('path');
      expect(typeof menu.path).toBe('string');
      expect(menu).toHaveProperty('title');
      expect(typeof menu.title).toBe('string');
    });
  });

  it('子应用配置接口必须满足契约', async () => {
    // 模拟后端返回不符合契约的数据(缺失 platform 字段)
    server.use(
      http.get('/api/micro-apps/configs', () => {
        return HttpResponse.json([
          {
            name: 'app-react',
            entry: 'http://localhost:3001',
            activeRule: '/react'
            // 缺少 platform 字段
          }
        ]);
      })
    );

    const data = await request.get<MicroAppConfigContract[]>('/api/micro-apps/configs');

    // 契约校验应失败
    data.forEach(config => {
      const isValid = (
        typeof config.name === 'string' &&
        typeof config.entry === 'string' &&
        typeof config.activeRule === 'string' &&
        ['react', 'vue', 'angular'].includes(config.platform)
      );
      // platform 缺失,校验失败
      if (!config.platform) {
        console.warn('[Contract] 子应用配置缺少 platform 字段');
      }
    });
  });
});

6.3 使用 Pact 进行消费者驱动契约测试

typescript 复制代码
// pact/consumer/user-service.pact.test.ts
import { PactV3, MatchersV3 } from '@pact-foundation/pact';
import { describe, it, expect } from 'vitest';

const provider = new PactV3({
  consumer: 'portal-platform',
  provider: 'user-service',
  port: 9000
});

describe('User Service 契约', () => {
  it('获取用户信息接口应满足消费者期望', async () => {
    // 定义消费者期望的交互
    provider
      .given('用户已登录')
      .uponReceiving('获取用户信息请求')
      .withRequest({
        method: 'GET',
        path: '/api/user/info',
        headers: { Authorization: 'Bearer valid-token' }
      })
      .willRespondWith({
        status: 200,
        headers: { 'Content-Type': 'application/json' },
        body: {
          id: MatchersV3.string('001'),
          name: MatchersV3.string('张三'),
          email: MatchersV3.string('zhangsan@company.com'),
          roles: MatchersV3.eachLike('admin'),
          permissions: MatchersV3.eachLike('dashboard:view')
        }
      });

    await provider.executeTest(async (mockProvider) => {
      // 发送真实请求到 Mock 服务
      const response = await fetch(
        `${mockProvider.url}/api/user/info`,
        { headers: { Authorization: 'Bearer valid-token' } }
      );
      const data = await response.json();

      // 验证响应符合消费者期望
      expect(response.status).toBe(200);
      expect(data).toHaveProperty('id');
      expect(data).toHaveProperty('name');
      expect(data).toHaveProperty('email');
      expect(Array.isArray(data.roles)).toBe(true);
      expect(Array.isArray(data.permissions)).toBe(true);
    });
  });
});

七、测试基础设施与 Mock 策略

7.1 Mock 策略总览

复制代码
┌─────────────────────────────────────────────────────────────┐
│                    Mock 策略                                 │
├────────────┬──────────────────┬──────────────────────────────┤
│ 层级        │ Mock 方式         │ 适用场景                     │
├────────────┼──────────────────┼──────────────────────────────┤
│ 网络请求    │ MSW / Mock Axios │ API 调用、接口契约测试        │
│ 模块       │ vi.mock          │ 第三方库、工具函数            │
│ 组件       │ Stub 组件        │ 子组件、远程组件              │
│ 路由       │ createRouter     │ 路由跳转、导航守卫            │
│ Store      │ setActivePinia   │ Pinia Store 测试             │
│ qiankun    │ 模拟生命周期      │ 微前端集成测试                │
│ 浏览器 API │ jsdom / happy-dom │ DOM 操作、Storage 等         │
│ 时间       │ vi.useFakeTimers │ 定时器、debounce 等          │
└────────────┴──────────────────┴──────────────────────────────┘

7.2 Module Federation 模块 Mock

typescript 复制代码
// src/__tests__/mocks/remote-modules.ts
import { vi } from 'vitest';

// Mock Module Federation 远程模块
vi.mock('remoteApp/Button', () => ({
  default: ({ label, onClick }: { label: string; onClick?: () => void }) => {
    return {
      type: 'button',
      props: { children: label, onClick },
      $$typeof: Symbol.for('react.element')
    };
  }
}));

vi.mock('remoteApp/Header', () => ({
  default: ({ title }: { title: string }) => {
    return {
      type: 'header',
      props: { children: title },
      $$typeof: Symbol.for('react.element')
    };
  }
}));

// 动态获取远程模块的工厂函数 Mock
export function createRemoteModuleMock<T>(defaultValue: T) {
  let currentValue = defaultValue;

  return {
    get: () => currentValue,
    set: (value: T) => { currentValue = value; },
    reset: () => { currentValue = defaultValue; }
  };
}

7.3 qiankun 沙箱 Mock

typescript 复制代码
// src/__tests__/mocks/qiankun-sandbox.ts
import { vi } from 'vitest';

// 模拟 qiankun 沙箱环境
export function setupQiankunSandboxMock() {
  const originalWindow = { ...window };

  // 保存原始方法
  const originalAddEventListener = window.addEventListener;
  const originalRemoveEventListener = window.removeEventListener;

  // 沙箱模拟
  const sandboxMock = {
    proxyWindow: new Proxy(window, {
      get(target: any, prop: string) {
        if (prop === '__POWERED_BY_QIANKUN__') return true;
        if (prop === '__INJECTED_PUBLIC_PATH_BY_QIANKUN__') return 'http://localhost:3001/';
        return target[prop];
      },
      set(target: any, prop: string, value: any) {
        // 模拟沙箱内的全局变量隔离
        if (prop.startsWith('__')) return true;
        target[prop] = value;
        return true;
      }
    }),

    // 模拟沙箱内的事件监听隔离
    eventListeners: new Map<string, Set<EventListener>>(),

    addEventListener(type: string, listener: EventListener) {
      if (!this.eventListeners.has(type)) {
        this.eventListeners.set(type, new Set());
      }
      this.eventListeners.get(type)!.add(listener);
    },

    removeEventListener(type: string, listener: EventListener) {
      this.eventListeners.get(type)?.delete(listener);
    },

    // 清理沙箱
    destroy() {
      this.eventListeners.clear();
      // 恢复原始 window
      Object.assign(window, originalWindow);
    }
  };

  return sandboxMock;
}

7.4 测试夹具(Fixtures)

typescript 复制代码
// src/__tests__/fixtures/micro-app.fixtures.ts

// 子应用配置夹具
export const microAppConfigFixture = {
  name: 'app-react',
  entry: 'http://localhost:3001',
  container: '#micro-app-container',
  activeRule: '/react',
  props: {
    appName: 'app-react',
    platform: 'react',
    token: 'mock-token',
    user: {
      id: '001',
      name: '张三',
      roles: ['admin'],
      permissions: ['*']
    }
  }
};

// 全局状态夹具
export const globalStateFixture = {
  user: {
    id: '001',
    name: '张三',
    avatar: '/avatars/001.png',
    permissions: ['dashboard:view', 'report:export']
  },
  theme: 'light',
  token: 'mock-token',
  messages: []
};

// 用户信息夹具
export const userInfoFixture = {
  id: '001',
  name: '张三',
  email: 'zhangsan@company.com',
  avatar: '/avatars/001.png',
  roles: ['admin'],
  permissions: [
    'dashboard:view',
    'report:export',
    'user:manage',
    'system:config',
    'micro-app:app-react',
    'micro-app:app-vue'
  ]
};

// 菜单夹具
export const menuFixture = [
  {
    path: '/dashboard',
    title: '工作台',
    icon: 'Odometer'
  },
  {
    path: '/react',
    title: 'React 应用',
    icon: 'Coin',
    microApp: 'app-react',
    permissions: ['micro-app:app-react']
  },
  {
    path: '/vue',
    title: 'Vue 应用',
    icon: 'Coin',
    microApp: 'app-vue',
    permissions: ['micro-app:app-vue']
  },
  {
    path: '/system',
    title: '系统管理',
    icon: 'Setting',
    children: [
      {
        path: '/system/users',
        title: '用户管理',
        permissions: ['user:manage']
      },
      {
        path: '/system/config',
        title: '系统配置',
        permissions: ['system:config']
      }
    ]
  }
];

八、质量门禁与 CI 集成

8.1 质量门禁流程

复制代码
                    ┌──────────────────────┐
                    │  开发者提交代码        │
                    └──────────┬───────────┘
                               │
                    ┌──────────▼───────────┐
                    │   Pre-commit Hook     │
                    │  ┌────────────────┐  │
                    │  │  ESLint        │  │
                    │  │  Prettier      │  │
                    │  │  TypeScript    │  │
                    │  └────────────────┘  │
                    └──────────┬───────────┘
                               │
                    ┌──────────▼───────────┐
                    │   单元测试 + 组件测试 │
                    │   ┌───────────────┐  │
                    │   │ 覆盖率 ≥ 80%  │  │
                    │   └───────────────┘  │
                    └──────────┬───────────┘
                               │
                    ┌──────────▼───────────┐
                    │   CI Pipeline         │
                    │  ┌─────────────────┐ │
                    │  │ Lint & Type     │ │
                    │  │ 单元测试        │ │
                    │  │ 契约测试        │ │
                    │  │ 构建验证        │ │
                    │  └─────────────────┘ │
                    └──────────┬───────────┘
                               │
                    ┌──────────▼───────────┐
                    │   集成测试(每日)    │
                    │   ┌───────────────┐  │
                    │   │ 跨应用交互    │  │
                    │   │ 通信机制      │  │
                    │   └───────────────┘  │
                    └──────────┬───────────┘
                               │
                    ┌──────────▼───────────┐
                    │   E2E 测试(预发布)  │
                    │   ┌───────────────┐  │
                    │   │ 全链路场景    │  │
                    │   │ 性能基线      │  │
                    │   └───────────────┘  │
                    └──────────┬───────────┘
                               │
                    ┌──────────▼───────────┐
                    │   生产发布            │
                    │   灰度 10% → 50% →100%│
                    └──────────────────────┘

8.2 CI 配置

yaml 复制代码
# .github/workflows/test.yml
name: Micro Frontend CI

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]

jobs:
  quality-gate:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        app: [portal, app-react, app-vue]

    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'pnpm'

      - name: Install dependencies
        run: |
          cd apps/${{ matrix.app }}
          pnpm install --frozen-lockfile

      - name: Lint & Type Check
        run: |
          cd apps/${{ matrix.app }}
          pnpm lint
          pnpm typecheck

      - name: Unit Tests with Coverage
        run: |
          cd apps/${{ matrix.app }}
          pnpm test:unit --coverage

      - name: Check Coverage Threshold
        run: |
          cd apps/${{ matrix.app }}
          pnpm test:coverage-check

      - name: Build
        run: |
          cd apps/${{ matrix.app }}
          pnpm build

  contract-test:
    runs-on: ubuntu-latest
    needs: quality-gate
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4

      - name: Install dependencies
        run: pnpm install

      - name: Contract Tests
        run: pnpm test:contract

      - name: Publish Pact
        if: github.ref == 'refs/heads/main'
        run: pnpm pact:publish
        env:
          PACT_BROKER_TOKEN: ${{ secrets.PACT_BROKER_TOKEN }}

  integration-test:
    runs-on: ubuntu-latest
    needs: [quality-gate, contract-test]
    if: github.ref == 'refs/heads/main' || github.event_name == 'schedule'

    services:
      portal:
        image: portal-platform:latest
        ports:
          - 8080:80
      react-app:
        image: react-app:latest
        ports:
          - 3001:80
      vue-app:
        image: vue-app:latest
        ports:
          - 3002:80

    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4

      - name: Integration Tests
        run: pnpm test:integration

  e2e-test:
    runs-on: ubuntu-latest
    needs: integration-test
    if: github.event_name == 'schedule' || startsWith(github.ref, 'refs/tags/')

    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4

      - name: Install dependencies
        run: pnpm install

      - name: Install Playwright
        run: npx playwright install chromium

      - name: Start all apps
        run: |
          pnpm dev:portal &
          pnpm dev:react-app &
          pnpm dev:vue-app &
          npx wait-on http://localhost:8080 http://localhost:3001 http://localhost:3002

      - name: Run E2E Tests
        run: pnpm test:e2e

      - name: Upload Test Results
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: e2e-results
          path: test-results/

8.3 覆盖率配置

typescript 复制代码
// vitest.config.ts(平台级)
import { defineConfig } from 'vitest/config';

export default defineConfig({
  test: {
    coverage: {
      // 各应用覆盖率阈值(硬性门禁)
      thresholds: {
        // 基座应用
        'apps/portal/src': {
          statements: 85,
          branches: 75,
          functions: 85,
          lines: 85
        },
        // 共享组件库
        'packages/ui/src': {
          statements: 90,
          branches: 80,
          functions: 90,
          lines: 90
        },
        // 子应用
        'apps/app-react/src': {
          statements: 80,
          branches: 70,
          functions: 80,
          lines: 80
        },
        'apps/app-vue/src': {
          statements: 80,
          branches: 70,
          functions: 80,
          lines: 80
        }
      },

      // 排除无需覆盖的文件
      exclude: [
        '**/*.d.ts',
        '**/*.config.*',
        '**/main.ts',
        '**/public-path.ts',
        '**/env.d.ts',
        '**/__tests__/**',
        '**/node_modules/**'
      ]
    }
  }
});

8.4 测试报告与可视化

typescript 复制代码
// scripts/generate-test-report.ts
// 合并所有子应用的测试报告,生成统一看板

interface TestReport {
  appName: string;
  totalTests: number;
  passed: number;
  failed: number;
  skipped: number;
  coverage: {
    statements: number;
    branches: number;
    functions: number;
    lines: number;
  };
  duration: number;
}

async function generateUnifiedReport() {
  const apps = ['portal', 'app-react', 'app-vue', 'app-angular'];
  const reports: TestReport[] = [];

  for (const app of apps) {
    const reportPath = `apps/${app}/test-results/report.json`;
    try {
      const report = await import(reportPath);
      reports.push(report);
    } catch {
      console.warn(`未找到 ${app} 的测试报告`);
    }
  }

  // 生成总览
  const total = {
    totalTests: reports.reduce((s, r) => s + r.totalTests, 0),
    passed: reports.reduce((s, r) => s + r.passed, 0),
    failed: reports.reduce((s, r) => s + r.failed, 0),
    avgCoverage: reports.reduce((s, r) => s + r.coverage.lines, 0) / reports.length
  };

  console.table(total);

  // 输出 Markdown 格式报告
  const markdown = [
    '# 微前端测试报告',
    '',
    '| 应用 | 通过率 | 行覆盖率 | 耗时 |',
    '|------|--------|---------|------|',
    ...reports.map(r =>
      `| ${r.appName} | ${(r.passed / r.totalTests * 100).toFixed(1)}% | ${r.coverage.lines.toFixed(1)}% | ${r.duration}ms |`
    ),
    '',
    `**总测试数**: ${total.totalTests}`,
    `**通过率**: ${(total.passed / total.totalTests * 100).toFixed(1)}%`,
    `**平均覆盖率**: ${total.avgCoverage.toFixed(1)}%`
  ].join('\n');

  return markdown;
}

通过本文,你已经构建了微前端架构下的完整质量保障体系:

  • 单元测试:Store、工具函数、Composables
  • 组件测试:Vue/React 组件、共享组件
  • 集成测试:qiankun 沙箱、全局状态、事件总线
  • E2E 测试:全链路用户场景、性能基线
  • 契约测试:API 接口契约、消费者驱动测试
  • Mock 策略:远程模块、沙箱环境
  • 质量门禁与 CI 集成
相关推荐
anOnion5 小时前
构建无障碍组件之Menu Button pattern
前端·html·交互设计
用户47949283569155 小时前
claude Fable用不了?把Gpt 5.5pro接到你的claude code里
前端·后端
zhangxingchao8 小时前
Kotlin常用的Flow 操作符整理
前端
IT_陈寒9 小时前
React的useState居然还有这种坑?我差点删库跑路
前端·人工智能·后端
Pedantic10 小时前
SwiftUI 手势笔记
前端·后端
橙子家11 小时前
浏览器缓存之【结构化数据库与缓存】: IndexedDB、Cache storage 和 Storage buckets
前端
user205855615181311 小时前
X6 中边悬浮置顶,规避 `mouseleave` 事件丢失问题
前端
李明卫杭州11 小时前
CSS aspect-ratio 属性完全指南
前端
Pedantic13 小时前
SwiftUI 手势层级(Gesture Hierarchy)详解
前端