Angular 2 RC5 Testing Promises in ngOnInit Not Working
Asked Answered
Q

1

2

I am trying to test a structural directive named MyDirective with Jasmine. The Angular version used is RC5.

// Part of the MyDirective class

@Directive({selector: '[myDirective]'})
export class MyDirective {
    constructor(protected templateRef: TemplateRef<any>,
                protected viewContainer: ViewContainerRef,
                protected myService: MyService) {
    }

    ngOnInit() {
        this.myService.getData()
            .then((data) => {
                if (!MyService.isValid(data)) {
                    this.viewContainer.createEmbeddedView(this.templateRef);
                } else {
                    this.viewContainer.clear();
                }
            })
            .catch((error) => {
                console.log(error);
                this.viewContainer.createEmbeddedView(this.templateRef);
            });
    }
}

The getData method is overwritten in the MockService class whereas the isValid method (a static method of MyService) is called directly, which checks the validity of the data.

// Part of the Jasmine unit test class for the MyDirective class

@Component({
    selector: 'test-cmp', template: '', directives: [MyDirective]
})
class TestComponent {}

class MockService {
    mockResponse: MyResponse = {valid date goes here};
    mockInvalidResponse: MyResponse = {};

    getData() {
        if (booleanCondition) {
            return Promise.resolve(this.mockResponse);
        } else {
            return Promise.resolve(this.mockInvalidResponse);
        }
    }
}

describe('MyDirective', () => {
    beforeEach(() => {
        TestBed.configureTestingModule({
            declarations: [TestComponent],
            providers: [
                {provide: MyService, useClass: MockService},
                TemplateRef,
                ViewContainerRef
            ]
        });
    });

    it('should remove the target DOM element when the condition is true', async(() => {
        booleanCondition = true;
        const template =
             '<div><div *myDirective><span>Hi</span></div></div>';

        TestBed.overrideComponent(TestComponent, {set: {template: template}});
        let fixture = TestBed.createComponent(TestComponent);
        fixture.detectChanges();
        expect(getDOM().querySelectorAll(fixture.debugElement.nativeElement, 'span').length).toEqual(0);
    }));

    it('should contain the target DOM element when the condition is false', async(() => {
        booleanCondition = false;
        const template =
             '<div><div *myDirective><span>Hi</span></div></div>';

        TestBed.overrideComponent(TestComponent, {set: {template: template}});
        let fixture = TestBed.createComponent(TestComponent);
        fixture.detectChanges();

        // The 'expect' bellow fails because the value is 0 for some reason
        expect(getDOM().querySelectorAll(fixture.debugElement.nativeElement, 'span').length).toEqual(1);
    }));
});

The second it is supposed to create a case in which the span element is in the DOM, but it does not. I checked to see if it goes to the first condition in the if statement like this:

if (!MyService.isValid(data)) {
        console.log('the first if condition is read.');
        this.viewContainer.createEmbeddedView(this.templateRef);
    } else {
        this.viewContainer.clear();
    }
}

And it logs it. So, it should keep the element in the DOM, but I can't find a way to test it.

Quaternion answered 11/9, 2016 at 12:44 Comment(0)
C
4

It because a Promise (the one returned from getData) is asynchronous. So all the synchronous activity gets handled ahead of the Promise activity. Even though ngOnInit is called, the Promise is resolved asynchronously.

There are a couple options that I usually use for this type thing.

One option is to use fakeAsync instead of async. This allows you to call tick to allow for asynchronous actions to complete synchronously

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

it('... when the condition is false', fakeAsync(() => {
  const template = '<div><div *myDirective><span>Hi</span></div></div>';

  TestBed.overrideComponent(TestComponent, { set: { template: template } });
  let fixture = TestBed.createComponent(TestComponent);
  fixture.detectChanges();
  // tick can also be called with a millisecond delay argument `tick(1000)`
  tick();          
  expect(getDOM().querySelectorAll(fixture.debugElement.nativeElement, 'span').length)
    .toEqual(1);
}));

Another options is to make the mocked service synchronous. You can easily do that by making the call to getData() return the service itself, and add a then and catch method to the service. For example

class MockMyService {
  data;
  error;

  getData() {
    return this;
  }

  then(callback) {
    if (!this.error) {
      callback('mockData');
    }
    return this;
  }

  catch(callback) {
    if (this.error) {
      callback(this.error);
    }
  }

  setData(data) {
    this.data = data;
  }

  setError(error) {
    this.error = error;
  }
}

One advantage of this approach is that it gives you more control over the service during the test execution. This is also very useful when testing components that use templateUrl. XHR calls can't be made in a fakeAsync, so using that is not an option. This is where the synchronous mock service comes in use.

You can either inject the service to your it test cases or you can just keep a variable in you test and set it up something like

let mockMyService: MockMyService;

beforeEach(() => {
  mockMyService = new MockMyService();
  TestBed.configureTestingModule({
    providers: [
      { provide: MyService, useValue: mockMyService }
    ]
  });
});

Note: You'll also want to fix your passing test, as your current test is not valid for reasons mentioned above.


See Also:

Consol answered 11/9, 2016 at 14:39 Comment(4)
Thanks @peeskillet! I used the first of your two solutions, and it worked as expected. Do you happen to know the difference between using Jasmine's done() (passing the done parameter) and using async? Angular's test source code uses async() instead of done(). I am trying to understand how async() recognizes all asynchronous calls are done. This might be worth another full question...Quaternion
I'm pretty sure it has to do with Zones. I am still learning zones, but I from what I currently understand, zones polyfill asynchronous methods like setTimout and such, to the point where the zone knows about all the asynchronous calls made in that zone. The zone keeps a record of all these pending tasks. Check this outConsol
If you look at the source for async, you see a lot of zone stuff going on. You'll also see there that async actually does call done when in a jasmine environmentConsol
Knowing that it might have to do with Zones made me feel better. Thanks! I will look into it when necessary. async actually calling done makes a lot of sense.Quaternion

© 2022 - 2024 — McMap. All rights reserved.