Search code examples
angularjasmineangular2-testing

Angular2 async testing issue with setTimeout


I am using Angular2.0.1 and was trying to write unit tests around an angular component with some async tasks. A fairly common thing to do, I'd say. Even their latest testing examples include these kind of async tests (see here).

My own test would never be successful, though, always failing with the message

Error: Timeout - Async callback was not invoked within timeout specified by jasmine.DEFAULT_TIMEOUT_INTERVAL.

Long story short, it took me hours to pinpoint the actual source of problem. I am using the library angular2-moment and there I was using a pipe called amTimeAgo. This pipe contains a window.setTimeout(...), which is never removed. If I removed the amTimeAgo pipe, the tests succeed, otherwise they fail.

Here's some very bare bones code to reproduce the issue:

testcomponent.html:

{{someDate | amTimeAgo}}

testcomponent.ts:

import { Component } from "@angular/core";
import * as moment from "moment";

@Component({
    moduleId: module.id,
    templateUrl: "testcomponent.html",
    providers: [],
})
export class TestComponent{
    someDate = moment();

    constructor() {
    }
}

testmodule.ts

import { NgModule }      from "@angular/core";
import {MomentModule} from 'angular2-moment';
import { TestComponent } from './testcomponent';

@NgModule({
    imports: [
        MomentModule,
    ],
    declarations: [
        TestComponent,
    ]
})
export class TestModule {
}

testcomponent.spec.ts:

import { async, TestBed, ComponentFixture } from "@angular/core/testing";
import { TestComponent } from './testcomponent';
import { TestModule } from './testmodule';

let component: TestComponent;
let fixture: ComponentFixture<TestComponent>;

function createComponent() {
    fixture = TestBed.createComponent(TestComponent);
    component = fixture.componentInstance;

    fixture.detectChanges();
    return Promise.resolve();
}

describe("TestComponent", () => {
    beforeEach(async(() => {
        TestBed.configureTestingModule({
            imports: [
                TestModule],
        }).compileComponents();
    }));

    it('should load the TestComponent', async(() => {
        createComponent().then(() => {
            expect(component).not.toBe(null);            
        });
    }));

});

Does anyone have an idea how to successfully test this? Can I somehow kill all the "left-over" timeouts in an afterEach? Or can I somehow reset the zones the async code runs in to get rid of this issue?

Has anybody else run into this or know how to successfully test this? Any hints would be appreciated.

UPDATE: After @peeskillet hinted at a solution using fixture.destroy(), I went off and tried this in my actual tests (the examples here are just the minimum code required to reproduce the issue). The actual tests contain nested promises, otherwise I wouldn't have required the async and detectChanges approach.

While the destroy suggestion is great and helps with the problem in the simple tests, my actual tests contain the following statement to ensure that the nested promises are properly resolved:

it('should check values after nested promises resolved', async(() => {
    createComponent().then(() => {
        fixture.whenStable().then(() => {
            component.selectedToolAssemblyId = "2ABC100035";

            expect(component.selectedToolAssembly).toBeDefined();
            expect(component.selectedToolAssembly.id).toBe("2ABC100035");

            fixture.destroy();
        });
        fixture.detectChanges();
    });
}));

The problem is that, with the amTimeAgo pipe in the page, the fixture.whenStable() promise is never resolved, so my assertion code is never executed and the test still fails with the same timeout.

So even though the destroy suggestion works on the given simplified tests, it does not enable me to fix the actual tests.

Thanks

Ben


Solution

  • For reference: here is the problem pipe in question

    I think the problem is that the component never gets destroyed in the async zone while there are pending async tasks, in this case the pipe's. So the pipe's ngOnDestroy (which removes the timeout) never gets called, and timeout is left hanging, which leaves the zone waiting.

    There are a couple things that will make it work:

    1. I don't know what else you have in your component, but just from what you are showing, the test doesn't need to be using async. The only reason it does is because you are returning a promise from your createComponent method. If you forget the promise (or just call the method without thening) then the test will be synchonous and no need for async. The component gets destroyed after the test finishes. The test passes.

    2. This is the better solution though: Simply destroy the component yourself!

       fixture.destroy();
      

      Everyone's happy!

    I tested both of these solutions, and they both work.


    UPDATE

    So the agreed upon solution for this particular case is to just mock the pipe. The pipe doesn't affect any of the component's behavior, so we shouldn't really care what it does, as it's only for display. The pipe itself is already tested by the author of the library, so there's no need for us to need to test its behavior within our component.

    @Pipe({
      name: 'amTimeAgo'
    })
    class MockTimeAgoPipe implements PipeTransform {
      transform(date: Date) {
        return date.toString();
      }
    }
    

    Then just take out the MomentModule from the TestBed configuration, and add the MockTimeAgoPipe to the declarations