Angular测试:使用async/await与fakeAsync

21
Angular Material提供了组件挂载进行测试,它允许您通过等待承诺来与其组件交互,例如:
  it('should click button', async () => {
    const matButton = await loader.getHarness(MatButtonHarness);
    await matButton.click();
    expect(...);
  });

但如果按钮点击触发了延迟操作怎么办?通常我会使用fakeAsync()/tick()来处理:

  it('should click button', fakeAsync(() => {
    mockService.load.and.returnValue(of(mockResults).pipe(delay(1000)));
    // click button
    tick(1000);
    fixture.detectChanges();
    expect(...);
  }));

但是有没有办法在同一个测试中同时做到这两点呢?
async函数包装在fakeAsync()内会给我带来“错误:必须在fakeAsync区域中运行代码才能调用该函数”的提示,这可能是因为一旦它完成了一个await,它就不再是我传递给fakeAsync()的同一个函数了。
我需要像这样做吗——在等待后启动一个fakeAsync函数?还是有更优雅的方法?
  it('should click button', async () => {
    mockService.load.and.returnValue(of(mockResults).pipe(delay(1000)));
    const matButton = await loader.getHarness(MatButtonHarness);

    fakeAsync(async () => {
      // not awaiting click here, so I can tick() first
      const click = matButton.click(); 
      tick(1000);
      fixture.detectChanges();
      await click;
      expect(...);
    })();
  });
4个回答

18

fakeAsync(async () => {...}) 是一个有效的结构。

而且,Angular Material 团队正在 明确测试这个场景

it('should wait for async operation to complete in fakeAsync test', fakeAsync(async () => {
        const asyncCounter = await harness.asyncCounter();
        expect(await asyncCounter.text()).toBe('5');
        await harness.increaseCounter(3);
        expect(await asyncCounter.text()).toBe('8');
      }));

1
好的,谢谢。看起来这个问题在我发布几个月后就被解决了。 - JW.
3
这是有效的,但也很危险。如果您的await应用于微任务,则没有任何问题,因为fakeAsync会在最后刷新微任务。但如果是宏任务,您将会遇到错误。tickawait之后就无法帮助您了,因为它已经在该宏任务中执行了。 - rainerhahnekamp

6

在从Angular 12升级到14后,以前可以正常运行的测试开始失败了。具体失败的测试取决于fakeAsyncasync

在我的情况下,解决方法是将以下target添加到tsconfig.spec.json中:

{
  "extends": "./tsconfig.json",
  "compilerOptions": {
    "outDir": "./out-tsc/spec",
    "module": "CommonJs",
    "target": "ES2016", // Resolved fakeAsync + async tests errors
    "types": ["jest"]
  },
  "include": ["src/**/*.spec.ts", "src/**/*.d.ts"]
}

一个人为的测试例子:
it('should load button with exact text', fakeAsync(async () => {
  const buttons = await loader.getAllHarnesses(
    MatButtonHarness.with({ text: 'Testing Button' })
  );

  tick(1000);

  expect(buttons.length).toBe(1);
  expect(await buttons[0].getText()).toBe('Testing Button');
}));

我遇到了以下错误,并且它直接指向了 tick(1000)

必须在 fakeAsync 区域中运行代码才能调用此函数

ES2016 目标添加到我的 tsconfig.spec.json 后,所有问题都得到了解决。

我使用 Jest,因此使用其他测试运行程序的人可能没有相同的解决方案。


谢谢,这解决了我的问题。在升级到Jest 29.5.0后,与tick()相关的fakeAsync错误出现了,而在Jest 28.x下通过的测试也没问题。 - pete19
我现在真想亲你一口。有人知道为什么这样能解决问题吗? - secondbreakfast
它可以工作,但它将你的测试和“真实”代码编译到不同的目标上。因此,你测试的是不同的“编译”代码。这不是最好的解决方案。当你升级你的“真实”代码(当你升级你的编译目标)时,你应该同时升级你的测试代码。 - undefined
我认为这与Angular中的一个已知问题有关,即ZoneJS在ES2017及更高版本中无法工作:https://github.com/angular/angular/issues/31730 - undefined

2
我刚刚发布了一个测试助手,可以完全满足您的需求。除其他功能外,它允许您在fakeAsync测试中使用材料挂件,并像您所描述的那样控制时间的流逝。
该助手会自动在假异步区域中运行您传递给其.run()方法的内容,并且它可以处理async/await。它看起来像这样,在您创建ctx助手代替TestBed.createComponent()(无论您何时完成)的地方:
it('should click button', () => {
  mockService.load.and.returnValue(of(mockResults).pipe(delay(1000)));
  ctx.run(async () => {
    const matButton = await ctx.getHarness(MatButtonHarness);
    await matButton.click();
    ctx.tick(1000);
    expect(...);
  });
});

这个库叫做@s-libs/ng-dev。查看此特定助手的文档在这里,如果有任何问题,请通过github在这里告诉我。


1

您不需要在fakeAsync内部使用(真正的)async,至少不需要为了控制模拟时间的流程。 fakeAsync 的目的是允许您将await替换为tick / flush。 当您实际需要值时,我认为您会被迫回归到then,就像这样:

  it('should click button', fakeAsync(() => {
    mockService.load.and.returnValue(of(mockResults).pipe(delay(1000)));
    const resultThing = fixture.debugElement.query(By.css("div.result"));
    loader.getHarness(MatButtonHarness).then(matButton => {
      matButton.click(); 
      expect(resultThing.textContent).toBeFalsy(); // `Service#load` observable has not emitted yet
      tick(1000); // cause observable to emit
      expect(resultThing.textContent).toBe(mockResults); // Expect element content to be updated
    });
  }));

现在,由于您的测试体函数在调用fakeAsync时内部执行,因此它应该:1)不允许测试完成,直到所有创建的Promise(包括getHarness返回的Promise)都被解决,2)如果有任何待处理任务,则测试失败。
(顺便说一下,如果您正在使用服务返回的Observable并使用async管道,则我认为您不需要在第二个expect之前进行fixture.detectChanges(),因为async管道在其内部订阅触发时明确地插入所有者的更改检测器。但如果我错了,我会很感兴趣知道。)

为什么使用 then 要优于 async/await?这似乎很愚蠢。 - Frederick
2
fakeAsync 通过猴子补丁运行时全局 Promise 对象的行为,但浏览器中没有钩子来修改 async/await 语句的行为。这就是为什么 你不能发出原生的异步函数而不破坏 Zone 的原因。在规范中使用 await 语句,在运行之前由转译器降级可能是可以的。 - Coderer

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接