bootstrap-datepicker 单元测试指南:基于 QUnit 的测试编写、运行与套件扩展
2026/9/23 14:34:33 网站建设 项目流程

bootstrap-datepicker 单元测试指南:基于 QUnit 的测试编写、运行与套件扩展

【免费下载链接】bootstrap-datepickerA datepicker for twitter bootstrap (@twbs)项目地址: https://gitcode.com/gh_mirrors/bo/bootstrap-datepicker

本篇技术指南以 bootstrap-datepicker 仓库中的 tests/README.md 为核心,系统讲解该日期选择器插件的 QUnit 单元测试体系:如何在浏览器中一键运行全部测试、如何通过 Grunt 命令行完成"代码检查 + 测试"的完整流程,以及如何按照仓库约定新增测试模块、处理闰年等年份特例用例。读完本文,你将掌握这套测试体系的目录规范、辅助工具与典型断言模式,能够独立为插件的新功能或 bug 修复补充可靠的回归测试。

测试体系概览:单元测试在这个仓库中的角色

根据 tests/README.md 的说明,bootstrap-datepicker 使用 QUnit 编写单元测试,其目标非常明确:

  • 暴露 bug:通过测试用例把潜在缺陷暴露出来,便于修复("squashing");
  • 防止 bug 复生:已修复的问题被固化为测试,杜绝回归("prevent bugs from respawning");
  • 抑制新 bug:在添加新特性、修改既有代码时,用测试把行为边界钉死,避免引入新的问题。

这是一套标准的"测试即契约"做法:测试用例不仅是验证工具,更是对插件公开 API 行为的活的文档。仓库内所有测试均放置在tests/目录下,并以 QUnit 的module(模块)概念组织测试套件——每个 JavaScript 文件对应一个模块。

快速开始:运行测试的两种方式

方式一:浏览器直接打开测试页

最快捷的途径是用浏览器打开 tests/tests.html:

测试套件会自动运行,并在页面上呈现结果。

QUnit 测试页加载后会自动执行#qunit-tests中注册的全部用例,并在页面顶部的 banner 区给出通过/失败统计。这种方式零依赖、无需构建,适合在开发过程中快速验证某个改动是否破坏了既有行为。

方式二:命令行执行grunt test

文档建议在命令行运行测试前先执行 jshint 和 jscs 代码检查(详见下文,grunt test实际上已内置这一步骤)。安装依赖后,在仓库任意位置执行:

$ grunt test

该命令的实际定义在 Gruntfile.js 中:

grunt.registerTask('test', 'Lint files and run unit tests', ['lint-js', /*'lint-css',*/ 'qunit-all']);

它由两个阶段组成:

  1. lint-js(Gruntfile.js):依次运行jshintjscs,对js/bootstrap-datepicker.jsjs/locales/*.jsGruntfile.js做语法与代码风格校验(检查规则分别来自js/.jshintrcjs/.jscsrcgrunt/.jshintrc);
  2. qunit-all(Gruntfile.js):运行qunit:main(即tests/tests.html)与qunit-timezone(即tests/timezone.html)。

其中qunit-timezone(Gruntfile.js)会在运行前强制设置process.env.TZ = 'Europe/Moscow',再执行时区相关的测试——这是为了在固定时区下验证日期标题等与时区相关的渲染行为,避免测试结果随运行机器时区漂移。

此外,package.json 将 npm 脚本test直接映射为grunt test,因此也可以使用:

$ npm test

一个容易被忽略的测试页:timezone.html

除了主测试页 tests/tests.html,仓库还维护了独立的 tests/timezone.html。它只加载suites/timezone.js一个套件,用于在固定时区(Europe/Moscow)下验证日期选择器标题渲染,例如 tests/suites/timezone.js 中断言 2015 年 8 月视图的标题为August 2015。由于时区测试结果依赖进程环境变量,它与主测试页解耦,由grunt test统一串联执行。

深入 tests.html:测试运行器的组装结构

tests/tests.html 是整个测试体系的"入口装配文件",其结构直接决定了测试的运行环境,理解它有助于定位各类问题:

  • QUnit 本体assets/qunit.cssassets/qunit.js
  • 可选的调试日志assets/qunit-logging.js被注释掉,注释说明"console.log for test failures: enable locally if you need extra debug info",本地排查失败用例时可临时启用;
  • 被测对象../node_modules/jquery/dist/jquery.slim.js(jQuery slim 版)与../js/bootstrap-datepicker.js(插件本体);
  • 专门的语言包../js/locales/bootstrap-datepicker.zh-CN.js,注释说明它被加载是为了测试options.jstitleFormat的国际化用法;
  • 测试辅助工具assets/utils.jsassets/mock.js(详见下文);
  • 测试套件清单:位于 HTML 注释<!-- Test suites -->之后的一长串<script src="suites/...">

页面 body 中按 QUnit 标准模板放置了#qunit-header#qunit-banner#qunit-testrunner-toolbar#qunit-userAgent#qunit-tests等容器,以及一个关键的#qunit-fixture容器——这是 QUnit 提供的"沙箱 DOM",每个测试执行前会自动重置,测试中创建的输入框、组件都会被挂载到它内部,从而保证用例之间互不污染。

页头还内联了一段样式,把.datepicker容器绝对定位到屏幕之外(top: -9999em; left: -9999em),确保测试过程中弹出的日期选择器不会遮挡页面、也不会干扰测试流程。

测试套件目录结构:tests/suites 全解析

按照 tests/README.md 的约定,测试文件都放在tests/suites/目录树中,一个 JS 文件对应一个 QUnit 模块。当前仓库的套件清单(均已在tests.html中注册)如下:

测试文件模块名覆盖内容
tests/suites/formats.jsFormats日期格式解析:yyyy/yymm/mdd/dMM/MDD/D及相对日期+1dtomorrow-1w+1y
tests/suites/mouse_navigation/all.jsMouse Navigation (All)鼠标交互通用行为:点击面板不关闭、点击外部关闭
tests/suites/mouse_navigation/2012.js / 2011.jsMouse Navigation 2011/2012指定年份下的鼠标导航特例(跨年、跨月选择等)
tests/suites/keyboard_navigation/all.jsKeyboard Navigation (All)键盘通用行为:TAB 关闭、配合daysOfWeekDisabled/datesDisabled的方向键导航
tests/suites/keyboard_navigation/2012.js / 2011.jsKeyboard Navigation 2011/2012指定年份下的键盘导航特例
tests/suites/touch_navigation/all.jsTouch Navigation (All)触摸交互:touchstart点击外部隐藏面板
tests/suites/component.jsComponent组件模式(input + add-on 图标)的初始化、激活、禁用与导航
tests/suites/events.jsEvents on initialization/Events事件行为:初始化不触发change/changeDate,视图切换触发changeYear
tests/suites/options.jsOptions全部配置项行为:autoclosestartView、自定义format函数等
tests/suites/inline.jsInline内联模式:从data-date属性取初值、初始化即可见
tests/suites/calendar-weeks.jsCalendar WeekscalendarWeeks选项:周数列头与每行周数单元格
tests/suites/data-api.jsDATA-APIdata-provide="datepicker"声明式初始化(input、组件、按钮、日期区间)
tests/suites/noconflict.jsNoConflict$.fn.datepicker.noConflict()命名空间让渡
tests/suites/methods.jsMethods实例方法:remove/show/hide/update的可链式调用与状态变更
tests/suites/methods_jquery.jsMethods (jQuery)jQuery 集合级方法调用及返回值语义

年份特例的目录约定

文档特别指出:如果测试包含大量年份相关用例(例如闰年与非闰年行为不同、某一年存在特定的 bug 行为),应当把模块放进独立的年份目录:

tests/suites/<new module>/<year>.js

其中<new module>是描述性模块名,<year>是对应的四位年份。仓库中 tests/suites/mouse_navigation/2011.js 与 tests/suites/keyboard_navigation/2012.js 即遵循此约定,其文件头部注释还记录了该年份的日历事实(如 "March 1, 2011 was on a Tuesday"),方便后续维护者理解用例意图。

测试辅助工具:utils.js 与 mock.js

两个辅助文件为测试提供了关键的确定性保障:

tests/assets/utils.js

  • UTCDate(...):基于Date.UTC构造日期,彻底消除本地时区对日期比较的干扰,测试中所有"期望日期"均用它生成,例如UTCDate(2012, 2, 15)表示 2012 年 3 月 15 日;
  • format_date(date):把日期格式化为YYYY-MM-DD HH:MM:SS.mmm的可读字符串;
  • datesEqual(actual, expected, message):调用QUnit.push(QUnit.equiv(...))对实际值与期望值做深度等价比较,失败时会用format_date输出可读的差异信息。这是各套件中最常用的断言函数。

tests/assets/mock.js

  • patch_date(f):临时替换全局Date构造器,把Date.now固定为某个确定时间点(测试运行结束后恢复原生Date)。tests/suites/formats.js 中测试+1d(明天)时,正是用它把"今天"钉死在 2012 年 3 月 15 日,从而断言结果为16-03-2012——没有这种打桩,相对日期测试会随真实日期漂移而永远不稳定;
  • patch_show_hide(f):临时替换$.fn.show/$.fn.hide,在调用时为元素添加/移除foo类,用于断言"show/hide 确实被调用过"。

编写新测试的完整流程

结合 tests/README.md 的指引与仓库内真实用例,新增测试需要三个步骤:

第一步:新建模块文件

如果新用例无法归入现有模块,就在tests/suites/下新建一个模块文件:

tests/suites/<new module>.js

<new module>应当是"宽泛而有描述性"的名字(如OptionsMethods),而不是某一个具体用例名。若用例与特定年份强相关,则遵循上文约定放入tests/suites/<new module>/<year>.js

第二步:按 QUnit 标准写法组织用例

每个模块文件以module(...)开头,用setup/teardown管理生命周期,用test(...)声明用例。以 tests/suites/methods.js 的骨架为例:

module('Methods', { setup: function(){ this.input = $('<input type="text" value="31-03-2011">') .appendTo('#qunit-fixture') .datepicker({format: "dd-mm-yyyy"}); this.dp = this.input.data('datepicker'); this.picker = this.dp.picker; }, teardown: function(){ this.dp.remove(); } });

几个从源码中归纳出的关键写法:

  • DOM 挂到#qunit-fixture:所有临时元素都appendTo('#qunit-fixture'),QUnit 会在每个用例前自动清空该容器(如 tests/suites/component.js 还演示了如何对 fixture 做事件监听与解绑);
  • 通过data('datepicker')获取实例:插件初始化后会把实例存入 jQuery data,测试通过this.input.data('datepicker')拿到dp,进而访问dp.picker(面板 DOM)、dp.viewDate(当前视图日期)、dp.dates(已选日期数组)等内部状态;
  • teardown 必须清理:移除面板(this.picker.remove())或调用dp.remove(),避免用例间残留 DOM 与事件;
  • 断言日期用datesEqual:比较日期对象一律使用辅助函数而非裸equal,以获得稳定的时区与格式表现。

第三步:导入 tests.html

新文件只有被导入 tests/tests.html 才会被执行。在 HTML 注释<!-- Test suites -->之后的脚本列表中追加一行:

<script src="suites/<new module>.js"></script>

(若新增的是年份目录,则为suites/<new module>/<year>.js。)完成导入后,浏览器刷新测试页或重新执行grunt test即可看到新用例运行。

典型测试模式示例

格式解析测试(tests/suites/formats.js)

该套件系统验证了每个格式标记的解析与补零行为,例如d(日,无前导零)与dd(日,带前导零):

test('dd: Day of month, leading zero.', function(){ this.input .val('2012-03-5') .datepicker({format: 'yyyy-mm-dd'}) .datepicker('setValue'); equal(this.input.val().split('-')[2], '05'); });

它还覆盖了一批颇具价值的回归用例,如dd-mm-yyyy格式下的月份溢出防护(Mar 31 解析后不得变成 Mar 01)、闰日29-02-2012yyyy-MM-dd下用数字解析月份的无限循环回归,以及assumeNearbyYear选项对两位年份的世纪归属推断。这类用例直接印证了 js/bootstrap-datepicker.js 中日期解析器的边界行为。

交互导航测试(mouse / keyboard / touch)

三套导航测试共享相似骨架:构造 input →.focus()激活面板 → 触发事件 → 用datesEqual断言dp.viewDate。例如 tests/suites/keyboard_navigation/all.js 验证在禁用周末(setDaysOfWeekDisabled('0,6'))后按左箭头,视图日期会跳过禁用日跳到 3 月 1 日;tests/suites/mouse_navigation/all.js 则验证"点击面板不隐藏、点击面板外隐藏"这一基础交互契约。

DATA-API 测试(tests/suites/data-api.js)

该套件逐项验证data-provide="datepicker"在不同宿主元素(裸 input、input-append/input-prepend 组件、按钮、input-daterange 区间)上,通过focusclick事件即可完成声明式初始化——这与插件文档中"零 JS 代码接入"的能力一一对应。

测试要点与最佳实践小结

从源码中可以归纳出这套测试体系的几条实践准则,供编写新用例时遵循:

  1. 用固定时间锚点保证确定性:凡涉及"今天/明天/相对日期"的用例,务必通过patch_date固定Date.now,否则用例在真实日期变化后会失效;
  2. 日期比较统一走datesEqual:避免时区与毫秒精度造成的误报;
  3. teardown 中清理面板与实例:每个模块的teardown都移除 picker 或调用dp.remove(),确保#qunit-fixture干净可复用;
  4. 年份特例单独成目录:闰年等年份相关行为按tests/suites/<module>/<year>.js组织,便于追溯与维护;
  5. 新套件必须注册进 tests.html:否则测试不会被任何运行入口加载;
  6. 提交前跑完整流水线grunt test会先执行 jshint/jscs 再运行全部 QUnit 用例(含固定时区的 timezone 套件),这是本仓库推荐的完整验证方式。

通过这套规范,bootstrap-datepicker 得以在迭代新功能的同时持续守住既有行为的边界——对希望为插件贡献代码或深入理解其行为的开发者而言,tests/目录本身就是一份极具参考价值的实现文档。

【免费下载链接】bootstrap-datepickerA datepicker for twitter bootstrap (@twbs)项目地址: https://gitcode.com/gh_mirrors/bo/bootstrap-datepicker

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

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

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

立即咨询