【免费下载链接】NativeScript
⚡ Write Native with TypeScript ✨ Best of all worlds (TypeScript, Swift, Objective C, Kotlin, Java, Dart). Use what you love ❤️ Angular, React, Solid, Svelte, Vue with: iOS (UIKit, SwiftUI), Android (View, Jetpack Compose), Flutter and you name it compatible.
本指南基于 NativeScript 官方仓库中面向贡献者的单元测试规范文档(.agent/skills/feat/references/WritingUnitTests.md)编写,系统讲解该仓库中*.spec.ts测试的组织方式、运行命令、编写规范与测试环境细节,并结合packages/core等包的源码、配置文件与实际测试用例进行深度印证。读完本文,你将掌握如何在该仓库中定位测试文件、使用 Nx 与 Vitest 跑通/过滤测试、编写同步与异步断言,以及理解 Node 环境下平台全局变量桩(stub)机制与 e2e 测试的边界划分。
一、测试的组织方式:与被测源码同目录的*.spec.ts
在 NativeScript 仓库中,单元测试采用 Vitest 作为测试框架,通过 Nx 按包(package)运行。测试文件是标准*.spec.ts,与被测源码同目录存放(colocated),散布在整个 workspace 的各包中。例如:
packages/core/xml/index.spec.ts紧挨着被测源码packages/core/xml/index.ts;packages/core/application/activity-lifecycle.android.spec.ts、packages/core/application/application-delegate.spec.ts、packages/core/application/scene-delegate-bridge.spec.ts、packages/core/application/window-content-resolver.spec.ts等,对应packages/core/application/目录下的各模块。
从packages/core/project.json的namedInputs配置可以看出,production输入明确排除了**/*.spec.ts,也就是说测试文件只服务于测试目标,不参与包的正式构建产物——这是 Nx 缓存与依赖图(task graph)中对测试文件的标准处理方式。
各包在project.json中通过 Nx 目标(target)暴露test任务,例如packages/core/project.json中的:
"test": { "executor": "@nx/vitest:test", "outputs": ["{options.reportsDirectory}"], "options": { "reportsDirectory": "../../coverage/packages/core" } }即由@nx/vitest:testexecutor 驱动,覆盖报告输出到coverage/packages/core下。拥有test目标的包(如core、vite)各自在包根目录维护自己的 Vitest 配置:
packages/core/vite.config.ts(core 包把 Vitest 配置放在vite.config.ts中);packages/vite/vitest.config.ts(vite 包使用独立的vitest.config.ts)。
二、运行测试:Nx 命令、watch 模式与按名称过滤
仓库的完整环境准备请参考 tools/notes/DevelopmentWorkflow.md:克隆仓库后先执行npm run setup安装本地依赖,之后既可以通过npm start打开交互菜单(输入core.test过滤到@nativescript.core.test任务后回车),也可以直接使用 Nx 命令。
运行 core 包的全部单元测试:
npx nx run core:test启用 watch 模式,改动源码或测试后自动重跑:
npx nx run core:test --watch按describe/it的名称过滤测试,例如只跑packages/core/xml/index.spec.ts中名为XmlParser的 describe 块:
npx nx run core:test -t 'XmlParser'-t是 Vitest 的--testNamePattern别名,也常与--watch组合使用:
npx nx run core:test --watch -t 'XmlParser'在packages/core/vite.config.ts中可以看到 Vitest 的核心配置(test块):
test: { watch: false, globals: true, environment: 'node', pool: 'forks', execArgv: process.env['VITEST_NO_OPT'] ? ['--max-opt=0'] : [], setupFiles: ['vitest.setup.ts'], include: ['**/*.{test,spec}.{ts,mts}'], reporters: ['default'], coverage: { reportsDirectory: '../../coverage/packages/core', provider: 'v8', }, },其中值得注意的几点:
globals: true意味着describe、it、expect、beforeEach、vi等全局可用(详见下文第三节);environment: 'node'确认测试运行在 Node 环境而非真机;include匹配*.test.ts与*.spec.ts(含.mts);pool: 'forks'使用子进程池执行测试;当设置了VITEST_NO_OPT=1环境变量时,会以--max-opt=0参数启动 worker——配置注释说明这是为了模拟 iOS 上 V8 以 jitless 方式执行 JavaScript 的行为,在基准测试(benchmark)中读数的差异非常明显;setupFiles指向packages/core/vitest.setup.ts,这是平台全局桩的关键入口;- 覆盖率由 v8 provider 收集,输出到
coverage/packages/core。
作为对比,packages/vite/vitest.config.ts中globals: false,因此 vite 包测试文件中必须显式从vitest导入 API,而 core 包则无需显式导入(但显式导入同样合法)。
三、编写测试:标准 Vitest API 与同步断言
使用标准 Vitest API 编写测试:describe组织套件、it/test定义用例、expect做断言、beforeEach做前置准备、vi做 mock。由于 core 包配置了globals: true,这些 API 在全局可用,从vitest导入是可选的。
原文档给出的最小示例(Observable的once语义):
import { Observable } from '.'; describe('Observable', () => { it('notifies a listener once', () => { const observable = new Observable(); let callCount = 0; observable.once('test', () => callCount++); observable.notify({ eventName: 'test', object: observable }); observable.notify({ eventName: 'test', object: observable }); expect(callCount).toBe(1); }); });该示例对应packages/core/data/observable/index.ts中Observable的once/notify机制:once注册的监听器在首次触发后即被移除,因此连续两次notify后callCount仍为 1。
再看仓库中真实的测试风格。packages/core/xml/index.spec.ts用describe组织Vanilla与Angular两种风味,并通过beforeEach重建XmlParser实例,保证每个用例互不污染:
describe('Vanilla', () => { let last_element = null; let last_attrs = null; let last_data = null; let parser = null; beforeEach(() => { parser = new XmlParser(function (event) { switch (event.eventType) { case ParserEventType.StartElement: last_element = event.elementName; last_attrs = event.attributes; break; case ParserEventType.Text: last_data = event.data; break; } }); }); it('handles whitespace around attribute =', () => { parser.parse("<TextField text = \n 'hello' />"); expect(last_element).toBe('TextField'); expect(last_attrs['text']).toBe('hello'); }); // ... });packages/core/utils/native-helper.ios.spec.ts则展示了beforeEach/afterEach配对保存与恢复全局状态、以as any构造轻量假对象(fake)的惯用做法——用createUIWindow/createNativeWindow工厂函数生成"身份等价"的桩对象,避免依赖真实 UIKit 类型:
beforeEach(() => { previousActiveWindow = getActiveWindow(); previousiOSWindow = getiOSWindow(); previousApplication = global.UIApplication; setActiveWindow(undefined); setiOSWindow(undefined); }); afterEach(() => { setActiveWindow(previousActiveWindow); setiOSWindow(previousiOSWindow); global.UIApplication = previousApplication; });四、异步测试:async 函数 + await
异步测试就是普通的async函数:返回或await你的 Promise,然后对结果做断言。Vitest 会自动等待返回的 Promise 结算,超时(默认 5 秒,可通过it(name, fn, timeout)的第三参数调整)则判失败。仓库中packages/core/utils/native-helper.ios.spec.ts、packages/core/application/application-delegate.spec.ts等文件均包含async () =>形态的用例。写异步用例时避免空跑或遗漏await,确保断言在 Promise resolve 之后执行。
五、测试环境:Node 下的平台全局桩与 e2e 边界
单元测试运行在 Node 中,而不是设备上。为了让 core 模块能够加载,packages/core/vitest.setup.ts对 NativeScript 的平台全局变量做了桩替换:
- 平台标志位:
global.__UNIT_TEST__ = true、global.__DEV__ = true、global.__ANDROID__ = false、global.__IOS__ = true、global.__VISIONOS__ = false、global.__APPLE__ = true、global.__COMMONJS__ = false,以及global.__CSS_PARSER__ = 'css-tree'(注释说明该值在真实应用中由打包器注入,css-tree 解析器是默认值); - 平台对象的"最小可用桩":
global.NSObject、global.NSTimer、global.NSBundle、global.NSData、global.NSString、global.NSFileManager、global.UIScreen、global.UIDevice、global.UIApplication等;其中NSData的桩会记录调用的 selector 与参数(dataWithData、dataWithBytesLength等),用于在规格测试中断言字节所有权(ownership)选择是否正确; - 加密相关桩:
vi.stubGlobal('crypto', { randomUUID: ... })用于让 crypto polyfill 顺利通过;同时提供 iOS 侧global.SecRandomCopyBytes/global.NSCCrypto与 Android 侧global.org.nativescript.winter_tc.Crypto两套桩,注释说明这样可以在同一个规格测试中通过切换__ANDROID__/__IOS__来分别覆盖wgc/crypto的平台分支; - 工具类桩:
global.interop、global.NativeClass、global.WeakRef.prototype.get等。
需要强调的是,这些桩只保证模块能被加载、纯逻辑可被验证,真实的 iOS/Android API 并不存在。如果你的测试需要更多原生表面(native surface),应该扩展packages/core/vitest.setup.ts中的桩,而不是依赖真机能力。
依赖真实原生运行时行为的功能,应该放到 e2e 测试套件中。仓库中的 e2e 应用包括apps/automated(自动化 e2e 测试)、apps/toolbox(本地开发实验与用例确认,更简单,常用)和apps/ui(更复杂的实验设置),详见 tools/notes/DevelopmentWorkflow.md 的"Running the e2e Test Apps"一节,例如:
npx nx run apps-automated:ios npx nx run apps-automated:android六、小结与最佳实践清单
综合原文档与仓库实际代码,在该仓库中编写单元测试可以遵循以下清单:
- 位置:测试文件以
*.spec.ts命名,与被测源码同目录放置; - 运行:
npx nx run <package>:test,需要实时反馈加--watch,只想跑某一组用例用-t 'DescribeName'过滤; - API:core 包全局启用 Vitest 全局 API,
import { ... } from 'vitest'可选;vite 包globals: false,需显式导入; - 隔离:用
beforeEach重建被测对象、afterEach恢复全局状态,避免用例间串扰; - 异步:直接写
async函数并awaitPromise 后断言; - 环境边界:Node 环境下只有桩(stub),需要真机/原生运行时行为时移步 e2e 套件(
apps/automated等); - 扩展桩:当测试需要更多原生 API 时,在
packages/core/vitest.setup.ts中扩展 mock,而不是在用例里临时修改全局。
掌握以上规范后,你可以为任何 core/vite 等包的模块补写规格测试,并借助 Nx 的按包执行与测试名过滤能力,在大型 workspace 中快速定位与验证行为。
【免费下载链接】NativeScript
⚡ Write Native with TypeScript ✨ Best of all worlds (TypeScript, Swift, Objective C, Kotlin, Java, Dart). Use what you love ❤️ Angular, React, Solid, Svelte, Vue with: iOS (UIKit, SwiftUI), Android (View, Jetpack Compose), Flutter and you name it compatible.
相关推荐
NativeScript 仓库单元测试编写指南:基于 Vitest 与 Nx 的 *.spec.ts 测试实践
NativeScript 仓库单元测试编写指南:基于 Vitest 与 Nx 的 .spec.ts 测试实践 导读 本文面向 NativeScript 仓库的贡
NativeScript 仓库单元测试编写指南:基于 Vitest 的 `.spec.ts` 测试实践
NativeScript 仓库单元测试编写指南:基于 Vitest 的 .spec.ts 测试实践 本篇技术指南围绕 NativeScript 开源仓库(mon
NativeScript 仓库单元测试编写指南:Vitest、Nx 与 Node 环境下的平台模拟实践
NativeScript 仓库单元测试编写指南:Vitest、Nx 与 Node 环境下的平台模拟实践 导读 本文以 tools/notes/WritingUn
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考