测试异步 PipeTransform

Test an async PipeTransform

上下文

我有一个基本的 PipeTransform,期望它是异步的。为什么?因为我有自己的 i18n 服务(由于解析、多元化和其他限制,我自己做了)并且它 returns a Promise<string>:

@Pipe({
    name: "i18n",
    pure: false
})
export class I18nPipe implements PipeTransform {

    private done = false;

    constructor(private i18n:I18n) {
    }

    value:string;

    transform(value:string, args:I18nPipeArgs):string {
        if(this.done){
            return this.value;
        }
        if (args.plural) {
            this.i18n.getPlural(args.key, args.plural, value, args.variables, args.domain).then((res) => {
                this.value = res;
                this.done = true;
            });
        }
        this.i18n.get(args.key, value, args.variables, args.domain).then((res) => {
            this.done = true;
            this.value = res;
        });
        return this.value;
    }
}

这个管道运行良好,因为唯一延迟的调用是第一个调用(I18nService 使用延迟加载,它仅在找不到密钥时才加载 JSON 数据,所以基本上,第一个调用将被延迟,其他的是即时的但仍然是异步的)。

问题

我不知道如何使用 Jasmine 测试这个管道,因为它在我知道它可以工作的组件内部工作,但这里的目标是使用 jasmine 对此进行全面测试,这样我可以将它添加到 CI 例程中。

以上测试:

describe("Pipe test", () => {

        it("can call I18n.get.", async(inject([I18n], (i18n:I18n) => {
            let pipe = new I18nPipe(i18n);
            expect(pipe.transform("nope", {key: 'test', domain: 'test domain'})).toBe("test value");
        })));
});

失败,因为 I18nService 给出的结果是异步的,返回值在同步逻辑中未定义。

I18n Pipe test can call I18n.get. FAILED

Expected undefined to be 'test value'.

编辑:一种方法是使用 setTimeout 但我想要一个更漂亮的解决方案,以避免在任何地方添加 setTimeout(myAssertion, 100)

使用 @angular/core/testing 中的 fakeAsync。它允许您调用 tick(),它将等待所有当前排队的异步任务完成,然后再继续。这给人一种动作是同步的错觉。在调用 tick() 之后,我们可以写下我们的期望。

import { fakeAsync, tick } from '@angular/core/testing';

it("can call I18n.get.", fakeAsync(inject([I18n], (i18n:I18n) => {
  let pipe = new I18nPipe(i18n);
  let result = pipe.transform("nope", {key: 'test', domain: 'test domain'});
  tick();
  expect(result).toBe("test value");
})));

那么什么时候用fakeAsync,什么时候用async呢?这是我(大部分时间)遵循的经验法则。当我们在测试中进行异步调用时,这就是我们应该使用async的时候。 async 允许测试继续,直到所有异步调用完成。例如

it('..', async(() => {
  let service = new Servce();
  service.doSomething().then(result => {
    expect(result).toBe('hello');
  });
});

在非 async 测试中,预期永远不会发生,因为测试会在承诺的异步解决之前完成。通过调用 async,测试被包裹在一个区域中,该区域跟踪所有异步任务并等待它们完成。

当异步行为超出测试的控制范围时使用fakeAsync(就像你的情况在管道中发生一样)。在这里我们可以 force/wait 来完成对 tick() 的调用。 tick 也可以传递一个毫秒延迟,以便在需要时允许更多时间传递。

另一种选择是模拟服务并使其同步,如 中所述。单元测试时,如果测试中的组件依赖于服务中的大量逻辑,那么测试中的组件将受制于该服务是否正常工作,这有点违背 "unit" 测试的目的。在很多情况下模拟是有意义的。