【免费下载链接】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.
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 # 或指定解释器运行器的工作方式非常朴素、也非常讲究:
- 逐个隔离:按文件名通配
tests/_test_*.lua,每个套件在独立的解释器进程里运行。某个套件崩溃不会连累其他套件,也不会污染全局状态。 - 三重失败判定:只要满足以下任意一条,该套件就算失败——进程退出码非零、输出了
FAIL前缀的行、或报告了非零的失败计数。因为多数套件是靠"打印计数"而非退出码来表达结果,三重判定防止漏网之鱼。 - CI 友好:只要有一个套件失败,脚本就以非零退出码结束,可以原封不动地接入 CI 或 pre-push 钩子。
- 明确跳过:少数套件依赖 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 给出了清晰的四步流程:
- 优先测纯逻辑:解析器、归一化函数、谓词、排序、配置表——它们最便宜,也是回归的重灾区。
- 命名有约定:新建
tests/_test_<area>.lua。运行器只匹配_test_*.lua,所以_helpers.lua(工具)和_live_*.lua(手动连真实网络的脚本,如 _live_opds_browse.lua)天然不会被误跑。 - 暴露内部逻辑有规矩:如果目标逻辑是某个大模块里的
local函数,在模块末尾挂一个Module._test = { ... }表暴露出来(参考 lib/bookshelf_chip_editor.lua),并注明"运行期不使用"。 - 全绿再提交:跑一遍
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.
相关推荐
ytDownloader 快速上手:一条链接如何从几百个站点抓到视频和音频
ytDownloader 快速上手:一条链接如何从几百个站点抓到视频和音频 你手上有一套 30 集的教程,平台高峰期限流、网页端反复缓冲,周六之前必须把离线副本
桌面应用音视频WingetUI插件测试框架:如何为自定义插件编写单元测试
WingetUI插件测试框架:如何为自定义插件编写单元测试 在WingetUI的插件生态系统中,单元测试是确保自定义插件质量的关键环节。本文将介绍如何利用Win
桌面应用Readest KOReader 插件单元测试指南:基于 LuaJIT 与 busted 的 spec 测试框架解析
Readest KOReader 插件单元测试指南:基于 LuaJIT 与 busted 的 spec 测试框架解析 本指南以 readest.koplugin
前端桌面应用移动开发跨平台
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考