Angular Router 测试 API 详解:@angular/router/testing 中的 RouterTestingHarness 与 RouterTestingModule
2026/9/8 20:05:56 网站建设 项目流程

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)及其源码实现,完整解读该测试工具包公开的全部 API:RouterTestingHarness的每个成员、已废弃的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): Promise<RouterTestingHarness>; detectChanges(): void; readonly fixture: ComponentFixture<{ routerOutletData: WritableSignal<unknown>; }>; navigateByUrl(url: string): Promise<null | {}>; navigateByUrl<T>(url: string, requiredRoutedComponentType: Type<T>): Promise<T>; get routeDebugElement(): DebugElement | null; get routeNativeElement(): HTMLElement | null; }

各成员的用途与行为边界如下:

API类型作用关键行为(源码佐证)
create(initialUrl?)静态方法创建 harness;可选传入初始 URL,创建后先完成一次导航若 harness 已存在则抛错;initialUrl !== undefined时自动navigateByUrl(initialUrl)
navigateByUrl(url)实例方法(重载 1)触发一次导航并等待其完成返回导航后RouterOutlet激活的组件实例,未激活时返回null
navigateByUrl<T>(url, Type<T>)实例方法(重载 2)同上,且断言激活组件类型激活组件类型不匹配、或导航未激活任何组件时抛出Error
detectChanges()实例方法让 harness 的根 fixture 运行变更检测直接委托给fixture.detectChanges()
fixture只读属性harness 根组件的ComponentFixture其组件类型含routerOutletData: WritableSignal<unknown>信号
routeDebugElementgetter路由 outlet 的DebugElementoutlet 未激活(如守卫拒绝导航)时返回null
routeNativeElementgetteroutlet 的HTMLElementrouteDebugElement?.nativeElement ?? null

2.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 = signal<unknown>(undefined); }

RootCmp是 harness 自动创建的"根组件",模板里只有一个<router-outlet>,用于渲染路由组件——这正是fixture属性类型为ComponentFixture<{routerOutletData: WritableSignal<unknown>}>的来源:该类型描述的就是RootCmp对外暴露的routerOutletData信号。RootFixtureService则负责懒创建并缓存该 fixture,createHarness()中有一句硬约束:

if (this.harness) { throw new Error('Only one harness should be created per test.'); }

每个测试用例只能创建一个 harness,重复创建会直接抛错;JSDoc 同时要求配合TestBedModuleTeardownOptions设置destroyAfterEach: true以保证清理。

2.3 navigateByUrl 的底层实现:等待导航完成 + 类型断言

navigateByUrl的实现(router_testing_harness.ts)分三步:

  1. 等待导航完成:先注入Router,用afterNextNavigation(来自路由包内部的ɵafterNextNavigation)挂一个一次性 Promise,再执行router.navigateByUrl(url)await该 Promise,随后fixture.detectChanges()

  2. 取出激活组件:读取RootCmp上的RouterOutlet,若outlet.isActivated && outlet.activatedRoute.component成立,则返回outlet.component

  3. 类型断言:若调用方传入了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.navigate(router_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,可直接作为编写路由测试的模板:

基本导航并断言组件实例与 DOM:

it('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 @deprecated

export class RouterTestingModule { static withRoutes(routes: Routes, config?: ExtraOptions): ModuleWithProviders<RouterTestingModule>; static ɵfac: i0.ɵɵFactoryDeclaration<RouterTestingModule, never>; static ɵinj: i0.ɵɵInjectorDeclaration<RouterTestingModule, never>; static ɵmod: i0.ɵɵNgModuleDeclaration<RouterTestingModule, 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/testingprovideLocationMocks单独引入。

它的实现细节(router_testing_module.ts):

@NgModule({ exports: [RouterModule], providers: [ ROUTER_PROVIDERS, provideLocationMocks(), withPreloading(NoPreloading).ɵproviders, // 测试中禁用预加载 {provide: ROUTES, multi: true, useValue: []}, ], }) export class RouterTestingModule { static withRoutes(routes: Routes, config?: ExtraOptions): ModuleWithProviders<RouterTestingModule> { 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)输出能正确引用它们;同时注释明确强调这些导出需要保持稳定、不要随意重命名,因为消费方库的编译产物可能已经引用了它们。对使用者而言,这属于实现细节,无需直接使用。

五、使用前提与注意事项

  1. 必须先配置路由RouterTestingHarness本身不注册路由,需先在TestBed中通过provideRouter(...)(或RouterModule)提供路由表,否则导航后routeDebugElementnull(官方用例已验证此行为)。
  2. 每个测试一个 harness:重复调用create()会抛出 "Only one harness should be created per test."。
  3. 需要destroyAfterEach: true:harness 的 JSDoc 明确要求在ModuleTeardownOptions中开启,以保证 fixture 在用例间被销毁。
  4. 守卫拒绝导航时不要期待组件:若测试的是拒绝导航的守卫,navigateByUrl返回nullrouteDebugElementnull;此时若误用带类型断言的重载,会收到 "navigation did not activate any component" 错误。
  5. 导航中的错误处理:用例throws error if routing throws展示了配合withRouterConfig({resolveNavigationPromiseOnError: true})时,路由抛错会被转化为"导航完成但未激活组件"(navigateByUrl('e')resolve 为null),便于对错误分支做确定性断言。
  6. 旧代码迁移:使用RouterTestingModule的测试代码属于废弃 API,建议迁移到provideRouter;若确实需要Locationmock,直接引入provideLocationMocks

六、相关文件索引

内容路径
本文依据的公共 API 报告goldens/public-api/router/testing/index.api.md
harness 源码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.md

@angular/router/testing的公开面很小但职责清晰:RouterTestingHarness用"自建带<router-outlet>的根组件 + 等待导航完成 + 组件类型断言"三件套,消除了手写根组件、手动监听events、反复detectChanges等样板代码;而RouterTestingModule则作为历史包袱被标记废弃,其能力(尤其是位置 mock)已由TestBed默认行为与provideLocationMocks承接。在编写路由相关的集成测试时,优先使用provideRouter+RouterTestingHarness的组合,是当前仓库自身测试代码(packages/router/testing/test/router_testing_harness.spec.ts)所遵循的范式。

【免费下载链接】angularDeliver web apps with confidence 🚀项目地址: https://gitcode.com/GitHub_Trending/an/angular

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

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

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

立即咨询