Appearance
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 微前端应用的质量和稳定性。