☰
NativeScript 仓库单元测试编写指南:基于 Vitest 与 Nx 的实践
2026/10/1 7:45:55 网站建设 项目流程

【免费下载链接】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.

项目地址:https://gitcode.com/gh_mirrors/na/NativeScript
点击查看免费下载

本指南基于 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

六、小结与最佳实践清单

综合原文档与仓库实际代码,在该仓库中编写单元测试可以遵循以下清单:

  1. 位置:测试文件以*.spec.ts命名,与被测源码同目录放置;
  2. 运行:npx nx run <package>:test,需要实时反馈加--watch,只想跑某一组用例用-t 'DescribeName'过滤;
  3. API:core 包全局启用 Vitest 全局 API,import { ... } from 'vitest'可选;vite 包globals: false,需显式导入;
  4. 隔离:用beforeEach重建被测对象、afterEach恢复全局状态,避免用例间串扰;
  5. 异步:直接写async函数并awaitPromise 后断言;
  6. 环境边界:Node 环境下只有桩(stub),需要真机/原生运行时行为时移步 e2e 套件(apps/automated等);
  7. 扩展桩:当测试需要更多原生 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.

项目地址:https://gitcode.com/gh_mirrors/na/NativeScript
点击查看免费下载
上一篇:Nuxt.js 项目中的 assets 目录详解:静态资源管理指南
下一篇:h5py文件操作详解:HDF5文件的高级使用指南

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

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

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

立即咨询