Angular小博客

分享让你更聪明

[译2023]无需路由器模拟/存根即可编写更好的测试

浏览:1次 日期:2024年10月21日 21:09:27 作者:admin

事先建议

传统观点可能建议测试作者应该存根外部依赖项(例如ActivatedRouteRouterLink) ,并监视服务和服务方法(例如navigateByUrl)。这种测试策略的辩护可能是

“依赖真正的路由器会让它们变得脆弱。他们可能会因与组件无关的原因而失败。”

这并不完全正确,但现在我们将指出这一辩护中一个经常被忽视的非常重要的细节。根据文档:

“一系列不同的测试可以探索应用程序在存在影响警卫的条件(例如用户是否经过身份验证和授权)的情况下是否按预期导航。”

重要的是,仍然存在一套测试覆盖执行依赖项的实际代码所产生的行为。不幸的是,这通常没有做到。当使用模拟编写的单元测试也可以轻松涵盖这些行为时,它会给开发人员带来维护额外测试套件的负担。 API 会随着时间的推移而变化,因此为所有依赖项编写和维护一套完整的模拟和存根通常比仅使用真实的模拟和存根要困难得多且脆弱得多。

使用真实路由器实例进行测试的样板

测试依赖于 Angular Router组件和服务可能很困难。例如,组件可能会注入ActivatedRoute并仅使用它来访问queryParamsObservable 。让Router创建一个用于测试的ActivatedRoute的真实实例将需要相当多的样板文件。例如:

  1. 为被测组件编写一个Route ( {path: ‘**', component: MyComponent} )
  2. Router添加到TestBed ( TestBed.configureTestingModule )
  3. 创建一个带有路由器出口的包装组件来渲染该组件
  4. 为包装器组件创建测试ComponentFixture ,而不是MyComponent
  5. 告诉Router使用MyComponent导航到Route
  6. 使用包装器ComponentFixture查询MyComponent

直到第 6 步,您才最终拥有可用于执行测试断言的组件。有了所有这些复杂的样板,测试作者可能想要创建一个快捷方式并共享ActivatedRoute的测试存根并覆盖TestBed中的可注入,这是有道理的。

RouterTestingHarness 提供帮助

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');

问题

回想一下之前的论点:

“依赖真正的路由器会让它们变得脆弱。”

事实上,这些测试脆弱,因为它们依赖于组件的实现细节。有一个很好的论点认为这些测试实际上是有害的变更检测器测试。组件实现中的重构不会影响最终行为或呈现的模板,但会导致测试失败,这可能表明您进行了更改检测器测试。

此外,如果Router行为发生变化,从而改变了组件与Router API 交互的结果,您可能会希望测试失败。如果没有失败,测试就无法捕获行为差异,这可能会导致生产错误。

使用 RouterTestingHarness 代替

我们将采用相同的示例并将其转换为使用新的RouterTestingHarness 。首先,配置测试模块以使用SearchCmpRouter提供路由:

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');

当以这种方式编写测试时,我们至少得到了一些不错的结果:

包起来

使用RouterTestingHarness而不是模拟或存根Router依赖项将使测试更加可靠。上面的示例仅概述了一种场景,但还有更多示例,其中模拟或存根会使测试变得更加脆弱和/或隐藏由于上游Router更改而导致的应用程序中的实际生产问题。

原文链接:https://blog.angular.dev/write-better-tests-without-router-mocks-stubs-bf5fc95c1c57