ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Meteor 单元测试框架 TinyTest 完全指南:从 package.js 配置到断言 API 实战

Meteor 单元测试框架 TinyTest 完全指南:从 package.js 配置到断言 API 实战 Meteor 单元测试框架 TinyTest 完全指南从 package.js 配置到断言 API 实战【免费下载链接】meteorMeteor, the JavaScript App Platform项目地址: https://gitcode.com/gh_mirrors/me/meteorTinyTestTinytest是 Meteor 官方提供的包级单元测试运行器服务于本地开发中的 Meteor 包测试场景。本文基于packages/tinytest的官方 README 与其源码实现系统讲解如何在Package.onTest()中接入 TinyTest、组织同步/异步测试、使用meteor test-packages命令运行并查看结果并逐条详解全部断言方法与工具方法让你能直接为自研 Meteor 包写出可运行、可维护的单元测试。TinyTest 是什么TinyTest 是 Meteor 的官方包测试运行器test runner包名为tinytest其定位与meteor test-packages命令深度绑定它用于测试本地local包即处于开发中的、位于packages/目录下的包。你不能用它直接测试通过meteor add some:package-name引入的非本地第三方包除非你将它们的仓库克隆下来并放到本地可用位置。README 中明确说明The assumption is, of course, that you are testing packages under development.默认假设你测试的就是正在开发的包。从源码看packages/tinytest/package.js描述其为 Tiny testing framework微型测试框架版本号1.4.1其依赖包括ecmascript、ejson、random、ddp、mongo、check以及 npm 包lodash.isequal并对外导出全局Tinytest对象。核心实现位于 tinytest.js由TestCaseResults断言结果收集、TestCase单个测试用例、TestManager测试注册表、TestRun测试运行器四个类构成客户端与服务器端各自通过 tinytest_client.js 与 tinytest_server.js 作为 mainModule 接入。单元测试的定位TinyTest 被专门设计用于单元测试unit testing。README 引用了 Wikipedia 对单元的定义直观地说单元可以被视为应用程序中最小的可测试部分。在过程式编程中一个单元可以是整个模块但更常见的是单个函数或过程。在面向对象编程中一个单元通常是整个接口如一个类但也可能是单个方法。理想情况下每个测试用例彼此独立。可以使用方法桩stubs、模拟对象mocks、假对象fakes和测试夹具test harnesses等替代物来辅助隔离测试某个模块。单元测试的目标是证明你自己编写功能的正确性而不是去验证构成你代码库一部分的第三方包或其他库的代码。这通常要求你在测试文件头部用stubs桩、mocks/spies模拟/间谍或fakes假对象来满足被测代码的依赖需求——它们可以是非功能性或功能受限的。除了把桩直接放在测试文件中你还可以把它们放进单独的stubs.js文件并通过api.addFiles添加或者干脆创建一个独立的 stubs 包然后在测试段中用api.use引入。README 引用了 martinfowler.com 对这三类测试替身的经典区分Fake objects 实际上有可工作的实现但通常会走某种捷径使其不适合用于生产环境内存数据库就是一个很好的例子。Stubs 为测试期间发出的调用提供预先准备好的答案通常对测试程序之外的东西一概不响应。Stubs 也可能记录调用信息比如一个记录它发送了哪些消息的邮件网关 stub或者只记录发送了多少条消息。Mocks [是] 预先编程了期望的对象这些期望构成了对它们预期收到的调用的规格说明。如果你需要更完备的 spies、stubs 等能力README 推荐查阅practicalmeteor:munit与smithy:describe——两者都在 TinyTest 之上包装了大量实用功能包括更广为使用的describe ... it ... expect语法。不过本 README 讲解的是标准 TinyTest如果你偏好那些语法和功能请参考它们各自的文档。在包中接入 TinyTestTinyTest 通过package.js文件中的Package.onTest()配置引入。下面这段来自 README 的完整示例展示了标准做法// 定义测试段 Package.onTest(function(api) { // 运行测试所需的包。 // 注意还必须包含被测包本身 (myname:mypackage)。 api.use([tinytest, underscore, ecmascript, myname:mypackage]); // 在 v1.2 中test-packages 不再全局包含任何包。 // 你可能需要将某些导出变成全局的例如 api.imply(underscore); // 这个文件包含我们要运行的测试它们会在客户端和服务器端都运行 api.addFiles(tests.js); // 可以添加任意数量的测试文件并选择它们在何处运行 api.addFiles(server-tests.js, server); // 以下这些只在客户端运行 api.addFiles(client-tests.js, client); });要点归纳必须同时引入tinytest与被测包本身api.use([tinytest, ..., myname:mypackage])缺一不可。测试段默认不包含任何全局包自 Meteor 1.2 起test-packages不再把任何包全局注入所以必要时用api.imply(underscore)之类的调用把某些包变为全局可用。api.addFiles决定测试运行位置不传架构参数表示客户端与服务器都运行传server只在服务器运行传client只在客户端运行。仓库内大量官方包正是这样组织的。例如 base64/package.js 的测试段为Package.onTest((api) { api.use([ecmascript, tinytest, ejson]); api.addFiles(base64_test.js, [client, server]); });random/package.js 则使用api.mainModule(random_tests.js)的方式引入测试文件。你可以用同样的模式为任意官方包找到对应的*_tests.js文件作为书写真实 TinyTest 用例的参考范本。test-packages 如何识别测试段从构建层看tools/cli/commands.js中的doTestCommand会设置全局变量global.testCommandMetadata {}见 tools/cli/commands.js。而 tinytest-harness/package.js 正是依赖这一全局变量来决定是否加载当global.testCommandMetadata存在即运行meteor test-packages时它才api.imply(tinytest)、api.imply(test-helpers)、api.imply(test-in-browser)并导出runTests。这就是测试驱动包在测试命令下被激活的机制。测试文件的结构测试文件通常用 JavaScript/ES5 编写如果通过api.use(some-transpiler)在Package.onTest中引入相应转译器也可以是其他语言。README 中的示例假定使用 ES2015因此你需要在Package.onTest中加入api.use(ecmascript)一行。注意目前package.js文件本身是按 JavaScript/ES5 处理的这一点无法更改。一个测试包含你想测试的代码外加零个或多个断言全部包裹在Tinytest.add或Tinytest.addAsync的回调中。测试的name名称在add或addAsync调用中指定。断言是对方法的调用用于将实际结果或实际结果的某个属性与预期结果进行比对例如结果是否 1或结果是否是一个 JavaScriptNumber。如果一个测试没有任何断言那么除非捕获到异常否则默认视为通过。但更常见的做法是显式指定断言。在任何一个测试中所有断言都必须通过整个测试才算通过。用测试名生成层级化结果页测试的name还可以用于生成层级化分组的结果页面。层级用名称中的 - 作为分隔符切分并且包名应位于name的最前面mypackage - functional tests - test 1 mypackage - functional tests - test 2 mypackage - performance tests - test 1 mypackage - performance tests - test 2以上是用于层级化报告测试结果的良好name示例。从源码看这一分组逻辑在TestCase构造函数中实现tinytest.js中this.name.split( - )后对每段做 trim最后一段成为shortName其余段组成groupPath并在最前面插入固定的tinytest段见 tinytest.js。TestRun在报告时即输出groupPath与shortName前端据此渲染树状结果。多个测试文件的执行顺序package.js中可以包含多个测试文件它们按声明的顺序被处理。测试结果则按各自分组内的出现顺序呈现。每个测试的通过/失败状态会按顺序更新包括等待异步测试完成。这一点在TestRun.run中体现TestManager维护ordered_tests数组TestRun逐个shift()取出测试串行执行见 tinytest.js。测试名必须唯一TestManager.addCase会抛出错误Every test needs a unique name, but there are two tests named 每个测试都需要唯一名称但存在两个同名测试见 tinytest.js。因此在组织测试时请确保所有测试名全局唯一。同步测试与异步测试测试可以是同步或异步的同步测试代码流程不需要等待某个外部操作的结果通过完成回调on-completion callback来获取。异步测试代码流程需要等待某个外部操作的结果通过完成回调来获取。同步测试更常见但异步测试偶尔很有用。随着 Meteor Promises运行在 fiber 中的 promise的引入我们现在可以在同步测试中成功编写包含异步代码的测试但这类用法超出该 README 本章节的范围。同步测试Tinytest.add(name, (test) { // 测试体 });异步测试回调风格Tinytest.addAsync(name, (test, onComplete) { someAsyncRequest((error, result) { // 测试体 onComplete(); // 异步函数完成时调用 } });异步测试Promise / async 风格你可以不显式调用onComplete回调而是从测试函数中返回一个Promise这样就能方便地使用async测试函数Tinytest.addAsync(name, async (test) { test.equal(shouldReturnFoo(), foo); const bar await shouldReturnBarAsync(); test.equal(bar, bar); });同步/异步的源码实现从 tinytest.js 源码看Tinytest.add实际上是把同步函数包进addAsync中自动补一个onComplete()调用tinytest.jsTinytest.add function (name, func, options) { Tinytest.addAsync(name, function (test, onComplete) { func(test); onComplete(); }, options); };而TestCase.run会通过Meteor._runFresh(() this.func(results, resolve))执行测试函数若返回结果带then方法即 Promise则等待其完成见 tinytest.js。这正是async测试函数得以工作的底层原理。另外值得注意的两个服务端细节见TestRun._runOnetinytest.js服务器端测试串行执行即使有多个客户端同时提交测试TestManager.testQueue基于Meteor._AsynchronousQueue保证同一时刻只有一个测试在服务器上运行。三分钟超时保护服务器端每个测试最多运行 3 分钟3 * 60 * 1000ms超时后报告 test timed out避免失败测试锁死服务器。运行测试meteor test-packages测试通过运行开发应用并附带test-packages命令来执行。默认测试所有包也可以通过指定名称只测试特定包。meteor test-packages—— 对应用中所有本地包运行测试。meteor test-packages her:package his:package—— 只对her:package和his:package运行测试。结果照常呈现在浏览器端口 3000上。按上述方式运行时文件监视器file watcher处于活动状态因此你可以编辑代码测试会自动重新运行。如果不希望这样可以加上--run-once开关。tools/cli/help.txt对test-packages命令的完整说明见 tools/cli/help.txt补充了更多实用细节包可以通过名称或路径指定如果包参数包含/则从该目录加载否则按常规包搜索算法解析当前应用的packages子目录 →$METEOR_PACKAGE_DIRS目录 → 核心包按此顺序。可以同时测试任意数量的包若不指定任何包名则测试所有可用包。结果仪表盘默认 URL 为localhost:3000可通过--port修改同时会占用 N1 与 N2 端口。常用选项包括--open, -o启动时打开浏览器窗口、--inspect[-brk][port]启用服务器端调试--inspect-brk会在启动时暂停等待调试客户端连接默认端口 9229、--production模拟生产模式压缩打包 CSS/JS、--settings, -s、--ios/--android/--ios-device/--android-device在移动端模拟器/设备上运行客户端与服务器端测试都会运行、--test-app-path设置用于测试的临时应用目录默认为系统临时目录通常是 /tmp、--no-lint每次测试应用重建时不运行被测试包使用的 linter、--extra-packages附加包逗号分隔如--extra-packages package-name1, package-name21.2.3、--driver-package指定测试驱动包来运行测试并展示结果例如--driver-package meteortesting:mocha。此外commands.js中testCommandOptions还支持--exclude逗号分隔的包名列表用于在测试所有包时排除某些包、--filter, -f等价于环境变量TINYTEST_FILTER只运行名称包含该子串的测试等见 tools/cli/commands.js。--filter会被写入process.env.TINYTEST_FILTERtools/cli/commands.js服务端在加载时读取并写入__meteor_runtime_config__.tinytestFilterTestManager.addCase据此跳过名称不包含该子串的测试见 tinytest.js 与 tinytest.js。测试结果如何从服务器流向浏览器TinyTest 使用 DDP 在客户端与服务器之间传输测试结果客户端调用Tinytest._runTestsEverywhere见 tinytest_client.js它订阅ServerTestResultsSubscription即tinytest_results_subscription见 model.js并通过Meteor.call(tinytest/run, runId, pathPrefix)触发服务器运行测试。服务器端的tinytest/run方法会先清空所有 MongoDB 集合然后运行Tinytest._runTests把每个报告通过发布句柄实时推送handle.changed(...)给客户端最后发送complete报告并清理资源见 tinytest_server.js。这是编辑代码后浏览器结果自动刷新背后的数据链路。只运行特定测试你可以临时把Tinytest.add或Tinytest.addAsync替换为Tinytest.only或Tinytest.onlyAsync这样只有用only*添加的测试会被执行。当只有少数几个测试失败时这很有帮助可以让你专注在它们身上。源码中Tinytest.only/Tinytest.onlyAsync通过{ isOnly: true }选项实现TestManager会收集所有isOnly测试名一旦存在这样的测试就只保留这些测试见 tinytest.js 与 tinytest.js。断言 API 详解上述回调中的test对象用于向测试添加断言。基本用法如下Tinytest.add(mypackage - basic tests, (test) { // 从被测函数获取结果 const result myFunction(1); // 执行测试……用 1 调用 myFunction 应返回 2 test.equal(result, 2); });测试可以包含多个断言。每个测试的成功与失败总数都会被报告。带可选失败消息的断言可选message可以添加到断言中它会把失败报告从fail - failure-test变为fail - failure-test - message optional message。equaltest.equal(actual, expected[, message[, not]]);按类型和值比较因此 2 不等于 2。not是布尔true/false参数可用于反转语义equal变成notEqual。notEqual内部使用它。不要在测试中使用它——只会让代码更难读懂。README 特别指出在检查某个值是否严格为true或false时equal比isTrue/isFalse是更好的选择因为isTrue/isFalse只做 truthy/falsy 判断不检查类型。从源码看tinytest.jsequal的实现细节非常丰富如果actual与expected都是字符串会转调_stringEqual获得更好的失败展示效果。如果expected是 DOM 节点带nodeType的对象则做字面比较。如果expected是Uint8Array类型化数组则做逐元素手动比较——源码注释指出 lodash 的_.isEqual在 Chrome 上处理Uint8Array时会卡死渲染进程因此回退到手动比较。其他情况使用EJSON.equals(expected, actual)做深度比较因此能正确处理 EJSON 可序列化的对象与日期等类型。notEqualtest.notEqual(actual, expected[, message]);内部调用equal并把not标志设为true。matchestest.matches(actual, regexp[, message]);检查regexp.test(actual)。源码中失败信息会携带actual与regexp.toString()见 tinytest.js。notMatchestest.notMatches(actual, regexp[, message]);检查!regexp.test(actual)。isTruetest.isTrue(actual[, message]);检查actual是否为 truthy不检查类型是否为Boolean。注意这与equal(actual, true)有区别。isFalsetest.isFalse(actual[, message]);检查actual是否为 falsey不检查类型是否为Boolean。isNulltest.isNull(actual[, message]);检查actual null。isNotNulltest.isNotNull(actual[, message]);检查!(actual null)。isUndefinedtest.isUndefined(actual[, message]);检查actual undefined。isNotUndefinedtest.isNotUndefined(actual[, message]);检查!(actual undefined)。isNaNtest.isNaN(actual[, message]);检查isNaN(actual)。isNotNaNtest.isNotNaN(actual[, message]);检查!(isNaN(actual))。lengthtest.length(obj, expected_length[, message]);检查obj.length expected_length。instanceOftest.instanceOf(obj, klass[, message]);检查obj instanceof klass。notInstanceOftest.notInstanceOf(obj, klass[, message]);检查!(obj instanceof klass)。includetest.include(haystack, needle[, message[, not]]);haystack可以是array、object或string。相应地needle可以是元素仅限简单元素、键方法名或属性名或子串。not是布尔true/false参数可用于反转语义include变成notInclude。notInclude内部使用它。不要在测试中使用它。源码实现tinytest.js数组用s.some(it isEqual(v, it))即基于lodash.isequal的深度相等判断元素存在性对象用v in s键存在性字符串用indexOf子串存在性。notIncludetest.notInclude(haystack, needle[, message]);内部调用include并把not标志设为true。_stringEqualtest._stringEqual: function (actual, expected[, message]);实验性的字符串比较方式能在测试运行器中得到更好的展示效果例如多行字符串。其失败事件类型为string_equal直接携带expected与actual字段见 tinytest.js。不带可选失败消息的断言test.throws(func, expected[, message]); test.throwsAsync(func, expected[, message]); test.doesNotThrows(func[, failureMessage]); test.doesNotThrowsAsync(func[, failureMessage]);expected可以是undefined接受任何异常。string如果该字符串是异常消息的子串则通过。regexp如果异常消息通过该正则则通过。function把该函数作为谓词用异常对象调用它。doesNotThrows和doesNotThrowsAsync断言函数不会抛出异常。如果函数抛出了断言失败。可选的failureMessage仅用于标注失败。源码中_guessPredicate正是按上述四种类型把expected转成谓词函数见 tinytest.jsthrows与throwsAsync的唯一区别在于后者await f()后捕获异常见 tinytest.js因此throwsAsync可以接收 async 函数。注意Node 的assert.throws还接受一个构造函数来测试错误是否属于预期的类。但由于 JavaScript 无法区分构造函数和普通函数而且 Node 的assert.throws也接受谓词函数因此如果错误未通过构造函数的instanceof测试该构造函数随后会被当作谓词调用。结论是如果你想测试错误是否属于某个特定的类请使用谓词函数。仓库内大量官方测试正是这么用的例如 base64/base64_test.js 中test.throws(() Base64.encode(\u0100), /Not ascii/); test.throws(() Base64.decode(), /invalid base64 string/);以及 accounts-password/password_tests.js 等处的test.throws(function () { ... })用法。工具方法runIdtest.runId();返回此测试的唯一字符串 ID例如ZmXxMPyoWGFy5wEiB。源码中该 ID 在TestCaseResults构造时通过Random.id()生成见 tinytest.js适合在测试中用于区分同一测试的多次运行。exceptiontest.exception(exception);只能与异步测试一起使用。调用它来让测试因异步回调中发生的异常而失败。如果调用此函数请确保测试不会调用其回调onComplete函数测试函数不会直接抛出异常。源码中exception直接转发给onException在TestCase.run中这会触发 Promise 的 reject 分支最终报告type: exception事件并携带异常消息与堆栈见 tinytest.js 与 tinytest.js。failtest.fail(doc);可用于输出包含路径和值的详细失败消息。示例调用test.fail({ type: match-error-path, message: The path of Match.Error doesnt match., pattern: JSON.stringify(pattern), value: JSON.stringify(value), path: err.path, expectedPath, });源码中fail的实现tinytest.js还有两个实用细节值得了解支持stop_at_offset调试模式到达指定失败偏移时在浏览器端触发debugger断点若未开启调试器会弹出 alert 提示先启用浏览器调试器。在 V8Chrome 或 Node环境下会通过Error.captureStackTrace解析调用栈用启发式规则找到最外层位于:tests.js文件中的行把filename与line写入失败详情便于定位失败代码位置。expect_failtest.expect_fail();可用于把一次失败改为合格通过qualified pass。该测试会计为通过但点击它可以看到底层测试实际是失败的- expected_fail — assert_equal - expected xxx - actual yyy - not源码中expect_fail把expecting_failure置为true随后发生的ok/fail事件分别被打上was_expecting_failure标记或把事件类型改为expected_fail见 tinytest.js 与 tinytest.js。这适合用来标记已知问题但暂时不阻塞发布的用例。实战建议与补充以包名为前缀的层级命名pkg - module - case的命名方式会让结果页呈现清晰的树状结构也符合groupPath的分组实现。服务器测试串行且带超时服务端测试默认串行、单测超时上限为 3 分钟涉及慢速外部服务如真实网络请求的用例要控制耗时。用only聚焦失败用例配合--filter/TINYTEST_FILTER子串过滤--filter会同时作用于注册阶段未匹配的测试根本不会入队可以大幅缩短调试反馈循环。配合test-in-console做无头运行仓库中的 test-in-console/package.js 描述为 Run tests noninteractively, with results going to the console非交互式运行测试结果输出到控制台适合 CI 场景若需要自定义驱动可用--driver-package指定。在 Meteor 3 中编写服务端测试TinyTest 底层已全面基于 Promise 与Meteor._runFresh执行测试函数addAsync配合async/await是推荐的异步测试写法示例见 README 的 async 测试一节。相关资源包配置与依赖packages/tinytest/package.js核心实现断言、测试用例、运行器packages/tinytest/tinytest.js客户端/服务器接入与 DDP 结果传输packages/tinytest/tinytest_client.js、packages/tinytest/tinytest_server.js测试驱动包packages/tinytest-harness/package.js、packages/test-in-browser/package.jsCLI 命令实现与帮助tools/cli/commands.js、tools/cli/help.txt真实测试用例参考packages/base64/base64_test.js、packages/random/random_tests.js、packages/accounts-password/password_tests.js【免费下载链接】meteorMeteor, the JavaScript App Platform项目地址: https://gitcode.com/gh_mirrors/me/meteor创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表