ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Angular Router 测试 API 详解:@angular/router/testing 中的 RouterTestingHarness 与 RouterTestingModule

2026/9/8 20:06:00 拓冰建站 浏览量
Angular Router 测试 API 详解:@angular/router/testing 中的 RouterTestingHarness 与 RouterTestingModule Angular Router 测试 API 详解angular/router/testing 中的 RouterTestingHarness 与 RouterTestingModule【免费下载链接】angularDeliver web apps with confidence 项目地址: https://gitcode.com/GitHub_Trending/an/angular本文基于 Angular 仓库中angular/router测试子包angular/router/testing的公共 API 报告goldens/public-api/router/testing/index.api.md及其源码实现完整解读该测试工具包公开的全部 APIRouterTestingHarness的每个成员、已废弃的RouterTestingModule以及各 API 背后的根组件、导航等待机制与组件类型校验等底层实现帮助你在 TestBed 环境中快速编写路由与路由组件的集成测试。一、这个 API 报告对应的包是什么API 报告文件开头声明了其归属// API Report File for angular/router_testing该报告由 API Extractor 自动生成文件顶部注明 Do not edit this file描述的是angular/router的 testing 入口从 packages/router/testing/src/testing.ts 的模块注释可见Entry point for all public APIs of the router/testing package。该入口导出两组核心能力export * from ./router_testing_module; // RouterTestingModule已废弃 export {RouterTestingHarness} from ./router_testing_harness;对应源码文件packages/router/testing/src/router_testing_harness.ts —RouterTestingHarness实现packages/router/testing/src/router_testing_module.ts —RouterTestingModule实现packages/router/testing/src/testing.ts — 公共入口packages/router/testing/test/router_testing_harness.spec.ts — 官方测试用例是各 API 行为的最直接证据。下面按 API 报告中的公开面逐项展开。二、RouterTestingHarness路由测试的一站式测试桩2.1 公开 API 总览API 报告中的完整签名为export class RouterTestingHarness { static create(initialUrl?: string): PromiseRouterTestingHarness; detectChanges(): void; readonly fixture: ComponentFixture{ routerOutletData: WritableSignalunknown; }; navigateByUrl(url: string): Promisenull | {}; navigateByUrlT(url: string, requiredRoutedComponentType: TypeT): PromiseT; get routeDebugElement(): DebugElement | null; get routeNativeElement(): HTMLElement | null; }各成员的用途与行为边界如下API类型作用关键行为源码佐证create(initialUrl?)静态方法创建 harness可选传入初始 URL创建后先完成一次导航若 harness 已存在则抛错initialUrl ! undefined时自动navigateByUrl(initialUrl)navigateByUrl(url)实例方法重载 1触发一次导航并等待其完成返回导航后RouterOutlet激活的组件实例未激活时返回nullnavigateByUrlT(url, TypeT)实例方法重载 2同上且断言激活组件类型激活组件类型不匹配、或导航未激活任何组件时抛出ErrordetectChanges()实例方法让 harness 的根 fixture 运行变更检测直接委托给fixture.detectChanges()fixture只读属性harness 根组件的ComponentFixture其组件类型含routerOutletData: WritableSignalunknown信号routeDebugElementgetter路由 outlet 的DebugElementoutlet 未激活如守卫拒绝导航时返回nullrouteNativeElementgetteroutlet 的HTMLElement即routeDebugElement?.nativeElement ?? null2.2 内部结构自建的根组件与根 fixture从 router_testing_harness.ts 可以看到 harness 依赖两个内部类Component({ template: router-outlet [routerOutletData]routerOutletData()/router-outlet, imports: [RouterOutlet], changeDetection: ChangeDetectionStrategy.Eager, }) export class RootCmp { ViewChild(RouterOutlet) outlet?: RouterOutlet; readonly routerOutletData signalunknown(undefined); }RootCmp是 harness 自动创建的根组件模板里只有一个router-outlet用于渲染路由组件——这正是fixture属性类型为ComponentFixture{routerOutletData: WritableSignalunknown}的来源该类型描述的就是RootCmp对外暴露的routerOutletData信号。RootFixtureService则负责懒创建并缓存该 fixturecreateHarness()中有一句硬约束if (this.harness) { throw new Error(Only one harness should be created per test.); }即每个测试用例只能创建一个 harness重复创建会直接抛错JSDoc 同时要求配合TestBed的ModuleTeardownOptions设置destroyAfterEach: true以保证清理。2.3 navigateByUrl 的底层实现等待导航完成 类型断言navigateByUrl的实现router_testing_harness.ts分三步等待导航完成先注入Router用afterNextNavigation来自路由包内部的ɵafterNextNavigation挂一个一次性 Promise再执行router.navigateByUrl(url)并await该 Promise随后fixture.detectChanges()取出激活组件读取RootCmp上的RouterOutlet若outlet.isActivated outlet.activatedRoute.component成立则返回outlet.component类型断言若调用方传入了requiredRoutedComponentType则做instanceof检查不匹配时抛出Unexpected routed component type. Expected ${requiredRoutedComponentType.name} but got ${activatedComponent.constructor.name}当导航根本没有激活任何组件例如守卫拒绝导航但调用方又期望得到组件实例时抛出Unexpected routed component type. Expected ${requiredRoutedComponentType.name} but the navigation did not activate any component.这里有一个值得注意的设计细节harness 不仅等待navigateByUrl的 Promise还额外等待下一次导航完成的信号。这在处理重定向场景时至关重要——官方测试用例waits for redirects using router.navigaterouter_testing_harness.spec.ts中guard 内部通过inject(Router).navigateByUrl(/redirect)发起二次导航且目标路由的 guard 还带 100ms 延迟RouterTestingHarness.create(test)依然能正确等到最终 URL 为/redirect。2.4 routeDebugElement / routeNativeElement 的判空逻辑routeDebugElement的实现是先取RootCmp上的RouterOutlet若 outlet 不存在或!outlet.isActivated直接返回null否则在 fixture 的debugElement树中查询componentInstance outlet.component的节点。这与navigateByUrl的 JSDoc 说明一致When testing Routes with guards that reject the navigation, the RouterOutlet might not be activated and the activatedComponent may be null。2.5 实战示例摘自仓库官方测试以下示例完整取自 packages/router/testing/test/router_testing_harness.spec.ts可直接作为编写路由测试的模板基本导航并断言组件实例与 DOMit(navigates to routed component, async () { Component({template: hello {{name}}}) class TestCmp { name world; } TestBed.configureTestingModule({providers: [provideRouter([{path: , component: TestCmp}])]}); const harness await RouterTestingHarness.create(); const activatedComponent await harness.navigateByUrl(/, TestCmp); expect(activatedComponent).toBeInstanceOf(TestCmp); expect(harness.routeNativeElement?.innerHTML).toContain(hello world); });注意测试中没有导入任何RouterTestingModule而是直接用provideRouter提供路由——这正是当前推荐姿势下文第三节解释原因。验证守卫被执行it(executes guards on the path, async () { let guardCalled false; TestBed.configureTestingModule({ providers: [ provideRouter([ { path: , canActivate: [() { guardCalled true; return true; }], children: [], }, ]), ], }); await RouterTestingHarness.create(/); expect(guardCalled).toBeTrue(); });参数变化时复用同一 harness 的二次导航it(can observe param changes on routed component with second navigation, async () { Component({template: {{(route.params | async)?.id}}, imports: [AsyncPipe]}) class TestCmp { constructor(readonly route: ActivatedRoute) {} } TestBed.configureTestingModule({ providers: [provideRouter([{path: :id, component: TestCmp}])], }); const harness await RouterTestingHarness.create(); const activatedComponent await harness.navigateByUrl(/123, TestCmp); expect(harness.routeNativeElement?.innerHTML).toContain(123); await harness.navigateByUrl(/456); expect(harness.routeNativeElement?.innerHTML).toContain(456); });组件类型断言失败的负向用例it(throws an error if the routed component instance does not match the one required, async () { // 路由指向 TestCmp却断言 OtherCmp await expectAsync(harness.navigateByUrl(/123, OtherCmp)).toBeRejected(); }); it(throws an error if navigation fails but expected a component instance, async () { // 守卫返回 false 拒绝导航却期望得到 TestCmp 实例 await expectAsync(harness.navigateByUrl(/123, TestCmp)).toBeRejected(); });此外当没有配置任何路由时harness.routeDebugElement应为nullgives null for the activatedComponent when no routes are configured用例可用于验证导航无处可去的边界场景。三、RouterTestingModule已废弃但 API 报告仍在API 报告将该类标记为// public deprecatedexport class RouterTestingModule { static withRoutes(routes: Routes, config?: ExtraOptions): ModuleWithProvidersRouterTestingModule; static ɵfac: i0.ɵɵFactoryDeclarationRouterTestingModule, never; static ɵinj: i0.ɵɵInjectorDeclarationRouterTestingModule, never; static ɵmod: i0.ɵɵNgModuleDeclarationRouterTestingModule, never, never, [typeof RouterModule]; }其中ɵfac、ɵinj、ɵmod是 Angular 编译器为 NgModule 生成的声明元数据ɵmod表明它导出RouterModule属于 Ivy 编译产物而非手写 API使用者一般无需关心。真正有意义的公开面只有withRoutes(routes, config?)。为什么废弃router_testing_module.ts 的 JSDoc 给出了官方结论UseprovideRouterorRouterModule/RouterModule.forRootinstead. This module was previously used to provide a helpful collection of test fakes, most notably those forLocationandLocationStrategy. These are generally not required anymore, asMockPlatformLocationis provided inTestBedby default. However, you can use them directly withprovideLocationMocks.即该模块当年的核心价值是为测试提供Location/LocationStrategy的 mock 实现如今TestBed默认提供MockPlatformLocation这一价值已不成立而位置 mock 可改用angular/common/testing的provideLocationMocks单独引入。它的实现细节router_testing_module.tsNgModule({ exports: [RouterModule], providers: [ ROUTER_PROVIDERS, provideLocationMocks(), withPreloading(NoPreloading).ɵproviders, // 测试中禁用预加载 {provide: ROUTES, multi: true, useValue: []}, ], }) export class RouterTestingModule { static withRoutes(routes: Routes, config?: ExtraOptions): ModuleWithProvidersRouterTestingModule { return { ngModule: RouterTestingModule, providers: [ {provide: ROUTES, multi: true, useValue: routes}, {provide: ROUTER_CONFIGURATION, useValue: config ? config : {}}, ], }; } }要点withRoutes返回ModuleWithProviders通过多提供者ROUTES注入路由表、通过ROUTER_CONFIGURATION注入可选的ExtraOptions模块本身额外提供NoPreloading确保测试环境不会触发预加载逻辑。旧式写法imports: [RouterTestingModule.withRoutes([...])]在新代码中应替换为providers: [provideRouter([...])]。四、入口文件中的内部符号再导出及其原因testing.ts 除导出两个公共 API 外还有一组带ɵɵ前缀的再导出export {RouterOutlet as ɵɵRouterOutlet} from ../../src/directives/router_outlet; export {RouterLink as ɵɵRouterLink} from ../../src/directives/router_link; export {RouterLinkActive as ɵɵRouterLinkActive} from ../../src/directives/router_link_active; export {EmptyOutletComponent as ɵɵEmptyOutletComponent} from ../../src/components/empty_outlet;源码注释解释了动机这些符号由RouterTestingModule经由RouterModule导出Angular 编译器在消费者侧对包内相对导入存在限制需要通过本入口的再导出让部分编译partial compilation输出能正确引用它们同时注释明确强调这些导出需要保持稳定、不要随意重命名因为消费方库的编译产物可能已经引用了它们。对使用者而言这属于实现细节无需直接使用。五、使用前提与注意事项必须先配置路由RouterTestingHarness本身不注册路由需先在TestBed中通过provideRouter(...)或RouterModule提供路由表否则导航后routeDebugElement为null官方用例已验证此行为。每个测试一个 harness重复调用create()会抛出 Only one harness should be created per test.。需要destroyAfterEach: trueharness 的 JSDoc 明确要求在ModuleTeardownOptions中开启以保证 fixture 在用例间被销毁。守卫拒绝导航时不要期待组件若测试的是拒绝导航的守卫navigateByUrl返回null、routeDebugElement为null此时若误用带类型断言的重载会收到 navigation did not activate any component 错误。导航中的错误处理用例throws error if routing throws展示了配合withRouterConfig({resolveNavigationPromiseOnError: true})时路由抛错会被转化为导航完成但未激活组件navigateByUrl(e)resolve 为null便于对错误分支做确定性断言。旧代码迁移使用RouterTestingModule的测试代码属于废弃 API建议迁移到provideRouter若确实需要Locationmock直接引入provideLocationMocks。六、相关文件索引内容路径本文依据的公共 API 报告goldens/public-api/router/testing/index.api.mdharness 源码packages/router/testing/src/router_testing_harness.ts测试模块源码已废弃packages/router/testing/src/router_testing_module.ts包入口packages/router/testing/src/testing.ts官方测试用例packages/router/testing/test/router_testing_harness.spec.ts包描述packages/router/testing/PACKAGE.mdangular/router/testing的公开面很小但职责清晰RouterTestingHarness用自建带router-outlet的根组件 等待导航完成 组件类型断言三件套消除了手写根组件、手动监听events、反复detectChanges等样板代码而RouterTestingModule则作为历史包袱被标记废弃其能力尤其是位置 mock已由TestBed默认行为与provideLocationMocks承接。在编写路由相关的集成测试时优先使用provideRouterRouterTestingHarness的组合是当前仓库自身测试代码packages/router/testing/test/router_testing_harness.spec.ts所遵循的范式。【免费下载链接】angularDeliver web apps with confidence 项目地址: https://gitcode.com/GitHub_Trending/an/angular创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考