Skip to content

调试和测试技巧 - 保证代码质量

🔍 好的代码需要充分的测试和调试,今天我们来学习如何保证代码质量

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()
  }
})

小结

今天我们学习了:

  • ✅ 开发者工具的调试技巧
  • ✅ 全局错误处理和监控
  • ✅ 单元测试的编写方法
  • ✅ 性能监控和测试
  • ✅ 端到端自动化测试

测试和调试要点

  • 调试要系统化,不要盲目猜测
  • 错误处理要考虑用户体验
  • 测试要覆盖核心功能和边界情况
  • 性能监控要持续进行

下一篇预告

下一篇是我们系列的最后一篇《发布和部署 - 让你的小程序上线》,学习如何将应用发布到各个平台。


质量是软件的生命线,充分的测试和调试是保证用户体验的基础!