传统观点可能建议测试作者应该存根外部依赖项(例如ActivatedRoute或RouterLink) ,并监视服务和服务方法(例如navigateByUrl)。这种测试策略的辩护可能是
“依赖真正的路由器会让它们变得脆弱。他们可能会因与组件无关的原因而失败。”
这并不完全正确,但现在我们将指出这一辩护中一个经常被忽视的非常重要的细节。根据文档:
“一系列不同的测试可以探索应用程序在存在影响警卫的条件(例如用户是否经过身份验证和授权)的情况下是否按预期导航。”
重要的是,仍然存在一套测试来覆盖执行依赖项的实际代码所产生的行为。不幸的是,这通常没有做到。当使用模拟编写的单元测试也可以轻松涵盖这些行为时,它会给开发人员带来维护额外测试套件的负担。 API 会随着时间的推移而变化,因此为所有依赖项编写和维护一套完整的模拟和存根通常比仅使用真实的模拟和存根要困难得多且脆弱得多。
测试依赖于 Angular Router
组件和服务可能很困难。例如,组件可能会注入ActivatedRoute并仅使用它来访问queryParams
的Observable
。让Router
创建一个用于测试的ActivatedRoute
的真实实例将需要相当多的样板文件。例如:
Route
( {path: ‘**', component: MyComponent}
)Router
添加到TestBed
( TestBed.configureTestingModule
)ComponentFixture
,而不是MyComponent
Router
使用MyComponent
导航到Route
ComponentFixture
查询MyComponent
直到第 6 步,您才最终拥有可用于执行测试断言的组件。有了所有这些复杂的样板,测试作者可能想要创建一个快捷方式并共享ActivatedRoute
的测试存根并覆盖TestBed
中的可注入,这是有道理的。
RouterTestingHarness在 Angular 15.2 中发布,用于简化依赖于Router
组件和服务的测试。
// Provide the router with your test route
TestBed.configureTestingModule({
providers: [
provideRouter([{path: '', component: TestCmp}])
],
});
// Create the testing harness
const harness = await RouterTestingHarness.create();
// Navigate to the route to get your component
const activatedComponent = await harness.navigateByUrl('/', TestCmp);
请注意,现在的样板文件是多么少。无需自己定义包装器组件,无需创建包装器固定装置,也无需手动查询测试组件。所有这些都是在线束的幕后完成的。
让我们介绍一个测试场景,并将模拟/存根和间谍方法与新RouterTestingHarness
测试进行比较。
ActivatedRoute
让我们测试一个简单的组件,它显示名为 search 的查询参数的值,并提供一种让路由器更新该参数的方法:
@Component({
standalone: true,
imports: [AsyncPipe],
template: `search: {{(route.queryParams | async)?.search}}`
})
class SearchCmp {
constructor(readonly route: ActivatedRoute, readonly router: Router) {}
searchFor(searchText: string) {
return this.router.navigate([], {queryParams: {'search': searchText}});
}
}
现在我们编写一个测试,确保模板使用参数的当前值进行更新。首先,我们需要创建存根:
const activatedRouteStub = {queryParams: new BehaviorSubject<any>({})};
const routerStub = jasmine.createSpyObj('router', ['navigate']);
routerStub.navigate.and.callFake(
(commands, navigationExtras) =>
activatedRouteStub.queryParams.next(navigationExtras.queryParams));
接下来,我们设置测试模块并创建我们的组件:
TestBed.configureTestingModule({
imports: [SearchCmp],
providers: [
{provide: Router, useValue: routerStub},
{provide: ActivatedRoute, useValue: activatedRouteStub},
]
});
const fixture = TestBed.createComponent(SearchCmp);
最后,让我们编写一个快速测试,以确保模板使用当前搜索查询参数进行更新:
await fixture.componentInstance.searchFor('books');
fixture.detectChanges();
expect(fixture.nativeElement.innerHTML).toContain('search: books');
RouterLink
, RouterLink
会从存根创建一个 url。存根通常不会被设置为完全满足接口。如果 Router/RouterLink 访问应该存在但不在存根上的新属性,则Router
/ RouterLink
中的重构可能会导致测试中断。ActivatedRoute
的其他属性也会破坏测试。回想一下之前的论点:
“依赖真正的路由器会让它们变得脆弱。”
事实上,这些测试更脆弱,因为它们依赖于组件的实现细节。有一个很好的论点认为这些测试实际上是有害的变更检测器测试。组件实现中的重构不会影响最终行为或呈现的模板,但会导致测试失败,这可能表明您进行了更改检测器测试。
此外,如果Router
行为发生变化,从而改变了组件与Router
API 交互的结果,您可能会希望测试失败。如果没有失败,测试就无法捕获行为差异,这可能会导致生产错误。
我们将采用相同的示例并将其转换为使用新的RouterTestingHarness
。首先,配置测试模块以使用SearchCmp
为Router
提供路由:
TestBed.configureTestingModule({
providers: [
provideRouter([{path: '**', component: SearchCmp}])
],
});
接下来,使用harness获取组件实例:
const harness = await RouterTestingHarness.create();
const activatedComponent = await harness.navigateByUrl('/', SearchCmp);
最后,我们像以前一样进行更新和断言:
await activatedComponent.searchFor(‘books’);
harness.detectChanges();
expect(harness.routeNativeElement?.innerHTML).toContain('search: books');
当以这种方式编写测试时,我们至少得到了一些不错的结果:
Router
执行,我们不必担心重写存根以实现类似的行为。使用RouterTestingHarness
而不是模拟或存根Router
依赖项将使测试更加可靠。上面的示例仅概述了一种场景,但还有更多示例,其中模拟或存根会使测试变得更加脆弱和/或隐藏由于上游Router
更改而导致的应用程序中的实际生产问题。
原文链接:https://blog.angular.dev/write-better-tests-without-router-mocks-stubs-bf5fc95c1c57