测试与调试
💡 核心结论
小程序测试包括单元测试、集成测试、端到端测试
开发者工具提供调试器、性能分析、网络监控等功能
真机调试是发现问题的关键,不同机型表现可能不同
Mock数据和测试工具可以提升开发效率
自动化测试可以保证代码质量和稳定性
1. 开发者工具调试
1.1 调试器使用
Console面板:
// 基础日志
console.log('普通日志', data)
console.info('信息日志', data)
console.warn('警告日志', data)
console.error('错误日志', data)
// 分组日志
console.group('分组1')
console.log('内容1')
console.log('内容2')
console.groupEnd()
// 表格显示
console.table([
{ name: '张三', age: 25 },
{ name: '李四', age: 30 }
])
// 计时
console.time('timer')
// ... 代码执行
console.timeEnd('timer') // 输出: timer: 123.456ms
Sources面板:
// 断点调试
// 1. 在代码行号左侧点击设置断点
// 2. 触发代码执行
// 3. 查看变量值、调用栈
// 4. 单步执行(F10)、进入函数(F11)、跳出函数(Shift+F11)
// 条件断点
// 右键断点 → 编辑断点 → 设置条件
// 例如:index > 10
Network面板:
// 查看所有网络请求
// - 请求URL、方法、状态码
// - 请求头、响应头
// - 请求参数、响应数据
// - 请求耗时
// 过滤请求
// - 按类型:XHR、Image、WebSocket
// - 按关键词搜索
1.2 性能分析
// Performance面板
// 1. 点击"开始录制"
// 2. 执行操作
// 3. 点击"停止录制"
// 4. 查看性能数据:
// - FPS(帧率)
// - CPU使用率
// - 内存占用
// - setData调用次数
// - 渲染时间
1.3 存储面板
// Storage面板
// 查看:
// - Local Storage
// - Session Storage
// - 小程序Storage(wx.setStorage存储的数据)
// 可以:
// - 查看所有key-value
// - 编辑、删除数据
// - 清空所有数据
2. 真机调试
2.1 预览功能
# 开发者工具 → 预览
# 生成二维码 → 微信扫码 → 真机预览
# 注意事项:
# 1. 手机和电脑需在同一网络
# 2. 需要登录开发者账号
# 3. 可以查看控制台日志
2.2 真机调试
# 开发者工具 → 真机调试
# 生成二维码 → 微信扫码 → 开启调试
# 功能:
# 1. 实时查看控制台日志
# 2. 查看网络请求
# 3. 查看Storage数据
# 4. 查看页面结构
# 5. 查看元素样式
2.3 远程调试
// 小程序后台 → 开发 → 开发管理 → 开发设置 → 远程调试
// 使用场景:
// 1. 真机预览无法复现问题
// 2. 需要查看详细日志
// 3. 需要调试特定功能
2.4 常见真机问题
1. 样式问题:
/* 不同机型可能表现不同 */
/* 使用rpx单位,避免使用px */
.container {
width: 750rpx; /* ✅ 正确 */
width: 375px; /* ❌ 可能在不同机型显示异常 */
}
/* 测试不同机型 */
/* iPhone、Android、iPad */
2. 兼容性问题:
// 检查API支持
if (wx.canIUse('getUpdateManager')) {
// 使用新API
} else {
// 降级方案
}
// 检查基础库版本
const systemInfo = wx.getSystemInfoSync()
console.log('基础库版本', systemInfo.SDKVersion)
// 最低基础库版本要求
// app.json
{
"libVersion": "2.19.4"
}
3. 网络问题:
// 真机网络环境可能不同
// 测试:
// - WiFi环境
// - 4G/5G环境
// - 弱网环境
// 模拟弱网
// 开发者工具 → 设置 → 项目设置 → 不校验合法域名
3. 单元测试
3.1 测试框架配置
# 安装依赖
npm install --save-dev jest @miniprogram/jest
# 或使用miniprogram-simulate
npm install --save-dev miniprogram-simulate
// package.json
{
"scripts": {
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage"
},
"jest": {
"preset": "@miniprogram/jest",
"testEnvironment": "jsdom",
"testMatch": ["**/__tests__/**/*.test.js"],
"collectCoverageFrom": [
"**/*.js",
"!**/node_modules/**",
"!**/dist/**"
]
}
}
3.2 工具函数测试
// utils/format.test.js
import { formatPrice, formatDate } from './format.js'
describe('format工具函数', () => {
test('formatPrice应该正确格式化价格', () => {
expect(formatPrice(100)).toBe('¥1.00')
expect(formatPrice(1234)).toBe('¥12.34')
expect(formatPrice(0)).toBe('¥0.00')
})
test('formatDate应该正确格式化日期', () => {
const date = new Date('2024-01-01')
expect(formatDate(date)).toBe('2024-01-01')
})
})
// utils/format.js
export function formatPrice(price) {
return '¥' + (price / 100).toFixed(2)
}
export function formatDate(date) {
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
return `${year}-${month}-${day}`
}
3.3 API测试
// utils/request.test.js
import { http } from './request.js'
// Mock wx.request
jest.mock('wx', () => ({
request: jest.fn()
}))
describe('request工具', () => {
beforeEach(() => {
jest.clearAllMocks()
})
test('get请求应该正确调用wx.request', async () => {
const mockData = { list: [1, 2, 3] }
wx.request.mockImplementation((options) => {
options.success({ data: mockData })
})
const result = await http.get('/api/products')
expect(wx.request).toHaveBeenCalledWith(
expect.objectContaining({
url: expect.stringContaining('/api/products'),
method: 'GET'
})
)
expect(result).toEqual(mockData)
})
test('请求失败应该抛出错误', async () => {
wx.request.mockImplementation((options) => {
options.fail({ errMsg: 'request:fail' })
})
await expect(http.get('/api/products')).rejects.toThrow()
})
})
3.4 组件测试
// components/product-item/product-item.test.js
const simulate = require('miniprogram-simulate')
describe('ProductItem组件', () => {
let comp
beforeEach(() => {
comp = simulate.load('/components/product-item/product-item')
})
test('应该正确显示商品信息', () => {
const product = {
id: 1,
name: '测试商品',
price: 10000
}
comp.setData({ product })
comp.attach(document.body)
expect(comp.querySelector('.product-name').textContent).toBe('测试商品')
expect(comp.querySelector('.product-price').textContent).toBe('¥100.00')
})
test('点击应该触发tap事件', () => {
const product = { id: 1, name: '测试商品', price: 10000 }
comp.setData({ product })
comp.attach(document.body)
const tapHandler = jest.fn()
comp.addEventListener('tap', tapHandler)
comp.querySelector('.product-item').dispatchEvent('tap')
expect(tapHandler).toHaveBeenCalledWith(
expect.objectContaining({
detail: { id: 1 }
})
)
})
})
4. 集成测试
4.1 页面流程测试
// tests/pages/index.test.js
const simulate = require('miniprogram-simulate')
describe('首页流程测试', () => {
let page
beforeEach(() => {
page = simulate.load('/pages/index/index')
})
test('页面加载应该请求数据', async () => {
const mockRequest = jest.fn()
wx.request = mockRequest
page.triggerLifeTime('onLoad')
await simulate.sleep(100)
expect(mockRequest).toHaveBeenCalled()
})
test('下拉刷新应该重新加载数据', async () => {
const mockRequest = jest.fn()
wx.request = mockRequest
page.triggerLifeTime('onPullDownRefresh')
await simulate.sleep(100)
expect(mockRequest).toHaveBeenCalled()
expect(wx.stopPullDownRefresh).toHaveBeenCalled()
})
test('上拉加载应该加载更多', async () => {
page.setData({
list: Array(10).fill(0).map((_, i) => ({ id: i }))
})
const mockRequest = jest.fn()
wx.request = mockRequest
page.triggerLifeTime('onReachBottom')
await simulate.sleep(100)
expect(mockRequest).toHaveBeenCalled()
})
})
4.2 用户交互测试
// tests/user-interaction.test.js
describe('用户交互测试', () => {
test('点击按钮应该触发相应操作', () => {
const page = simulate.load('/pages/index/index')
page.attach(document.body)
const button = page.querySelector('.submit-button')
const submitHandler = jest.fn()
page.instance.onSubmit = submitHandler
button.dispatchEvent('tap')
expect(submitHandler).toHaveBeenCalled()
})
test('输入框输入应该更新数据', () => {
const page = simulate.load('/pages/index/index')
page.attach(document.body)
const input = page.querySelector('.search-input')
input.dispatchEvent('input', {
detail: { value: '测试' }
})
expect(page.data.keyword).toBe('测试')
})
})
5. E2E测试
5.1 自动化测试工具
# 安装miniprogram-automator
npm install --save-dev miniprogram-automator
// tests/e2e/index.test.js
const automator = require('miniprogram-automator')
describe('E2E测试', () => {
let miniProgram
let page
beforeAll(async () => {
miniProgram = await automator.launch({
projectPath: './miniprogram',
cliPath: '/Applications/wechatwebdevtools.app/Contents/MacOS/cli'
})
page = await miniProgram.reLaunch('/pages/index/index')
await page.waitFor(1000)
})
afterAll(async () => {
await miniProgram.close()
})
test('应该正确显示首页内容', async () => {
const title = await page.$('.page-title')
const text = await title.text()
expect(text).toBe('首页')
})
test('应该能够搜索商品', async () => {
const searchInput = await page.$('.search-input')
await searchInput.input('手机')
const searchButton = await page.$('.search-button')
await searchButton.tap()
await page.waitFor(2000)
const products = await page.$$('.product-item')
expect(products.length).toBeGreaterThan(0)
})
test('应该能够添加商品到购物车', async () => {
const addButton = await page.$('.add-to-cart')
await addButton.tap()
await page.waitFor(500)
const toast = await page.$('.weui-toast')
const toastText = await toast.text()
expect(toastText).toContain('添加成功')
})
})
6. Mock数据
6.1 本地Mock
// mock/api.js
export const mockData = {
products: [
{ id: 1, name: '商品1', price: 10000 },
{ id: 2, name: '商品2', price: 20000 }
],
user: {
id: 1,
name: '测试用户',
avatar: 'https://example.com/avatar.png'
}
}
// utils/request.js
import { mockData } from '../mock/api.js'
const isDev = process.env.NODE_ENV === 'development'
const useMock = isDev && wx.getStorageSync('useMock')
export function request(options) {
if (useMock) {
return Promise.resolve(mockData[options.url])
}
return new Promise((resolve, reject) => {
wx.request({
url: options.url,
success: resolve,
fail: reject
})
})
}
6.2 Mock服务
// 使用mockjs
// npm install mockjs
// mock/index.js
import Mock from 'mockjs'
Mock.mock('/api/products', 'get', {
code: 0,
data: {
list: Mock.mock({
'list|10': [{
'id|+1': 1,
'name': '@ctitle(5, 10)',
'price|1000-10000': 1,
'image': '@image("200x200")'
}]
})
}
})
// 在开发环境启用
if (process.env.NODE_ENV === 'development') {
require('./mock')
}
7. 调试技巧
7.1 条件断点
// 只在特定条件下暂停
// 例如:只在index > 10时暂停
for (let i = 0; i < 100; i++) {
// 设置条件断点:i > 10
console.log(i)
}
7.2 日志分级
// utils/logger.js
const DEBUG = wx.getStorageSync('debug') === 'true'
export const logger = {
debug(...args) {
if (DEBUG) {
console.log('[DEBUG]', ...args)
}
},
info(...args) {
console.log('[INFO]', ...args)
},
error(...args) {
console.error('[ERROR]', ...args)
}
}
7.3 性能分析
// utils/performance.js
export function measure(name, fn) {
const start = Date.now()
const result = fn()
const end = Date.now()
console.log(`[Performance] ${name}: ${end - start}ms`)
return result
}
// 使用
measure('loadData', () => {
this.loadData()
})
7.4 数据快照
// 保存数据快照用于调试
export function snapshot(data, name = 'snapshot') {
const snapshotData = {
timestamp: Date.now(),
data: JSON.parse(JSON.stringify(data))
}
wx.setStorageSync(`snapshot_${name}`, snapshotData)
console.log(`[Snapshot] ${name} saved`, snapshotData)
}
// 使用
snapshot(this.data, 'pageData')
8. 测试最佳实践
8.1 测试覆盖率
# 运行测试并生成覆盖率报告
npm run test:coverage
# 目标覆盖率:
# - 语句覆盖率 > 80%
# - 分支覆盖率 > 75%
# - 函数覆盖率 > 80%
8.2 测试金字塔
/\
/E2E\ ← 少量端到端测试
/------\
/集成测试\ ← 适量集成测试
/----------\
/ 单元测试 \ ← 大量单元测试
/--------------\
8.3 测试命名规范
// ✅ 好的测试命名
describe('ProductItem组件', () => {
test('应该正确显示商品名称和价格', () => {})
test('点击商品应该跳转到详情页', () => {})
test('价格应该格式化为两位小数', () => {})
})
// ❌ 不好的测试命名
describe('test', () => {
test('test1', () => {})
test('test2', () => {})
})
8.4 测试隔离
// 每个测试应该独立,不依赖其他测试
describe('用户登录', () => {
beforeEach(() => {
// 每个测试前重置状态
wx.clearStorageSync()
})
test('应该能够登录', () => {
// 测试代码
})
test('登录失败应该显示错误', () => {
// 测试代码
})
})
9. 常见问题
Q1: 真机调试看不到日志?
A:
确认开启了真机调试模式
检查手机和电脑是否在同一网络
尝试重启开发者工具
检查防火墙设置
Q2: 如何测试不同机型?
A:
使用真机预览功能
准备多台测试设备
使用云测试平台(如Testin、WeTest)
Q3: 单元测试如何Mock wx API?
A:
// 使用jest.mock
jest.mock('wx', () => ({
request: jest.fn(),
showToast: jest.fn()
}))
Q4: 如何测试异步操作?
A:
test('应该异步加载数据', async () => {
const promise = loadData()
await expect(promise).resolves.toEqual(expectedData)
})
参考资源
微信开发者工具文档
Jest测试框架文档
miniprogram-simulate文档
小程序测试最佳实践