返回首页

教程导航

返回首页

🏷️ 标签(1)

🗂️ 分类(1)

第 19 章 测试、调试与性能

第 19 章 测试、调试与性能

Lua 生态没有强制统一的测试框架。小型项目可以用一个断言驱动的最小测试器起步,再根据规模引入成熟的测试工具。

小型测试框架

local tests = {}

local function test(name, callback)
  tests[#tests + 1] = {
    name = name,
    callback = callback
  }
end

local function equal(actual, expected, message)
  if actual ~= expected then
    error(string.format(
      "%s\nexpected: %s\nactual:   %s",
      message or "values are not equal",
      tostring(expected),
      tostring(actual)
    ), 2)
  end
end

local function run()
  local passed = 0
  local failed = 0

  for _, item in ipairs(tests) do
    local ok, message = pcall(item.callback)
    if ok then
      passed = passed + 1
      print("PASS", item.name)
    else
      failed = failed + 1
      print("FAIL", item.name)
      print(message)
    end
  end

  print(string.format("%d passed, %d failed", passed, failed))
  return failed == 0
end

测试任务模块:

local tasks = require("tasks")

test("添加任务会生成递增 ID", function()
  local state = { nextId = 1, tasks = {} }
  local first = assert(tasks.add(state, "first"))
  local second = assert(tasks.add(state, "second"))

  equal(first.id, 1)
  equal(second.id, 2)
  equal(state.nextId, 3)
end)

test("空标题被拒绝", function()
  local state = { nextId = 1, tasks = {} }
  local task, message = tasks.add(state, "  ")

  equal(task, nil)
  equal(message, "标题不能为空")
end)

assert(run())

测试原则

  • 一次测试只验证一个行为。

  • 测试名描述行为和预期,而不是实现细节。

  • 每个测试创建独立状态,避免相互污染。

  • 覆盖成功、边界和失败路径。

  • 文件系统测试使用临时目录,并在结束时清理。

  • 不为私有实现制造大量测试,优先测试公开契约。

调试技巧

使用 traceback

local ok, message = xpcall(function()
  error("something failed")
end, debug.traceback)

if not ok then
  io.stderr:write(message, "\n")
end

debug.traceback 适合本地开发。面向用户输出时只展示稳定错误码,不要把完整内部堆栈暴露给外部。

打印结构化上下文

local function logError(operation, errorValue, context)
  io.stderr:write(string.format(
    "operation=%s error=%s request_id=%s\n",
    operation,
    tostring(errorValue),
    context.requestId or "-"
  ))
end

日志字段需要避免换行注入。处理不可信字符串时,应进行转义或使用结构化日志协议。

复现步骤

调试问题时记录:

  • Lua 版本和宿主版本。

  • 输入数据与最小复现脚本。

  • 操作系统和文件系统差异。

  • 随机种子或时间依赖。

  • 模块版本和加载路径。

不要只记录“偶发失败”。没有可重复上下文的错误很难真正修复。

性能度量

先用真实负载测量,再优化。os.clock 测量 CPU 时间:

local started = os.clock()

-- 待测量代码

local elapsed = os.clock() - started
print(string.format("CPU: %.6f seconds", elapsed))

墙上时钟可以用于观察等待时间:

local before = os.time()
-- I/O 或外部操作
local after = os.time()
print("elapsed seconds:", after - before)

os.time 精度通常只有秒。需要更高精度时,使用宿主的单调时钟 API,不要依赖系统时间回拨。

基准测试

local function benchmark(label, iterations, callback)
  local started = os.clock()

  for _ = 1, iterations do
    callback()
  end

  local elapsed = os.clock() - started
  print(string.format(
    "%s: %.6fs, %.2f us/op",
    label,
    elapsed,
    elapsed * 1e6 / iterations
  ))
end

至少执行一次预热,并使用多次测量而不是单次结果。测量时关闭调试输出,避免把日志成本算进目标代码。

常见性能陷阱

反复连接字符串

-- 较慢
local result = ""
for index = 1, 10000 do
  result = result .. tostring(index)
end

-- 较快
local parts = {}
for index = 1, 10000 do
  parts[index] = tostring(index)
end
local result = table.concat(parts)

热点循环中的临时表

频繁创建临时表会增加垃圾回收压力。可以把可复用缓冲区作为参数传入,或者只在循环外创建一次。但不要为了微优化牺牲可读性,先确认内存分配真的是瓶颈。

无效缓存

缓存应具备明确键、容量和失效策略。无上限缓存会把内存问题从 CPU 转移到进程崩溃。

过早抽象

多层代理、元表查找和动态派发会让性能分析困难。对热点路径保留简单直接的实现,并写基准测试保护关键指标。

发布前检查清单

  • 所有测试通过,错误路径有覆盖。

  • 无调试打印、硬编码密钥或测试数据。

  • 支持的最低 Lua 版本与 CI 一致。

  • 依赖版本固定,许可证合规。

  • 数据文件升级和回滚方案明确。

  • 日志不包含密码、令牌和完整个人信息。

  • 长任务支持超时或取消。

  • 退出码、标准输出和标准错误符合工具约定。

上一章:任务管理 CLI 实战 | 下一章:Lua 开发最佳实践