调试和测试技巧 - 保证代码质量
🔍 好的代码需要充分的测试和调试,今天我们来学习如何保证代码质量
1. 开发者工具调试
HBuilderX调试技巧
javascript
// 1. 使用console进行调试
console.log('普通日志')
console.warn('警告信息')
console.error('错误信息')
console.table([{name: '张三', age: 25}, {name: '李四', age: 30}])
// 2. 使用debugger断点
function complexFunction(data) {
debugger; // 程序会在这里暂停
const result = data.map(item => {
debugger; // 也可以在循环中设置断点
return item.value * 2
})
return result
}
// 3. 条件断点
function processData(items) {
items.forEach((item, index) => {
if (index === 5) {
debugger; // 只在第6个元素时暂停
}
// 处理逻辑
})
}
浏览器开发者工具
javascript
// utils/debug.js
class DebugHelper {
constructor() {
this.isDebug = process.env.NODE_ENV === 'development'
this.logs = []
}
// 增强的日志功能
log(message, data = null, level = 'info') {
if (!this.isDebug) return
const timestamp = new Date().toISOString()
const logEntry = {
timestamp,
level,
message,
data,
stack: new Error().stack
}
this.logs.push(logEntry)
// 根据级别输出不同颜色
const styles = {
info: 'color: #007aff',
warn: 'color: #ff9500',
error: 'color: #ff3b30',
success: 'color: #34c759'
}
console.log(
`%c[\${level.toUpperCase()}] \${timestamp} - \${message}`,
styles[level] || styles.info,
data
)
}
// 性能监控
time(label) {
if (!this.isDebug) return
console.time(label)
}
timeEnd(label) {
if (!this.isDebug) return
console.timeEnd(label)
}
// 内存使用情况
memory() {
if (!this.isDebug || !performance.memory) return
const memory = performance.memory
this.log('内存使用情况', {
used: `\${Math.round(memory.usedJSHeapSize / 1024 / 1024)} MB`,
total: `\${Math.round(memory.totalJSHeapSize / 1024 / 1024)} MB`,
limit: `\${Math.round(memory.jsHeapSizeLimit / 1024 / 1024)} MB`
})
}
// 网络请求监控
trackRequest(url, options, response, duration) {
if (!this.isDebug) return
this.log('网络请求', {
url,
method: options.method || 'GET',
status: response.statusCode,
duration: `\${duration}ms`,
size: response.header['content-length'] || 'unknown'
})
}
// 导出日志
exportLogs() {
if (!this.isDebug) return
const logsJson = JSON.stringify(this.logs, null, 2)
uni.setClipboardData({
data: logsJson,
success: () => {
uni.showToast({
title: '日志已复制到剪贴板',
icon: 'success'
})
}
})
}
// 清空日志
clearLogs() {
this.logs = []
console.clear()
}
}
export default new DebugHelper()
2. 错误处理和监控
全局错误处理
javascript
// utils/error-handler.js
class ErrorHandler {
constructor() {
this.errorQueue = []
this.maxErrors = 50
this.setupGlobalErrorHandling()
}
setupGlobalErrorHandling() {
// Vue错误处理
Vue.config.errorHandler = (err, vm, info) => {
this.handleError({
type: 'vue',
error: err,
component: vm?.$options.name || 'Unknown',
info,
timestamp: new Date().toISOString()
})
}
// Promise错误处理
window.addEventListener('unhandledrejection', (event) => {
this.handleError({
type: 'promise',
error: event.reason,
timestamp: new Date().toISOString()
})
})
// 网络错误处理
this.setupNetworkErrorHandling()
}
handleError(errorInfo) {
// 添加到错误队列
this.errorQueue.push(errorInfo)
// 保持队列大小
if (this.errorQueue.length > this.maxErrors) {
this.errorQueue.shift()
}
// 开发环境下输出详细错误
if (process.env.NODE_ENV === 'development') {
console.error('错误详情:', errorInfo)
}
// 生产环境下上报错误
if (process.env.NODE_ENV === 'production') {
this.reportError(errorInfo)
}
// 用户友好的错误提示
this.showUserFriendlyError(errorInfo)
}
setupNetworkErrorHandling() {
const originalRequest = uni.request
uni.request = (options) => {
const startTime = Date.now()
return new Promise((resolve, reject) => {
originalRequest({
...options,
success: (res) => {
const duration = Date.now() - startTime
// 记录成功的请求
if (process.env.NODE_ENV === 'development') {
console.log(`请求成功: \${options.url} (\${duration}ms)`)
}
resolve(res)
},
fail: (err) => {
const duration = Date.now() - startTime
// 处理网络错误
this.handleError({
type: 'network',
error: err,
url: options.url,
method: options.method || 'GET',
duration,
timestamp: new Date().toISOString()
})
reject(err)
}
})
})
}
}
reportError(errorInfo) {
// 上报到错误监控服务
try {
uni.request({
url: '/api/errors/report',
method: 'POST',
data: {
...errorInfo,
userAgent: navigator.userAgent,
url: window.location.href,
userId: this.getCurrentUserId()
}
})
} catch (reportError) {
console.error('错误上报失败:', reportError)
}
}
showUserFriendlyError(errorInfo) {
let message = '操作失败,请稍后重试'
// 根据错误类型显示不同消息
if (errorInfo.type === 'network') {
message = '网络连接失败,请检查网络设置'
} else if (errorInfo.error?.message?.includes('timeout')) {
message = '请求超时,请稍后重试'
}
uni.showToast({
title: message,
icon: 'error',
duration: 3000
})
}
getCurrentUserId() {
try {
const userInfo = uni.getStorageSync('userInfo')
return userInfo?.id || 'anonymous'
} catch {
return 'anonymous'
}
}
getErrorLogs() {
return [...this.errorQueue]
}
clearErrorLogs() {
this.errorQueue = []
}
}
export default new ErrorHandler()
3. 单元测试
Jest测试配置
javascript
// jest.config.js
module.exports = {
preset: '@vue/cli-plugin-unit-jest',
testEnvironment: 'jsdom',
moduleFileExtensions: ['js', 'json', 'vue'],
transform: {
'^.+\\.vue$': 'vue-jest',
'.+\\.(css|styl|less|sass|scss|svg|png|jpg|ttf|woff|woff2)$': 'jest-transform-stub',
'^.+\\.jsx?$': 'babel-jest'
},
moduleNameMapping: {
'^@/(.*)$': '<rootDir>/src/$1'
},
snapshotSerializers: ['jest-serializer-vue'],
testMatch: [
'**/tests/unit/**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)'
],
collectCoverageFrom: [
'src/**/*.{js,vue}',
'!src/main.js',
'!**/node_modules/**'
],
setupFilesAfterEnv: ['<rootDir>/tests/unit/setup.js']
}
工具函数测试
javascript
// tests/unit/utils/date.spec.js
import dateUtils from '@/utils/date.js'
describe('dateUtils', () => {
describe('format', () => {
it('应该正确格式化日期', () => {
const date = new Date('2024-01-15T10:30:00')
const result = dateUtils.format(date, 'YYYY-MM-DD')
expect(result).toBe('2024-01-15')
})
it('应该使用默认格式', () => {
const date = new Date('2024-01-15T10:30:00')
const result = dateUtils.format(date)
expect(result).toBe('2024-01-15 10:30:00')
})
})
describe('isToday', () => {
it('应该正确判断今天的日期', () => {
const today = new Date()
expect(dateUtils.isToday(today)).toBe(true)
})
it('应该正确判断非今天的日期', () => {
const yesterday = new Date()
yesterday.setDate(yesterday.getDate() - 1)
expect(dateUtils.isToday(yesterday)).toBe(false)
})
})
describe('fromNow', () => {
it('应该返回相对时间', () => {
const oneHourAgo = new Date()
oneHourAgo.setHours(oneHourAgo.getHours() - 1)
const result = dateUtils.fromNow(oneHourAgo)
expect(result).toContain('小时前')
})
})
})
组件测试
javascript
// tests/unit/components/TaskItem.spec.js
import { shallowMount } from '@vue/test-utils'
import TaskItem from '@/components/TaskItem.vue'
describe('TaskItem.vue', () => {
const mockTask = {
id: '1',
title: '测试任务',
description: '这是一个测试任务',
completed: false,
priority: 'medium',
dueDate: '2024-12-31T23:59:59'
}
it('应该正确渲染任务信息', () => {
const wrapper = shallowMount(TaskItem, {
propsData: { task: mockTask }
})
expect(wrapper.find('.task-title').text()).toBe('测试任务')
expect(wrapper.find('.task-description').text()).toBe('这是一个测试任务')
})
it('应该显示正确的优先级样式', () => {
const wrapper = shallowMount(TaskItem, {
propsData: { task: mockTask }
})
expect(wrapper.find('.priority-medium').exists()).toBe(true)
})
it('点击切换按钮应该触发toggle事件', async () => {
const wrapper = shallowMount(TaskItem, {
propsData: { task: mockTask }
})
await wrapper.find('.toggle-btn').trigger('click')
expect(wrapper.emitted().toggle).toBeTruthy()
expect(wrapper.emitted().toggle[0]).toEqual([mockTask.id])
})
it('已完成的任务应该显示完成样式', () => {
const completedTask = { ...mockTask, completed: true }
const wrapper = shallowMount(TaskItem, {
propsData: { task: completedTask }
})
expect(wrapper.find('.task-completed').exists()).toBe(true)
})
it('过期任务应该显示过期样式', () => {
const overdueTask = {
...mockTask,
dueDate: '2020-01-01T00:00:00',
completed: false
}
const wrapper = shallowMount(TaskItem, {
propsData: { task: overdueTask }
})
expect(wrapper.find('.task-overdue').exists()).toBe(true)
})
})
Vuex测试
javascript
// tests/unit/store/tasks.spec.js
import { createLocalVue } from '@vue/test-utils'
import Vuex from 'vuex'
import tasksModule from '@/store/modules/tasks.js'
import { Task } from '@/models/task.js'
const localVue = createLocalVue()
localVue.use(Vuex)
describe('tasks store module', () => {
let store
beforeEach(() => {
store = new Vuex.Store({
modules: {
tasks: {
...tasksModule,
namespaced: true
}
}
})
})
describe('mutations', () => {
it('SET_TASKS应该设置任务列表', () => {
const tasks = [
new Task({ title: '任务1' }),
new Task({ title: '任务2' })
]
store.commit('tasks/SET_TASKS', tasks)
expect(store.state.tasks.tasks).toEqual(tasks)
})
it('ADD_TASK应该添加新任务', () => {
const task = new Task({ title: '新任务' })
store.commit('tasks/ADD_TASK', task)
expect(store.state.tasks.tasks).toContain(task)
})
it('TOGGLE_TASK应该切换任务状态', () => {
const task = new Task({ title: '任务', completed: false })
store.commit('tasks/SET_TASKS', [task])
store.commit('tasks/TOGGLE_TASK', task.id)
expect(store.state.tasks.tasks[0].completed).toBe(true)
})
})
describe('getters', () => {
beforeEach(() => {
const tasks = [
new Task({ title: '任务1', completed: false }),
new Task({ title: '任务2', completed: true }),
new Task({ title: '任务3', completed: false })
]
store.commit('tasks/SET_TASKS', tasks)
})
it('taskStats应该返回正确的统计信息', () => {
const stats = store.getters['tasks/taskStats']
expect(stats.total).toBe(3)
expect(stats.completed).toBe(1)
expect(stats.active).toBe(2)
expect(stats.completionRate).toBe(33)
})
it('filteredTasks应该根据过滤器返回任务', () => {
store.commit('tasks/SET_FILTER', 'active')
const filtered = store.getters['tasks/filteredTasks']
expect(filtered.length).toBe(2)
expect(filtered.every(task => !task.completed)).toBe(true)
})
})
})
4. 性能测试
性能监控工具
javascript
// utils/performance-monitor.js
class PerformanceMonitor {
constructor() {
this.metrics = new Map()
this.observers = []
this.setupObservers()
}
setupObservers() {
// 页面性能观察器
if (typeof PerformanceObserver !== 'undefined') {
const observer = new PerformanceObserver((list) => {
list.getEntries().forEach((entry) => {
this.recordMetric(entry.name, entry.duration, entry.entryType)
})
})
observer.observe({ entryTypes: ['measure', 'navigation'] })
this.observers.push(observer)
}
}
// 开始性能测量
startMeasure(name) {
performance.mark(`\${name}-start`)
}
// 结束性能测量
endMeasure(name) {
performance.mark(`\${name}-end`)
performance.measure(name, `\${name}-start`, `\${name}-end`)
const measure = performance.getEntriesByName(name, 'measure')[0]
if (measure) {
this.recordMetric(name, measure.duration, 'custom')
}
}
// 记录指标
recordMetric(name, value, type) {
if (!this.metrics.has(name)) {
this.metrics.set(name, [])
}
this.metrics.get(name).push({
value,
type,
timestamp: Date.now()
})
// 性能警告
if (value > this.getThreshold(name)) {
console.warn(`性能警告: \${name} 耗时 \${value.toFixed(2)}ms`)
}
}
// 获取性能阈值
getThreshold(name) {
const thresholds = {
'page-load': 3000,
'api-request': 2000,
'component-render': 100,
'list-scroll': 16 // 60fps
}
return thresholds[name] || 1000
}
// 获取性能报告
getPerformanceReport() {
const report = {}
this.metrics.forEach((values, name) => {
const durations = values.map(v => v.value)
report[name] = {
count: durations.length,
average: durations.reduce((a, b) => a + b, 0) / durations.length,
min: Math.min(...durations),
max: Math.max(...durations),
p95: this.percentile(durations, 0.95)
}
})
return report
}
// 计算百分位数
percentile(arr, p) {
const sorted = arr.slice().sort((a, b) => a - b)
const index = Math.ceil(sorted.length * p) - 1
return sorted[index]
}
// 内存使用情况
getMemoryUsage() {
if (performance.memory) {
return {
used: Math.round(performance.memory.usedJSHeapSize / 1024 / 1024),
total: Math.round(performance.memory.totalJSHeapSize / 1024 / 1024),
limit: Math.round(performance.memory.jsHeapSizeLimit / 1024 / 1024)
}
}
return null
}
// 清理资源
cleanup() {
this.observers.forEach(observer => observer.disconnect())
this.observers = []
this.metrics.clear()
}
}
export default new PerformanceMonitor()
自动化测试脚本
javascript
// tests/e2e/task-management.spec.js
describe('任务管理功能', () => {
beforeEach(() => {
// 重置应用状态
cy.clearLocalStorage()
cy.visit('/')
})
it('应该能够创建新任务', () => {
// 点击添加按钮
cy.get('[data-test="add-task-btn"]').click()
// 填写任务信息
cy.get('[data-test="task-title-input"]').type('测试任务')
cy.get('[data-test="task-description-input"]').type('这是一个测试任务')
cy.get('[data-test="priority-select"]').select('high')
// 保存任务
cy.get('[data-test="save-task-btn"]').click()
// 验证任务已创建
cy.get('[data-test="task-list"]').should('contain', '测试任务')
cy.get('[data-test="task-item"]').should('have.class', 'priority-high')
})
it('应该能够完成任务', () => {
// 先创建一个任务
cy.createTask('待完成任务')
// 点击完成按钮
cy.get('[data-test="task-item"]').first().find('[data-test="toggle-btn"]').click()
// 验证任务已完成
cy.get('[data-test="task-item"]').first().should('have.class', 'completed')
cy.get('[data-test="completed-count"]').should('contain', '1')
})
it('应该能够过滤任务', () => {
// 创建多个任务
cy.createTask('任务1', { completed: false })
cy.createTask('任务2', { completed: true })
cy.createTask('任务3', { completed: false })
// 过滤已完成任务
cy.get('[data-test="filter-completed"]').click()
cy.get('[data-test="task-item"]').should('have.length', 1)
cy.get('[data-test="task-item"]').should('contain', '任务2')
// 过滤进行中任务
cy.get('[data-test="filter-active"]').click()
cy.get('[data-test="task-item"]').should('have.length', 2)
})
it('应该能够搜索任务', () => {
// 创建多个任务
cy.createTask('购买牛奶')
cy.createTask('写代码')
cy.createTask('买菜做饭')
// 搜索包含"买"的任务
cy.get('[data-test="search-input"]').type('买')
cy.get('[data-test="task-item"]').should('have.length', 2)
cy.get('[data-test="task-item"]').should('contain', '购买牛奶')
cy.get('[data-test="task-item"]').should('contain', '买菜做饭')
})
})
// 自定义命令
Cypress.Commands.add('createTask', (title, options = {}) => {
cy.get('[data-test="add-task-btn"]').click()
cy.get('[data-test="task-title-input"]').type(title)
if (options.description) {
cy.get('[data-test="task-description-input"]').type(options.description)
}
if (options.priority) {
cy.get('[data-test="priority-select"]').select(options.priority)
}
cy.get('[data-test="save-task-btn"]').click()
if (options.completed) {
cy.get('[data-test="task-item"]').last().find('[data-test="toggle-btn"]').click()
}
})
小结
今天我们学习了:
- ✅ 开发者工具的调试技巧
- ✅ 全局错误处理和监控
- ✅ 单元测试的编写方法
- ✅ 性能监控和测试
- ✅ 端到端自动化测试
测试和调试要点:
- 调试要系统化,不要盲目猜测
- 错误处理要考虑用户体验
- 测试要覆盖核心功能和边界情况
- 性能监控要持续进行
下一篇预告
下一篇是我们系列的最后一篇《发布和部署 - 让你的小程序上线》,学习如何将应用发布到各个平台。
质量是软件的生命线,充分的测试和调试是保证用户体验的基础!