微前端进阶(三)

微前端进阶(三)

从单元测试到 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 集成
相关推荐
红辣椒...1 小时前
codex+第三方模型
java·服务器·前端
木子雨廷1 小时前
Flutter 使用 flutter_flavorizr 多渠道打包
前端·flutter
环境工程笔记1 小时前
浏览器自动化跑成功了,为什么结果还是不对?
前端
东风破_1 小时前
一文搞懂 JavaScript 变量声明:var、let、const 到底有什么区别?
前端·javascript
问心无愧05131 小时前
ctf show web入门261
android·前端·笔记
触底反弹1 小时前
你真的理解 JavaScript 变量提升(Hoisting)吗?从 V8 引擎编译原理深入剖析
前端·面试
蜡台2 小时前
Vue2 使用 typescript 教程
前端·vue.js·typescript
光影少年2 小时前
Redux Toolkit 用法、解决原生Redux 冗余问题
开发语言·前端·javascript·react.js·中间件·前端框架·ecmascript
云水一下2 小时前
JavaScript 从零基础到精通系列:DOM 操作与事件驱动编程
前端·javascript