Skip to content
On this page

qiankun 测试策略

测试概述

qiankun 微前端架构的测试需要覆盖多个层面:单元测试、集成测试、端到端测试以及微应用间的通信测试。由于涉及多个独立的应用,测试策略需要特别关注隔离性和集成性。

单元测试

1. 主应用单元测试

qiankun 配置测试

javascript
// main-app.spec.js
import { registerMicroApps, start } from 'qiankun';
import { setupQiankun, getMicroAppsConfig } from './qiankun-config';

// 模拟 qiankun API
jest.mock('qiankun', () => ({
  registerMicroApps: jest.fn(),
  start: jest.fn(),
  initGlobalState: jest.fn(() => ({
    onGlobalStateChange: jest.fn(),
    setGlobalState: jest.fn(),
    getGlobalState: jest.fn()
  }))
}));

describe('qiankun configuration', () => {
  beforeEach(() => {
    jest.clearAllMocks();
  });

  test('should register micro apps with correct configuration', () => {
    const mockApps = [
      {
        name: 'user-center',
        entry: '//localhost:3001',
        container: '#container',
        activeRule: '/user'
      }
    ];
    
    setupQiankun(mockApps);
    
    expect(registerMicroApps).toHaveBeenCalledWith(
      expect.arrayContaining([
        expect.objectContaining({
          name: 'user-center',
          entry: '//localhost:3001'
        })
      ]),
      expect.any(Object) // 生命周期钩子配置
    );
  });

  test('should start qiankun with correct options', () => {
    setupQiankun([]);
    
    expect(start).toHaveBeenCalledWith(
      expect.objectContaining({
        prefetch: true,
        sandbox: expect.objectContaining({
          strictStyleIsolation: true
        })
      })
    );
  });
});

微应用注册逻辑测试

javascript
// micro-app-registration.spec.js
import { registerMicroApps } from 'qiankun';
import { registerMicroAppsSecurely } from './micro-app-registration';

describe('secure micro app registration', () => {
  const mockApps = [
    {
      name: 'test-app',
      entry: '//localhost:3001',
      container: '#container',
      activeRule: '/test'
    }
  ];

  test('should validate app entries before registration', () => {
    const isValidEntry = jest.fn().mockReturnValue(true);
    
    registerMicroAppsSecurely(mockApps, { isValidEntry });
    
    expect(isValidEntry).toHaveBeenCalledWith('//localhost:3001');
    expect(registerMicroApps).toHaveBeenCalled();
  });

  test('should filter out invalid apps', () => {
    const isValidEntry = jest.fn()
      .mockReturnValueOnce(false)  // 第一个应用无效
      .mockReturnValueOnce(true);  // 第二个应用有效
    
    const apps = [
      { name: 'invalid-app', entry: 'invalid://url', container: '#container', activeRule: '/invalid' },
      { name: 'valid-app', entry: '//localhost:3001', container: '#container', activeRule: '/valid' }
    ];
    
    registerMicroAppsSecurely(apps, { isValidEntry });
    
    expect(registerMicroApps).toHaveBeenCalledWith(
      expect.arrayContaining([
        expect.objectContaining({ name: 'valid-app' })
      ])
    );
    expect(registerMicroApps).not.toHaveBeenCalledWith(
      expect.arrayContaining([
        expect.objectContaining({ name: 'invalid-app' })
      ])
    );
  });
});

2. 微应用单元测试

生命周期函数测试

javascript
// user-center/__tests__/lifecycles.spec.js
import { bootstrap, mount, unmount } from '../src/lifecycles';
import { createApp } from 'vue';

// 模拟 Vue
jest.mock('vue', () => ({
  createApp: jest.fn(() => ({
    use: jest.fn().mockReturnThis(),
    mount: jest.fn(),
    unmount: jest.fn()
  }))
}));

describe('micro app lifecycles', () => {
  const mockProps = {
    container: { querySelector: jest.fn().mockReturnValue(document.createElement('div')) },
    name: 'user-center'
  };

  beforeEach(() => {
    jest.clearAllMocks();
  });

  test('bootstrap should log app info', async () => {
    const consoleSpy = jest.spyOn(console, 'log').mockImplementation();
    
    await bootstrap(mockProps);
    
    expect(consoleSpy).toHaveBeenCalledWith('User center app bootstrap', mockProps);
    consoleSpy.mockRestore();
  });

  test('mount should create and mount Vue app', async () => {
    await mount(mockProps);
    
    expect(createApp).toHaveBeenCalled();
    // 验证应用被挂载到正确的容器
    expect(mockProps.container.querySelector).toHaveBeenCalledWith('#app');
  });

  test('unmount should properly destroy app', async () => {
    const appInstance = {
      unmount: jest.fn(),
      $destroy: jest.fn(),
      $el: { innerHTML: 'test' }
    };
    
    // 模拟全局应用实例
    window.__USER_CENTER_APP__ = appInstance;
    
    await unmount(mockProps);
    
    expect(appInstance.unmount).toHaveBeenCalled();
    expect(window.__USER_CENTER_APP__).toBeUndefined();
  });
});

微应用内部组件测试

javascript
// user-center/__tests__/components/UserProfile.spec.js
import { mount } from '@vue/test-utils';
import UserProfile from '../../src/components/UserProfile.vue';

describe('UserProfile component', () => {
  test('should display user information correctly', () => {
    const wrapper = mount(UserProfile, {
      props: {
        user: {
          id: 1,
          name: 'John Doe',
          email: 'john@example.com'
        }
      }
    });

    expect(wrapper.text()).toContain('John Doe');
    expect(wrapper.text()).toContain('john@example.com');
  });

  test('should emit update event when user data changes', async () => {
    const wrapper = mount(UserProfile, {
      props: {
        user: { name: 'John' }
      }
    });

    const input = wrapper.find('input[name="name"]');
    await input.setValue('Jane');
    await input.trigger('input');

    expect(wrapper.emitted('update:user')).toBeTruthy();
  });
});

集成测试

1. 主应用与微应用通信测试

全局状态通信测试

javascript
// integration/__tests__/global-state.spec.js
import { initGlobalState } from 'qiankun';

// 模拟全局状态管理
jest.mock('qiankun', () => ({
  initGlobalState: jest.fn()
}));

describe('global state communication', () => {
  let mockSetGlobalState, mockOnGlobalStateChange;

  beforeEach(() => {
    mockSetGlobalState = jest.fn();
    mockOnGlobalStateChange = jest.fn();
    
    initGlobalState.mockReturnValue({
      setGlobalState: mockSetGlobalState,
      onGlobalStateChange: mockOnGlobalStateChange,
      getGlobalState: jest.fn()
    });
  });

  test('should synchronize user data between main and micro apps', () => {
    const { setGlobalState, onGlobalStateChange } = initGlobalState({
      user: null
    });

    // 模拟主应用设置用户信息
    setGlobalState({ user: { id: 1, name: 'Admin' } });

    // 验证微应用能接收到状态变化
    const stateChangeCallback = jest.fn();
    onGlobalStateChange(stateChangeCallback);

    // 触发状态变化
    setGlobalState({ user: { id: 1, name: 'Updated Admin' } });

    expect(stateChangeCallback).toHaveBeenCalledWith(
      { user: { id: 1, name: 'Updated Admin' } },
      { user: { id: 1, name: 'Admin' } }
    );
  });
});

微应用间通信测试

javascript
// integration/__tests__/app-communication.spec.js
describe('micro app communication', () => {
  test('should handle cross-app data transfer', async () => {
    // 模拟两个微应用的通信场景
    const mockPropsForAppA = {
      onGlobalStateChange: jest.fn(),
      setGlobalState: jest.fn(),
      container: { querySelector: () => document.createElement('div') }
    };

    const mockPropsForAppB = {
      onGlobalStateChange: jest.fn(),
      setGlobalState: jest.fn(),
      container: { querySelector: () => document.createElement('div') }
    };

    // App A 设置数据
    mockPropsForAppA.setGlobalState({ sharedData: 'from-app-a' });

    // App B 监听数据变化
    let receivedData = null;
    mockPropsForAppB.onGlobalStateChange((state) => {
      receivedData = state.sharedData;
    });

    // 触发状态更新
    mockPropsForAppA.setGlobalState({ sharedData: 'updated-from-app-a' });

    // 验证 App B 接收到了更新
    expect(receivedData).toBe('updated-from-app-a');
  });
});

2. 路由集成测试

主应用路由测试

javascript
// integration/__tests__/routing.spec.js
import { createRouter, createWebHistory } from 'vue-router';
import { registerMicroApps } from 'qiankun';

describe('routing integration', () => {
  test('should activate correct micro app based on route', () => {
    const apps = [
      {
        name: 'user-center',
        entry: '//localhost:3001',
        container: '#container',
        activeRule: '/user'
      },
      {
        name: 'product-center',
        entry: '//localhost:3002',
        container: '#container',
        activeRule: '/product'
      }
    ];

    // 模拟路由变化
    const location = { pathname: '/user/profile' };
    
    // 验证正确的应用被激活
    const activeApp = apps.find(app => 
      typeof app.activeRule === 'function' 
        ? app.activeRule(location) 
        : location.pathname.startsWith(app.activeRule)
    );

    expect(activeApp.name).toBe('user-center');
  });

  test('should handle route transitions between micro apps', async () => {
    const transitionSpy = jest.fn();
    
    // 模拟路由跳转
    const fromRoute = '/user';
    const toRoute = '/product';
    
    // 验证微应用正确卸载和加载
    const { mount, unmount } = await import('../src/user-center/lifecycles');
    const productLifecycles = await import('../src/product-center/lifecycles');
    
    // 模拟从用户中心切换到产品中心
    await unmount({ container: document.createElement('div') });
    await productLifecycles.mount({ 
      container: document.createElement('div') 
    });
    
    // 验证切换逻辑正确执行
    expect(transitionSpy).toHaveBeenCalledTimes(2); // 卸载 + 挂载
  });
});

端到端测试

1. 微应用加载测试

使用 Cypress 的 E2E 测试

javascript
// cypress/e2e/micro-app-loading.cy.js
describe('Micro App Loading', () => {
  beforeEach(() => {
    cy.visit('http://localhost:8080');
  });

  it('should load user center micro app', () => {
    // 访问用户中心路由
    cy.visit('/user');
    
    // 验证微应用容器存在
    cy.get('#user-center-container').should('exist');
    
    // 验证微应用内容加载
    cy.get('#user-center-container').within(() => {
      cy.get('[data-testid="user-profile"]').should('be.visible');
      cy.get('[data-testid="user-nav"]').should('be.visible');
    });
    
    // 验证微应用功能正常
    cy.get('[data-testid="user-settings-btn"]').click();
    cy.get('[data-testid="settings-modal"]').should('be.visible');
  });

  it('should switch between micro apps correctly', () => {
    // 访问第一个微应用
    cy.visit('/user');
    cy.get('#user-center-container').should('exist');
    
    // 切换到第二个微应用
    cy.visit('/product');
    cy.get('#product-container').should('exist');
    
    // 验证第一个微应用已卸载
    cy.get('#user-center-container').should('not.exist');
    
    // 验证第二个微应用已加载
    cy.get('#product-container').should('be.visible');
  });
});

使用 Playwright 的 E2E 测试

javascript
// tests/micro-app.spec.js
import { test, expect } from '@playwright/test';

test.describe('Micro App Integration', () => {
  test('should load and interact with micro app', async ({ page }) => {
    await page.goto('http://localhost:8080/user');
    
    // 等待微应用加载
    await page.waitForSelector('[data-testid="user-profile"]');
    
    // 验证微应用内容
    await expect(page.locator('[data-testid="user-name"]')).toBeVisible();
    
    // 测试微应用交互
    await page.click('[data-testid="edit-profile"]');
    await page.fill('input[name="name"]', 'Test User');
    await page.click('[data-testid="save-btn"]');
    
    // 验证交互结果
    await expect(page.locator('[data-testid="user-name"]')).toContainText('Test User');
  });

  test('should handle micro app errors gracefully', async ({ page }) => {
    // 模拟微应用加载失败的场景
    await page.route('**/micro-app-entry.js', route => {
      route.abort();
    });
    
    await page.goto('http://localhost:8080/failed-app');
    
    // 验证错误处理
    await expect(page.locator('[data-testid="error-boundary"]')).toBeVisible();
    await expect(page.locator('[data-testid="error-message"]')).toContainText('Failed to load');
  });
});

2. 通信功能 E2E 测试

javascript
// cypress/e2e/communication.cy.js
describe('Micro App Communication', () => {
  it('should sync user data across micro apps', () => {
    cy.visit('/user');
    
    // 在用户中心更新用户信息
    cy.get('[data-testid="user-name-input"]').type('New Name');
    cy.get('[data-testid="save-user"]').click();
    
    // 切换到另一个微应用
    cy.visit('/dashboard');
    
    // 验证用户信息已同步
    cy.get('[data-testid="displayed-user-name"]').should('contain', 'New Name');
  });

  it('should handle global state changes', () => {
    cy.visit('/user');
    
    // 触发全局状态更新
    cy.window().then(win => {
      win.__TEST_GLOBAL_STATE__.set({ theme: 'dark' });
    });
    
    // 验证其他微应用接收到状态变化
    cy.visit('/product');
    cy.get('body').should('have.class', 'theme-dark');
  });
});

性能测试

1. 加载性能测试

javascript
// performance/__tests__/load-time.spec.js
describe('Micro App Performance', () => {
  test('should load within acceptable time limits', async () => {
    const startTime = performance.now();
    
    // 模拟微应用加载
    await loadMicroApp({
      name: 'test-app',
      entry: '//localhost:3001',
      container: '#container'
    });
    
    const loadTime = performance.now() - startTime;
    
    // 验证加载时间在可接受范围内(例如:小于 3 秒)
    expect(loadTime).toBeLessThan(3000);
  });

  test('should handle concurrent app loading', async () => {
    const startTime = performance.now();
    
    // 并发加载多个微应用
    const apps = [
      loadMicroApp({ name: 'app1', entry: '//localhost:3001', container: '#c1' }),
      loadMicroApp({ name: 'app2', entry: '//localhost:3002', container: '#c2' }),
      loadMicroApp({ name: 'app3', entry: '//localhost:3003', container: '#c3' })
    ];
    
    await Promise.all(apps);
    
    const totalTime = performance.now() - startTime;
    
    // 验证并发加载时间合理
    expect(totalTime).toBeLessThan(5000); // 所有应用在 5 秒内加载完成
  });
});

2. 内存使用测试

javascript
// performance/__tests__/memory.spec.js
describe('Memory Usage', () => {
  test('should clean up memory on app unmount', async () => {
    if (performance.memory) {
      const initialMemory = performance.memory.usedJSHeapSize;
      
      // 加载微应用
      const app = await loadMicroApp({
        name: 'test-app',
        entry: '//localhost:3001',
        container: '#container'
      });
      
      const afterLoadMemory = performance.memory.usedJSHeapSize;
      
      // 卸载微应用
      await app.unmount();
      
      const afterUnmountMemory = performance.memory.usedJSHeapSize;
      
      // 验证内存使用量在卸载后减少
      expect(afterUnmountMemory).toBeLessThan(afterLoadMemory);
      // 并且接近初始内存使用量(允许一定的误差)
      expect(afterUnmountMemory).toBeLessThan(initialMemory + 1024 * 1024); // 1MB 误差
    }
  });
});

测试工具和框架

1. 测试工具配置

Jest 配置

javascript
// jest.config.js
module.exports = {
  testEnvironment: 'jsdom',
  setupFilesAfterEnv: ['<rootDir>/tests/setup.js'],
  moduleNameMapping: {
    '^@/(.*)$': '<rootDir>/src/$1',
  },
  testMatch: [
    '**/__tests__/**/*.spec.js',
    '**/__tests__/**/*.test.js',
  ],
  collectCoverageFrom: [
    'src/**/*.{js,vue}',
    '!src/**/*.spec.js',
    '!src/main.js',
  ],
  coverageThreshold: {
    global: {
      branches: 80,
      functions: 80,
      lines: 80,
      statements: 80,
    },
  },
};

测试工具设置

javascript
// tests/setup.js
// 模拟 qiankun 环境
Object.defineProperty(window, '__POWERED_BY_QIANKUN__', {
  value: true,
  writable: true,
  configurable: true,
});

// 模拟 fetch
global.fetch = jest.fn(() =>
  Promise.resolve({
    ok: true,
    status: 200,
    json: () => Promise.resolve({}),
    text: () => Promise.resolve(''),
  })
);

// 模拟 Proxy
global.Proxy = function(target, handler) {
  return new (require('harmony-proxy'))(target, handler);
};

2. Mock 工具

qiankun Mock

javascript
// tests/mocks/qiankun.js
export const mockQiankun = () => {
  const apps = new Map();
  
  return {
    registerMicroApps: jest.fn((appConfigs, lifecycle) => {
      appConfigs.forEach(app => {
        apps.set(app.name, {
          ...app,
          lifecycle,
          loaded: false
        });
      });
    }),
    
    start: jest.fn(() => {
      // 模拟启动逻辑
    }),
    
    loadMicroApp: jest.fn(async (appConfig) => {
      const app = {
        ...appConfig,
        mount: jest.fn(),
        unmount: jest.fn(),
        getStatus: () => 'MOUNTED'
      };
      
      await app.mount();
      return app;
    }),
    
    initGlobalState: jest.fn((initialState) => {
      let state = { ...initialState };
      const listeners = [];
      
      return {
        onGlobalStateChange: jest.fn((callback, fireImmediately = false) => {
          listeners.push(callback);
          if (fireImmediately) {
            callback(state, state);
          }
        }),
        setGlobalState: jest.fn((newState) => {
          const oldState = { ...state };
          state = { ...state, ...newState };
          listeners.forEach(listener => listener(state, oldState));
        }),
        getGlobalState: jest.fn(() => state)
      };
    }),
    
    getApps: () => Array.from(apps.values())
  };
};

测试最佳实践

1. 测试策略

javascript
// tests/best-practices.spec.js
describe('qiankun Testing Best Practices', () => {
  test('should test isolation between micro apps', () => {
    // 验证一个微应用的错误不会影响其他微应用
    const appA = createMockMicroApp('app-a');
    const appB = createMockMicroApp('app-b');
    
    // 当 appA 出错时,appB 应该正常工作
    appA.throwError();
    
    expect(() => appB.normalOperation()).not.toThrow();
  });

  test('should test communication contracts', () => {
    // 验证微应用间通信的数据格式
    const expectedContract = {
      user: {
        id: expect.any(Number),
        name: expect.any(String),
        email: expect.stringMatching(/.+@.+\..+/)
      }
    };
    
    const actualData = getUserDataFromMicroApp();
    expect(actualData).toMatchObject(expectedContract);
  });

  test('should test error boundaries', () => {
    // 验证错误边界正常工作
    const wrapper = mountWithErrorBoundary();
    
    // 模拟子组件错误
    wrapper.vm.triggerChildError();
    
    // 验证错误边界显示了错误内容
    expect(wrapper.find('[data-testid="error-fallback"]')).toBeTruthy();
  });
});

2. CI/CD 集成

yaml
# .github/workflows/test.yml
name: Tests
on: [push, pull_request]

jobs:
  unit-tests:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-node@v3
        with:
          node-version: '16'
      - run: npm ci
      - run: npm run test:unit
      - name: Upload coverage to Codecov
        uses: codecov/codecov-action@v3

  integration-tests:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-node@v3
        with:
          node-version: '16'
      - run: npm ci
      - run: npm run test:integration
      - name: Archive integration test results
        uses: actions/upload-artifact@v3
        with:
          name: integration-test-results
          path: reports/integration/

  e2e-tests:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-node@v3
        with:
          node-version: '16'
      - name: Install dependencies
        run: npm ci
      - name: Install Playwright
        run: npx playwright install --with-deps
      - name: Run E2E tests
        run: npm run test:e2e
      - name: Upload E2E test results
        if: always()
        uses: actions/upload-artifact@v3
        with:
          name: e2e-test-results
          path: test-results/

通过这些全面的测试策略,可以确保 qiankun 微前端应用的质量和稳定性。