尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Angular 升级混合应用测试指南:@angular/upgrade/static/testing 双入口 API 及其 Golden 文件校验机制

Angular 升级混合应用测试指南:@angular/upgrade/static/testing 双入口 API 及其 Golden 文件校验机制 Angular 升级混合应用测试指南angular/upgrade/static/testing 双入口 API 及其 Golden 文件校验机制【免费下载链接】angularDeliver web apps with confidence 项目地址: https://gitcode.com/GitHub_Trending/an/angular本文围绕 Angular 仓库中的公共 API 报告文件 index.api.md 展开完整解读angular/upgrade/static/testing包的公开 API 表面并结合 源码实现、官方示例 与 单元测试 深入说明createAngularTestingModule与createAngularJSTestingModule两个测试助手的用法、底层注入器桥接原理以及公共 API Golden 文件的校验流程。读完本文你将掌握在混合 Angular/AngularJS 应用中对服务做双向单元测试的完整方案并理解仓库如何保证已发布包的 API 表面稳定。一、这份 API 报告文件是什么index.api.md 是 API Extractor 生成的 API 报告文件API Report File对应 npm 包angular/upgrade下的static/testing入口内部包名为angular/upgrade_static_testing。文件开头明确标注Do not edit this file. It is a report generated by API Extractor.也就是说该文件是只读快照它逐字记录了该包的公共 API 表面。当前快照完整内容如下import { Type } from angular/core; // public export function createAngularJSTestingModule(angularModules: any[]): string; // public export function createAngularTestingModule(angularJSModules: string[], strictDi?: boolean): Typeany; // (No packageDocumentation comment for this package)从中可以读出三层信息该包的公开 API 只有两个函数createAngularJSTestingModule与createAngularTestingModule均标记为publiccreateAngularTestingModule的返回类型Typeany来自angular/core因此报告中出现了import { Type } from angular/core这一行说明报告文件会如实反映公共 API 的外部类型依赖该包没有packageDocumentation注释报告中以(No packageDocumentation comment for this package)占位。按照 goldens 目录说明public-api/目录下所有 npm 发布包的 API Golden 文件都会在每个 PR 和 commit 中作为 Bazel 测试的一部分被校验。开发者可以通过以下命令检查或更新公共 API Goldenpnpm public-api:check pnpm public-api:update当源码中publicApi标记的符号签名发生变化时导出清单 生成的报告就会与该 Golden 文件产生 diffpublic-api:check会失败从而阻止无意中破坏已发布 API 表面的变更。这正是该文件在整个仓库中的核心价值它是angular/upgrade/static/testing公开 API 的契约基线。二、包结构两个测试入口的完整签名该包的公共 API 由 public_api.ts 统一导出export {createAngularTestingModule} from ./src/create_angular_testing_module; export {createAngularJSTestingModule} from ./src/create_angularjs_testing_module;对应两个源文件与两个测试 spec整体目录结构如下文件作用create_angular_testing_module.tscreateAngularTestingModule实现从 Angular 侧测试create_angularjs_testing_module.tscreateAngularJSTestingModule实现从 AngularJS 侧测试create_angular_testing_module_spec.tsAngular 入口的单元测试create_angularjs_testing_module_spec.tsAngularJS 入口的单元测试mocks.ts模拟混合应用验证双向依赖链两个函数的签名、参数与职责对比如下函数参数返回值职责createAngularTestingModule(angularJSModules: string[], strictDi?: boolean)angularJSModules要纳入配置的 AngularJS 模块名集合strictDi可选是否对 AngularJS 注入器启用strictDITypeany一个NgModule装饰的类供Angular 单元测试使用将两个框架的注入器互连但不引导混合应用createAngularJSTestingModule(angularModules: any[])angularModules要纳入配置的 Angular 模块NgModule集合string一个 AngularJS 模块名供AngularJS 单元测试使用在angular.mock.module()中使用互连两个注入器两者的共同目标是在无实际引导bootstrap混合应用的前提下把 AngularInjector与 AngularJS$injector桥接起来从而可以更快、更简单地对跨框架依赖的服务做单元测试。三、createAngularTestingModuleAngular 侧测试入口3.1 官方文档给出的使用场景源码中的 JSDoccreate_angular_testing_module.ts描述了其定位当你要单元测试一个依赖被升级upgradedAngularJS 服务的 Angular 服务时使用。它返回一个配置好的NgModule类你在配置TestBed时把它作为imports的一项传入。仓库中的官方示例位于 module.spec.ts对应文档 regionangular-setup/angular-specimport {TestBed} from angular/core/testing; import { createAngularJSTestingModule, createAngularTestingModule, } from angular/upgrade/static/testing; import {HeroesService, ng1AppModule, Ng2AppModule} from ./module; describe(HeroesService (from Angular), () { beforeEach(() { TestBed.configureTestingModule({ imports: [createAngularTestingModule([ng1AppModule.name]), Ng2AppModule], }); }); it(should have access to the HeroesService, () { const heroesService TestBed.inject(HeroesService); expect(heroesService).toBeDefined(); }); });这里导入了两个模块Ng2AppModule是混合应用中 Angular 的部分ng1AppModule是 AngularJS 的部分。配置完成后即可像平常一样通过 AngularInjector获取服务其中依赖或被依赖了升级 AngularJS 服务的服务会按需要由 AngularJS$injector实例化。示例中的被测对象是 module.ts 里的HeroesService——一个 Angular 服务其构造函数依赖 AngularJS 服务textFormatterInjectable() export class HeroesService { heroes: Hero[] [/* superman / wonder woman / thor */]; constructor(textFormatter: TextFormatter) { // Change all the hero names to title case, using the upgraded AngularJS service this.heroes.forEach((hero: Hero[]) (hero.name textFormatter.titleCase(hero.name))); } }而textFormatter是在 AngularJS 模块中注册的普通服务module.tsng1AppModule.service(textFormatter, [TextFormatter]);Ng2AppModule则通过一个 provider 把升级后的 AngularJS 服务接入 Angular 侧module.tsproviders: [ HeroesService, // Register an Angular provider whose value is the upgraded AngularJS service {provide: TextFormatter, useFactory: (i: any) i.get(textFormatter), deps: [$injector]}, ], imports: [BrowserModule, UpgradeModule],createAngularTestingModule让这段依赖$injector的工厂在TestBed中无需真实引导应用即可工作。3.2 源码实现剖析核心实现仅十余行create_angular_testing_module.tsexport function createAngularTestingModule( angularJSModules: string[], strictDi?: boolean, ): Typeany { angular .module_($$angularJSTestingModule, angularJSModules) .constant(ɵconstants.UPGRADE_APP_TYPE_KEY, UpgradeAppType.Static) .factory(ɵconstants.INJECTOR_KEY, () injector); $injector angular.injector([ng, $$angularJSTestingModule], strictDi); return AngularTestingModule; }逐句解读angular.module_($$angularJSTestingModule, angularJSModules)创建名为$$angularJSTestingModule的内部测试模块并以angularJSModules为依赖。module_是内部对 AngularJS 全局对象ɵangular1见 angular1.ts的封装调用避免与用户代码中的全局angular变量冲突。.constant(UPGRADE_APP_TYPE_KEY, UpgradeAppType.Static)向 AngularJS 注入器声明应用类型为静态升级路径static upgrade。常量键名为$$angularUpgradeAppType见 constants.tsUpgradeAppType定义于 util.ts。.factory(INJECTOR_KEY, () injector)注册键为$$angularInjectorconstants.ts的工厂返回模块侧保存的 AngularInjector实例。这样 AngularJS 侧的服务就可以按需从 Angular 注入器取对象。angular.injector([ng, $$angularJSTestingModule], strictDi)创建真实测试用的$injector只加载核心ng模块和测试模块第二个参数即 API 签名中的strictDi开启后会强制 AngularJS 服务使用可注入injectable的依赖声明形式。返回AngularTestingModule该 NgModule 的定义在同文件 L21-L26let $injector: angular.IInjectorService | null null; let injector: Injector; export function $injectorFactory() { return $injector; } NgModule({providers: [{provide: ɵconstants.$INJECTOR, useFactory: $injectorFactory}]}) export class AngularTestingModule { constructor(i: Injector) { injector i; } }它用一个模块级变量$injector保存 AngularJS 注入器并通过{provide: $injector, useFactory: $injectorFactory}在 Angular 侧提供它AngularTestingModule的构造函数则在 TestBed 实例化该模块时捕获 AngularInjector。至此双向引用在两个模块级变量中闭环Angular 侧可注入$injectorAngularJS 侧可经$$angularInjector工厂取到 AngularInjector。四、createAngularJSTestingModuleAngularJS 侧测试入口4.1 用法当反过来测试依赖被降级downgradedAngular 服务的 AngularJS 服务时使用。它返回一个 AngularJS 模块名字符串你在angular.mock.module()中传入即可。官方示例见 module.spec.tsconst {module, inject} (window as any).angular.mock; describe(HeroesService (from AngularJS), () { beforeEach(module(createAngularJSTestingModule([Ng2AppModule]))); beforeEach(module(ng1AppModule.name)); it(should have access to the HeroesService, inject((heroesService: HeroesService) { expect(heroesService).toBeDefined(); })); });配置完成后通过 AngularJS$injector获取的服务中若依赖或被依赖了降级 Angular 服务则会按需要由 Angular 根Injector实例化。示例中heroesService正是通过 module.ts 里downgradeInjectable(HeroesService)注册到 AngularJS 模块的ng1AppModule.factory(heroesService, downgradeInjectable(HeroesService) as any);4.2 源码实现剖析实现位于 create_angularjs_testing_module.tsexport function createAngularJSTestingModule(angularModules: any[]): string { return ng .module_($$angularJSTestingModule, []) .constant(ɵconstants.UPGRADE_APP_TYPE_KEY, UpgradeAppType.Static) .factory(ɵconstants.INJECTOR_KEY, [ ɵconstants.$INJECTOR, ($injector: ng.IInjectorService) { TestBed.configureTestingModule({ imports: angularModules, providers: [{provide: ɵconstants.$INJECTOR, useValue: $injector}], }); return TestBed.inject(Injector); }, ]).name; }注意方向与上一节正好对偶它同样创建名为$$angularJSTestingModule的 AngularJS 模块并注册同一个常量UPGRADE_APP_TYPE_KEY Static但其$$angularInjector工厂反向工作AngularJS 侧注入该工厂时工厂会惰性调用TestBed.configureTestingModule把调用方传入的angularModules作为imports同时用{provide: $injector, useValue: $injector}把当前 AngularJS 注入器回注给 Angular 侧最后返回TestBed.inject(Injector)工厂的返回时机被刻意推迟到 AngularJS 注入器真正请求$$angularInjector时——这正是与 angular1_providers.ts 中little dance协作的地方。返回值为.name即模块名字符串$$angularJSTestingModule符合 API 报告中createAngularJSTestingModule(angularModules: any[]): string的签名。五、双向注入器桥接关键常量与临时引用技巧理解这两个测试助手离不开三个共享常量定义于 constants.ts常量键名含义$INJECTOR$injectorL14AngularJS 注入器在 Angular 侧的提供键也是 AngularJS 侧注入器的自然键INJECTOR_KEY$$angularInjectorL29跨框架暴露对方注入器的自定义键UPGRADE_APP_TYPE_KEY$$angularUpgradeAppTypeL32标记当前为静态升级Static应用静态升级路径本身也依赖同样的桥接。angular1_providers.ts 中有一段注释非常直白// We have to do a little dance to get the ng1 injector into the module injector. // We store the ng1 injector so that the provider in the module injector can access it // Then we get the ng1 injector from the module injector, which triggers the provider to read // the stored injector and release the reference to it. let tempInjectorRef: IInjectorService | null null; export function setTempInjectorRef(injector: IInjectorService) { tempInjectorRef injector; } export function injectorFactory() { if (!tempInjectorRef) { throw new Error(Trying to get the AngularJS injector before it being set.); } const injector: IInjectorService tempInjectorRef; tempInjectorRef null; // clear the value to prevent memory leaks return injector; }即AngularJS 注入器先存入模块级临时变量再由模块注入器中的 provider 读取并立即清空引用以防内存泄漏。测试助手采用两个模块级变量互相持有的同类技巧但作用域是每 spec 一份配合TestBed.resetTestingModule()重建下一节有测试验证。同文件还导出了angular1ProvidersL41-L50把$rootScope、$compile、$parse等核心服务也桥接为 Angular 侧可注入的 provider测试助手创建的$$angularJSTestingModule正是这条链路的测试替身。测试用的双向依赖链mocks.ts 构造了一条刻意交叉的依赖链用于同时验证两个方向的跨框架实例化shoppingCart (AngularJS) - Inventory (Angular - downgraded) - serverRequest (AngularJS - upgraded) - Logger (Angular - downgraded)即同时覆盖AngularJS → Angular → AngularJS与Angular → AngularJS → Angular两条路径。其中 Angular 侧的Inventory通过{provide: serverRequest, useFactory: serverRequestFactory, deps: [$injector]}依赖$injectormocks.ts与官方示例中TextFormatter的接法如出一辙。六、源码级行为验证单元测试如何确认桥接正确两个 spec 都使用withEachNg1Versioncommon_test_helpers.ts在多个 AngularJS 版本下重复执行保证结论对 ng1 各版本稳定。Angular 入口create_angular_testing_module_spec.ts包含两条断言跨框架服务可达配置TestBed.configureTestingModule({imports: [createAngularTestingModule([app]), AppModule]})后TestBed.inject(Inventory)返回的 Angular 服务其serverRequest依赖恰好是 AngularJS 侧的serverRequestInstance——证明Angular → AngularJS方向的按需实例化成立重复使用会创建全新注入器先TestBed.resetTestingModule()再重新调用createAngularTestingModule([app])断言TestBed.inject(Injector)与TestBed.inject($INJECTOR)都不同于上一轮的实例。这解释了源码中每次调用都会重建$$angularJSTestingModule并重新创建$injector的行为也呼应了 JSDoc 中每个 spec 只调用一次该助手的约束。AngularJS 入口create_angularjs_testing_module_spec.ts则走 ngMock 流程const {inject, module} getAngularJSGlobal().mock; module(app); // 载入应用的 AngularJS 部分 module(createAngularJSTestingModule([AppModule])); // 接入 Angular 侧 let inventory: any undefined; inject(function (shoppingCart: any) { inventory shoppingCart.inventory; }); expect(inventory).toEqual(jasmine.any(Inventory));shoppingCart是 AngularJS 服务其inventory属性是被降级的 Angular 类Inventory实例——证明AngularJS → Angular方向的桥接成立。注释中特别说明必须从全局angular.mock取 mock 对象而不能从angular/upgrade内部导入因为内部模块不导出 ngMock 辅助函数。七、使用约束与最佳实践两个函数的 JSDocAngular 侧、AngularJS 侧给出了完全一致的三条重要提示使用时务必遵守只用于测试服务services不用于测试组件Components。要测试组件仍然必须真实引导一个混合应用参见UpgradeModule或downgradeModule本仓库 upgrade_module.ts 与 downgrade_module.ts该配置不会把 AngularJS digest 接到 Zone hooks 上。如果需要触发来自 Angular 侧异步事件的 AngularJS 处理器由测试作者负责调用$rootScope.$apply助手会设置全局变量来持有共享的 Angular 与 AngularJS 注入器因此每个 spec 中只调用该助手一次不要在同一个 spec 中同时使用createAngularTestingModule与createAngularJSTestingModule——两者会互相覆盖全局桥接状态。另外从实现细节可补充两个实践要点strictDi参数只对 Angular 入口有效见第三节第 4 点它会传入angular.injector(modules, strictDi)开启 AngularJS 严格 DI 检查适合在测试中尽早暴露不规范的 AngularJS 依赖声明由于桥接状态保存在模块级变量中L14-L15 的$injector/injector不同 spec 文件之间依赖 TestBed 的自动重置即可获得干净状态这与第六节重复使用创建新注入器的测试结论一致。八、小结从 Golden 文件到源码的完整链路本文从一份 17 行的 API 报告出发串起了angular/upgrade/static/testing的完整证据链契约层index.api.md 由 API Extractor 生成经pnpm public-api:check/pnpm public-api:updategoldens/README.md在每次 PR/commit 的 Bazel 测试中校验锁定public符号签名导出层public_api.ts 导出两个publicApi函数与报告逐一对应实现层create_angular_testing_module.ts 与 create_angularjs_testing_module.ts 分别以$$angularJSTestingModule$$angularInjector工厂的方式把 AngularInjector与 AngularJS$injector在无引导bootstrap场景下互连验证层两个 spec 配合 mocks.ts 的双向依赖链在多个 ng1 版本下验证了Angular → AngularJS与AngularJS → Angular两条实例化路径。在把 AngularJS 应用逐步迁移到 Angular 的过程中这两个助手让你不必为每个服务测试搭建完整的混合应用Angular 侧测试用createAngularTestingModuleAngularJS 侧测试用createAngularJSTestingModule并注意只测服务、每 spec 一次、两助手不混用三条约束即可获得更快、更隔离的单元测试环境。【免费下载链接】angularDeliver web apps with confidence 项目地址: https://gitcode.com/GitHub_Trending/an/angular创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表