☰
Bookshelf插件单元测试体系全解析:370个测试如何守护这个KOReader插件
2026/10/16 2:53:31 网站建设 项目流程

【免费下载链接】bookshelf.koplugin

A nice looking home screen for KOReader. Lets you pick a book from your shelf and read it, with some customisation around the book preview info that's shown.

项目地址:https://gitcode.com/gh_mirrors/bo/bookshelf.koplugin
点击查看免费下载

Bookshelf是 KOReader 上一款高颜值的书架首页插件:让你在书架上挑一本书开始阅读,并可自定义书籍预览信息的展示方式。很少有人注意到,这个插件在tests/目录下藏着370 个单元测试套件、约 4000 次断言——从排序引擎到 OPDS 目录,从书脊排版到壁纸加载,几乎所有核心逻辑都有测试兜底。本文将带你完整看懂这套 Bookshelf 测试体系:它为什么不用完整 KOReader 环境、如何用桩代码(stub)模拟依赖、以及一条命令如何跑完全部测试。


一、为什么一个"首页插件"需要如此重的测试体系

直觉上,Bookshelf 只是个"选书界面",写错了大不了显示不好看。但翻一下它的模块目录 lib/ 就会发现,它其实承担了大量易错的工程逻辑:

  • 书籍数据管线:扫描书库、读取 Calibre 元数据、从 Kindle/Kobo 同步、按 13 种维度排序
  • 封面网络请求:多个封面源的优先级、超时控制、磁盘缓存
  • 布局排版:书脊宽度、行高、字距、竖排中文……几乎每个像素都经过计算
  • 持久化:设置项、阅读进度、OPDS 目录配置,任何一个字段写错都会损坏用户数据

tests/README.md 开篇就说明了定位:这是一套快速、无依赖的纯 Lua 单元测试,直接跑在普通的lua(或luajit)解释器里,而不是 KOReader 内部。这个选择是整个体系的基石——后文所有设计都源于此。


二、测试运行器一键跑法:如何运行 Bookshelf 全部单元测试

运行入口是 tests/run.sh,在项目根目录执行:

sh tests/run.sh # 使用 PATH 中的 lua LUA=luajit sh tests/run.sh # 或指定解释器

运行器的工作方式非常朴素、也非常讲究:

  1. 逐个隔离:按文件名通配tests/_test_*.lua,每个套件在独立的解释器进程里运行。某个套件崩溃不会连累其他套件,也不会污染全局状态。
  2. 三重失败判定:只要满足以下任意一条,该套件就算失败——进程退出码非零、输出了FAIL前缀的行、或报告了非零的失败计数。因为多数套件是靠"打印计数"而非退出码来表达结果,三重判定防止漏网之鱼。
  3. CI 友好:只要有一个套件失败,脚本就以非零退出码结束,可以原封不动地接入 CI 或 pre-push 钩子。
  4. 明确跳过:少数套件依赖 KOReader 的原生库(SQLite、ffi 等),会被标记为SKIP并注明原因,而不是静默消失——tests/run.sh 中的skip_reason()就是这份"跳过清单"。

开发时只想盯着某一个模块迭代,也可以直接单独跑一个套件:

lua tests/_test_settings_store.lua

最终报告长什么样

全部跑完后,最后一行会输出类似这样的汇总:

ran 368 suites, 0 failed, 2 skipped

任何一行FAIL都会把该套件的完整输出缩进打印出来,定位问题无需二次操作。


三、桩代码(stub):不启动 KOReader 如何测试 KOReader 插件

这是整套体系最精巧的部分。插件代码里到处是require("logger")、require("datastorage")这类 KOReader 模块,在独立 Lua 解释器里根本加载不了。

测试的答案是:在被测代码加载之前,往package.loaded里塞进替身。以最精简的 _test_sort_engine.lua 为例:

-- 把 logger 换成四个空函数 package.loaded["logger"] = { dbg = function() end, info = function() end, warn = function() end, err = function() end } -- 让 lib/ 下的真实模块可以被 require package.preload["lib/bookshelf_author_name"] = function() return dofile("lib/bookshelf_author_name.lua") end local SortEngine = dofile("lib/bookshelf_sort_engine.lua") -- 加载真实被测代码

关键取舍有两点:

  • 只 stub 依赖,不 stub 被测对象。真正运行的永远是lib/里的生产代码,测试断言的是它的真实行为,而不是对行为的"复述"。
  • 空表就够用的 UI 模块。很多 KOReader UI 模块只被require而不在加载阶段被调用,所以一个空的{}桩就足以让 UI 模块独立加载(参见 _test_chip_editor.lua)。

而面对 SQLite 这类"stub 了等于没测"的依赖,思路又不同:_helpers.lua 提供了install_hardcover_cache_fake(),用纯 Lua 表实现了一个接口完全一致的假 SQLite——被测模块真实的_cacheGet/_cachePut路径原封不动地跑,只是底层存储换成了内存。


四、测试分层:从排序算法到发布打包的四层防线

把 370 个套件按文件名前缀归类,可以清晰看到测试金字塔的分层设计:

层级典型套件测试什么
纯逻辑层_test_sort_engine.lua、_test_filter.lua、_test_color.lua排序、过滤、颜色解析等最容易藏回归的地方
数据层_test_book_facts_db.lua、_test_opds_db.lua、_test_settings_store.lua数据库读写、设置持久化
UI 几何层_test_spine_layout.lua、_test_list_geom.lua、_test_footer_geom.lua书脊宽度、行预算、焦点框位置等像素级计算
发布与卫生层_test_release_assets.lua、_test_no_stray_globals.lua、_test_meta.lua发布包完整性、全局变量泄漏、元数据契约

几个值得细看的例子:

🔤 排序引擎的"平局"测试(_test_sort_engine.lua):用户选按"最后打开时间"排序时,如果几十本书的打开时间相同,顺序会怎样?测试明确钉死了"平局时按书名→文件名兜底,顺序必须确定",对应真实问题 #205/#208——这类"多数情况下都对、边界上全乱"的缺陷,正是纯逻辑测试最擅长捕捉的。

📦 发布打包测试(_test_release_assets.lua):发布 zip 由git archive生成,只有被.gitattributes白名单放行的资产才会真正打包。这个套件直接解析.gitattributes,逐目录断言壁纸、橡木书架板等运行时必读资产都在白名单内——因为内置橡木书架板真的曾从发布包里"消失"过一次。

🌐 网络请求测试(_test_http.lua):用桩替换 luasocket 和 curl 后,断言每个调用方的 User-Agent、超时参数真的"到达了线上",并且回退路径有界——因为无限等待的 curl 回退曾被确认为会冻结 UI 的缺陷。


五、共享工具箱:_helpers.lua 如何消灭样板代码

新套件不再各自复制粘贴断言逻辑,而是共用 tests/_helpers.lua:

local helpers = dofile("tests/_helpers.lua") local t = helpers.runner() -- 微型测试运行器 local eq = helpers.eq -- 深度值相等断言 t.test("does the thing", function() eq(Unit.thing(), { 1, 2, 3 }) end) t.done() -- 打印 "PASS n FAIL n",失败则退出码非零

其中有个细节特别能体现团队的"踩坑文化":t.skip()通过抛错而非返回来工作。注释(_helpers.lua)记录了事故背景——早期有 12 个套件在 CI 上因 KOReader 环境缺失而应跳过,却因为没有 skip 机制直接调用 nil 函数,被 CI 误报成"测试失败"。跳过必须大声说出来,而不是沉默。

同一份文件里的shelf_record()辅助函数也写着一段"血泪注释":它构造的书目记录刻意保持最小字段,因为历史上恰恰是"为了让测试通过而给 fixture 加字段",才让测试在开发机上全绿、真机上却渲染出满屏破折号。


六、贡献者指南:如何为 Bookshelf 添加一个测试

tests/README.md 给出了清晰的四步流程:

  1. 优先测纯逻辑:解析器、归一化函数、谓词、排序、配置表——它们最便宜,也是回归的重灾区。
  2. 命名有约定:新建tests/_test_<area>.lua。运行器只匹配_test_*.lua,所以_helpers.lua(工具)和_live_*.lua(手动连真实网络的脚本,如 _live_opds_browse.lua)天然不会被误跑。
  3. 暴露内部逻辑有规矩:如果目标逻辑是某个大模块里的local函数,在模块末尾挂一个Module._test = { ... }表暴露出来(参考 lib/bookshelf_chip_editor.lua),并注明"运行期不使用"。
  4. 全绿再提交:跑一遍sh tests/run.sh,确认整个套件仍然通过。

此外,运行器还承担两项"外部一致性"检查:若本地旁边 checkout 了 bookends 仓库,会逐字节比对 vendored 的 token 文件(tools/check_token_parity.sh)和进度条绘制代码(tools/check_bar_parity.sh),漂移会在测试里立刻现形,而不是数月后变成 bug 报告。


七、常见问题速答

Q:为什么有的测试用lua,有的用luajit?因为lua和luajit在数字格式化、整数除法上行为不同,默认lua的运行本身是"承重"的覆盖。个别需要 ffi 的套件(如 _test_cover_disk_cache.lua)会自动切到 luajit;需要真实 SQLite 的套件则用 KOReader 自带 luajit 手动运行(tests/run.sh 注释里有现成命令)。

Q:为什么有些套件显示 SKIP 而不是运行?依赖 KOReader 原生库的套件无法在独立解释器中运行。设计原则是:宁可大声跳过,也不沉默通过——静默的"假绿"比没有测试更危险。

Q:测试代码量大概是什么规模?tests/目录 370 个_test_*.lua套件,合计约79000 行、4000+ 次断言,覆盖 30 多个功能域:书脊(24 个)、OPDS(22 个)、封面(12 个)、列表(11 个)、装饰(10 个)、Hero 卡片(10 个)、Chip 条(10 个)……与 lib/ 的 160 多个模块形成了相当完整的对应关系。


小结

Bookshelf 的测试体系给普通插件开发者提供了一份很好的范本:

  • ✅无重型依赖:一条sh tests/run.sh在任何装有 Lua 的机器上即可运行
  • ✅stub 边界清晰:替身只替换依赖,被测对象永远是生产代码
  • ✅失败大声、跳过大名:三重失败判定 + 显式跳过清单,杜绝"假绿"
  • ✅测试即事故记录:几乎每个套件头部注释都引用了它守护的真实缺陷编号

如果你是 KOReader 插件作者,或者只是想了解"一个严肃的 Lua 插件如何自证可靠",tests/ 目录本身就值得逐文件读一遍。

【免费下载链接】bookshelf.koplugin

A nice looking home screen for KOReader. Lets you pick a book from your shelf and read it, with some customisation around the book preview info that's shown.

项目地址:https://gitcode.com/gh_mirrors/bo/bookshelf.koplugin
点击查看免费下载

相关推荐

上一篇:LitGPT 权重转换实战:将 Hugging Face Checkpoint 转为 LitGPT 格式
下一篇:Dozzle 日志调试指南:用 --level 与 DOZZLE_LEVEL 定位容器日志查看器的问题

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询