软件质量保障离不开系统化的测试与持续交付。微信小程序作为运行在微信客户端内的应用,其测试与部署流程既有与传统 Web/App 共通的测试方法论,也有因为平台封闭性带来的特殊挑战。本文将从小程序特有的测试工具链出发,系统讲解单元测试、端到端自动化测试、微信官方 CI 服务以及与第三方 CI/CD 平台的集成方案,帮助团队建立起可靠的质量门禁与交付流水线。
一、小程序测试体系概览
1.1 测试金字塔
小程序测试遵循经典的测试金字塔模型,从底层到顶层依次为:
| 层级 | 测试类型 | 工具/框架 | 运行速度 | 维护成本 |
|---|---|---|---|---|
| 底层 | 单元测试 | Jest + miniprogram-simulate | 极快 | 低 |
| 中层 | 集成测试 | Jest + 云函数本地测试 | 快 | 中 |
| 上层 | E2E 测试 | miniprogram-automator | 慢 | 高 |
| 顶层 | 手动测试 | 真机 + 开发者工具 | 最慢 | 最高 |
底层单元测试数量应最多,聚焦于单个函数或组件的逻辑正确性;中层集成测试验证模块间协作;上层 E2E 测试模拟真实用户操作流程;顶层手动测试用于探索性测试和用户体验评估。
1.2 测试工具链选型
| 工具 | 用途 | 官方维护 |
|---|---|---|
| Jest | JavaScript 单元测试框架 | |
| miniprogram-simulate | 小程序组件单元测试 | 微信官方 |
| miniprogram-automator | 小程序 E2E 自动化测试 | 微信官方 |
| 微信 CI | 云测服务、真机测试 | 微信官方 |
| GitHub Actions / Jenkins | CI/CD 流水线 | 社区/云厂商 |
二、单元测试
2.1 环境配置
npm install --save-dev jest miniprogram-simulate
// jest.config.js
module.exports = {
testEnvironment: 'node',
moduleFileExtensions: ['js', 'json'],
testMatch: ['**/__tests__/**/*.test.js'],
collectCoverageFrom: [
'utils/**/*.js',
'store/**/*.js',
'!**/__tests__/**'
],
coverageThreshold: {
global: {
branches: 70,
functions: 80,
lines: 80,
statements: 80
}
}
};
2.2 工具函数单元测试
// utils/__tests__/format.test.js
const { formatPrice, formatDate, debounce } = require('../format');
describe('formatPrice', () => {
test('formats integer price', () => {
expect(formatPrice(100)).toBe('¥100.00');
});
test('formats decimal price', () => {
expect(formatPrice(99.9)).toBe('¥99.90');
});
test('handles zero', () => {
expect(formatPrice(0)).toBe('¥0.00');
});
test('rounds to two decimals', () => {
expect(formatPrice(99.999)).toBe('¥100.00');
});
});
describe('debounce', () => {
jest.useFakeTimers();
test('defers execution', () => {
const fn = jest.fn();
const debounced = debounce(fn, 300);
debounced('arg1');
expect(fn).not.toBeCalled();
jest.advanceTimersByTime(300);
expect(fn).toBeCalledWith('arg1');
expect(fn).toBeCalledTimes(1);
});
test('cancels previous calls', () => {
const fn = jest.fn();
const debounced = debounce(fn, 300);
debounced('first');
jest.advanceTimersByTime(100);
debounced('second');
jest.advanceTimersByTime(300);
expect(fn).toBeCalledWith('second');
expect(fn).toBeCalledTimes(1);
});
});
2.3 小程序组件单元测试
miniprogram-simulate 提供了在 Node.js 环境中模拟小程序组件渲染和行为的能力:
// components/star-rating/__tests__/star-rating.test.js
const simulate = require('miniprogram-simulate');
const path = require('path');
describe('StarRating Component', () => {
const id = simulate.load(path.join(__dirname, '../star-rating'));
test('renders correct number of stars', () => {
const comp = simulate.render(id, { score: 3, maxScore: 5 });
comp.attach(document.createElement('parent-wrapper'));
const stars = comp.querySelectorAll('.star');
expect(stars.length).toBe(5);
const fullStars = comp.querySelectorAll('.star.full');
expect(fullStars.length).toBe(3);
});
test('emits change event when interactive', () => {
const onChange = jest.fn();
const comp = simulate.render(id, {
score: 0,
interactive: true
});
comp.attach(document.createElement('parent-wrapper'));
comp.addEventListener('change', onChange);
const thirdStar = comp.querySelectorAll('.star')[2];
thirdStar.dispatchEvent('tap');
expect(onChange).toBeCalled();
expect(onChange.mock.calls[0][0].detail.score).toBe(3);
});
test('observer triggers on score change', async () => {
const comp = simulate.render(id, { score: 2 });
comp.setData({ score: 4 });
await simulate.sleep(0);
const fullStars = comp.querySelectorAll('.star.full');
expect(fullStars.length).toBe(4);
});
});
2.4 云函数单元测试
云函数本质上是 Node.js 函数,可以直接使用 Jest 测试:
// cloudfunctions/login/__tests__/login.test.js
const cloud = require('wx-server-sdk');
jest.mock('wx-server-sdk');
describe('Login Cloud Function', () => {
const mockDb = {
collection: jest.fn(() => mockDb),
where: jest.fn(() => mockDb),
get: jest.fn(),
add: jest.fn()
};
beforeEach(() => {
cloud.init.mockReturnValue(undefined);
cloud.getWXContext.mockReturnValue({
OPENID: 'test_openid_123',
APPID: 'test_appid'
});
cloud.database.mockReturnValue(mockDb);
});
test('creates new user if not exists', async () => {
mockDb.get.mockResolvedValue({ data: [] });
mockDb.add.mockResolvedValue({ _id: 'new_user_id' });
const login = require('../index');
const result = await login.main({ userInfo: { nickName: 'Test' } });
expect(result.success).toBe(true);
expect(result.data.openid).toBe('test_openid_123');
expect(mockDb.add).toBeCalled();
});
test('returns existing user', async () => {
const existingUser = { _id: 'existing_id', nickName: 'Test' };
mockDb.get.mockResolvedValue({ data: [existingUser] });
const login = require('../index');
const result = await login.main({});
expect(result.success).toBe(true);
expect(result.data.user._id).toBe('existing_id');
expect(mockDb.add).not.toBeCalled();
});
});
三、端到端(E2E)测试
3.1 miniprogram-automator
微信官方提供的 miniprogram-automator 可以在开发者工具或真机上自动执行小程序操作,是 E2E 测试的核心工具。
npm install --save-dev miniprogram-automator
// e2e/order-flow.test.js
const automator = require('miniprogram-automator');
describe('Order Flow E2E', () => {
let miniProgram;
beforeAll(async () => {
miniProgram = await automator.launch({
projectPath: path.resolve(__dirname, '../'),
cliPath: '/Applications/wechatwebdevtools.app/Contents/MacOS/cli'
});
}, 30000);
afterAll(async () => {
await miniProgram.close();
});
test('complete purchase flow', async () => {
// 1. 打开首页
const page = await miniProgram.reLaunch('/pages/index/index');
await page.waitFor(500);
// 2. 点击第一个商品
const firstProduct = await page.$('.product-item');
await firstProduct.tap();
await page.waitFor(1000);
// 3. 进入详情页,验证商品信息
const pagePath = await page.path;
expect(pagePath).toContain('/pages/detail/detail');
const title = await page.$eval('.product-title', el => el.innerText);
expect(title).toBeTruthy();
const price = await page.$eval('.product-price', el => el.innerText);
expect(price).toMatch(/¥\d+\.\d{2}/);
// 4. 点击加入购物车
const addBtn = await page.$('.add-cart-btn');
await addBtn.tap();
await page.waitFor(500);
const toast = await page.$eval('.toast', el => el.innerText);
expect(toast).toContain('已加入购物车');
// 5. 跳转到购物车页
const cartTab = await page.$('.tab-cart');
await cartTab.tap();
await page.waitFor(800);
// 6. 验证购物车有商品
const cartItems = await page.$$('.cart-item');
expect(cartItems.length).toBeGreaterThan(0);
}, 60000);
});
3.2 E2E 测试最佳实践
- 数据隔离:每次测试前重置测试数据库到已知状态,测试间不互相影响
- 等待策略:优先使用元素可见性等待而非固定时间等待
- 选择性执行:将 E2E 测试按模块分组,CI 中全量执行,本地仅执行相关模块
- 截图与录屏:失败时自动截图保存,便于排查问题
- Mock 外部依赖:支付、地图等外部服务应在测试环境中使用 Mock 实现
四、微信 CI 云测服务
微信官方提供了免费的云测服务,支持在真机上执行自动化测试:
4.1 配置云测计划
# .ma-ci.yml
name: Miniprogram CI
on:
push:
branches: [main, develop]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node
uses: actions/setup-node@v3
with:
node-version: '18'
- name: Install dependencies
run: npm ci
- name: Run unit tests
run: npm run test:unit -- --coverage
- name: Upload coverage
uses: codecov/codecov-action@v3
build:
needs: test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Build miniprogram
run: npm run build
- name: Upload to WeChat CI
uses: wechat-miniprogram/ci-upload-action@v1
with:
appid: ${{ secrets.WECHAT_APPID }}
private-key: ${{ secrets.WECHAT_PRIVATE_KEY }}
version: ${{ github.run_number }}
desc: 'CI build from ${{ github.sha }}'
4.2 微信 CI 上传
// scripts/ci-upload.js
const ci = require('miniprogram-ci');
(async () => {
const project = new ci.Project({
appid: process.env.WECHAT_APPID,
type: 'miniProgram',
projectPath: './dist',
privateKeyPath: './private.key',
ignores: ['node_modules/**/*']
});
const result = await ci.upload({
project,
version: process.env.VERSION || '1.0.0',
desc: process.env.DESC || 'CI upload',
setting: {
es6: true,
es7: true,
minify: true,
autoPrefixWXSS: true
}
});
console.log('Upload result:', result);
})();
五、GitHub Actions 完整流水线
# .github/workflows/miniprogram-ci.yml
name: Miniprogram CI/CD
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- run: npm ci
- run: npm run lint
- run: npm run typecheck
test:
runs-on: ubuntu-latest
needs: lint
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- run: npm ci
- run: npm run test:unit -- --coverage --ci
- name: Upload coverage
uses: codecov/codecov-action@v3
with:
files: ./coverage/lcov.info
test-cloud:
runs-on: ubuntu-latest
needs: test
# 仅在 main 分支上运行云测试
if: github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- run: npm ci
- run: npm run test:e2e
env:
WECHAT_APPID: ${{ secrets.WECHAT_APPID }}
build:
runs-on: ubuntu-latest
needs: [lint, test]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- run: npm ci
- run: npm run build:prod
- name: Upload dist artifact
uses: actions/upload-artifact@v3
with:
name: miniprogram-dist
path: ./dist/**
deploy-preview:
runs-on: ubuntu-latest
needs: build
if: github.event_name == 'pull_request'
steps:
- uses: actions/download-artifact@v3
with:
name: miniprogram-dist
path: ./dist
- name: Upload preview
run: |
node scripts/ci-upload.js
env:
WECHAT_APPID: ${{ secrets.WECHAT_APPID }}
WECHAT_PRIVATE_KEY: ${{ secrets.WECHAT_PRIVATE_KEY }}
VERSION: pr-${{ github.event.number }}-${{ github.run_number }}
DESC: "PR #${{ github.event.number }} preview"
deploy-production:
runs-on: ubuntu-latest
needs: [build, test-cloud]
if: github.ref == 'refs/heads/main'
environment: production
steps:
- uses: actions/download-artifact@v3
with:
name: miniprogram-dist
path: ./dist
- name: Upload to WeChat
run: node scripts/ci-upload.js
env:
WECHAT_APPID: ${{ secrets.WECHAT_APPID }}
WECHAT_PRIVATE_KEY: ${{ secrets.WECHAT_PRIVATE_KEY }}
VERSION: ${{ github.run_number }}
DESC: "Production release ${{ github.sha }}"
六、测试数据与环境管理
6.1 测试数据库隔离
// tests/setup.js
const { setup: setupDevServer } = require('jest-dev-server');
module.exports = async () => {
// 启动本地云函数模拟器
await setupDevServer({
command: 'npx cloudbase-emulator run --db test',
launchTimeout: 30000,
port: 9229
});
// 初始化测试数据
const testData = require('./fixtures/test-data.json');
await seedDatabase(testData);
};
module.exports = async () => {
// 清理测试数据
await cleanupDatabase();
};
6.2 Mock 策略
// tests/setupMocks.js
global.wx = {
request: jest.fn(),
getStorageSync: jest.fn(),
setStorageSync: jest.fn(),
showToast: jest.fn(),
showLoading: jest.fn(),
hideLoading: jest.fn(),
showModal: jest.fn(() => Promise.resolve({ confirm: true })),
getSystemInfoSync: jest.fn(() => ({
windowWidth: 375,
windowHeight: 667,
pixelRatio: 2
})),
cloud: {
init: jest.fn(),
callFunction: jest.fn(),
database: jest.fn(() => ({
collection: jest.fn(),
command: {}
}))
}
};
七、总结
小程序的自动化测试与 CI/CD 建设需要结合平台特性进行适配。单元测试通过 Jest 和 miniprogram-simulate 覆盖工具函数和组件逻辑;E2E 测试通过 miniprogram-automator 模拟真实用户操作;微信 CI 云测服务提供了真机验证能力;GitHub Actions 则串联起代码检查、测试执行、构建打包和自动部署的完整流水线。
建立的测试体系应当是增量演进的:初期以单元测试为主,保障核心业务逻辑的正确性;随着项目成熟,逐步补充集成测试和 E2E 测试,覆盖关键用户路径;最终形成完善的 CI/CD 流水线,每次代码提交都经过自动化的质量门禁,只有通过全部检查后才允许部署到生产环境。这种「左移」的质量保障策略可以大幅降低线上缺陷率和修复成本。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。