Search code examples
asynchronousangularjasmineangular2-testingangular2-pipe

Test an async PipeTransform


Context

I have a basic PipeTransform, expect the fact that it is async. Why? because I have my own i18n service (because of parsing, pluralization and other constraints, I did my own) and it 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;
    }
}

This pipe works well, because the only delayed call is the very first one (the I18nService uses lazy loading, it loads JSON data only if the key is not found, so basically, the first call will be delayed, the other ones are instant but still async).

Problem

I can't figure out how to test this pipe using Jasmine, since it is working inside a component I know it works, but the goal here is to get this fully tested using jasmine, this way I can add it to a CI routine.

The above test:

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");
        })));
});

Fails because since the result given by the I18nService is async, the returned value is undefined in a sync logic.

I18n Pipe test can call I18n.get. FAILED

Expected undefined to be 'test value'.

EDIT: One way to do it would be to use setTimeout but I want a prettier solution, to avoid adding setTimeout(myAssertion, 100) everywhere.


Solution

  • Use fakeAsync from @angular/core/testing. It allows you to call tick(), which will wait for all currently queued asynchronous tasks to complete before continuing. This gives the illusion of the actions being synchronous. Right after the call to tick() we can write our expectations.

    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");
    })));
    

    So when should we use fakeAsync and when should we use async? This is the rule of thumb that I go by (most of the time). When we are making asynchronous calls inside the test, this is when we should use async. async allows to test to continue until all asynchronous calls are complete. For example

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

    In a non async test, the expectation would never occur, as the test would complete before the asynchronous resolution of the promise. With the call to async, the test gets wrapped in a zone, which keeps track of all asynchronous tasks, and waits for them to complete.

    Use fakeAsync when the asynchronous behavior is outside the control of the test (like in your case is going on in the pipe). Here we can force/wait for it to complete with the call to tick(). tick can also be passed a millisecond delay to allow more time to pass if needed.

    Another option is to mock the service and make it synchronous, as mentioned in this post. When unit testing, if your components in test are dependent on heavy logic in the service, then the component in test is at the mercy of that service working correctly, which kinda defeats the purpose of a "unit" test. Mocking makes sense in a lot of cases.