6
votes

I have a basic component class, inside ngOnInit it subscribes to a service call

ngOnInit() {
    this.activityService.getActivities().subscribe(activities => {
        console.log('inside sub');
        this.recentActivities = activities;
    });
}

So I included this console.log to see if this subscribe ever executes.

In my test I spyOn this activity service method to return an Observable collection of the data I need. Then in the test, I do the whole fixture.detectChanges() deal and expect my array to be of length 1.

fit('should populate recent activities', () => {
    const activities = [new ActivityBuilder()
        .withId('1').withRecentActivityActionId(RecentActivityActionType.Created).build()
    ];
    spyOn(activityService, 'getActivities').and.returnValue(of(activities));

    fixture.detectChanges();
    fixture.whenStable().then(() => {
        fixture.detectChanges();
    });
    fixture.detectChanges();

    expect(component.recentActivities.length).toBe(1);
});

According to the Angular docs, they pretty much do the same thing, I even tried the fakeAsync approach, but to no avail. What am I missing here? Why does the subscribe block not execute?

1
Please include all the relevant sections of your .spec file, including where you set up your TestBed, any beforeEach() functions that affect this spec, and variables declared. Even better would be a link to a stackblitz. Trying to take a swag based on what you showed so far I would guess that you haven't gotten a hold of the actual activityService that was actually injected into the component being tested.dmcgrandle
Seeing the full context would be incredibly valuable to provide a more accurate recommendation. Once you've got the test spec passing, you can probably delete a few instances of your fixture.detectChanges(). The initial invocation triggers the ngOnInit behavior, which you need. However, any additional calls to fixture.detectChanges are only relevant if you intend to make assertions about the rendered HTML (change detection in test specs is off by default).ericksoen

1 Answers

16
votes

After hours of trying fakeAsync, providing mock class for the service, various tick() calls, and dozens of other posts and slighty different approaches, the culprit came down to adding one line: component.ngOnInit();.

Apparently, while fixture.detectChanges() certainly calls ngOnInit, it was somehow keeping the observable cold. When I would call fixture.detectChanges(), it would not go into the subscribe of the observable. As soon as I added component.ngOnInit(), the observable became hot, and the code inside the subscribe was executed, and I could see my console logs being printed. Oddly, this made the following fixture.detectChanges() calls also jump into the subscribe code for each subsequent call.

Here is a simplified version of my spec file, hopefully this aids anyone in the future with the same problem. fakeAsync and sync zones still passed the test, I had assumed I would have needed at least a tick() inside the fakeAsync zone, but it wasn't necessary.

describe('RecentActivityComponent', () => {
let component: RecentActivityComponent;
let fixture: ComponentFixture<RecentActivityComponent>;
let activityService: ActivityService;

beforeEach(async(() => {
    TestBed.configureTestingModule({
        imports: [AccordionModule, FontAwesomeModule, HttpClientTestingModule],
        declarations: [ RecentActivityComponent ],
        providers: [AccordionConfig, ActivityService]
    }).compileComponents();
}));

beforeEach(async () => {
    fixture = TestBed.createComponent(RecentActivityComponent);
    component = fixture.componentInstance;
    activityService = TestBed.get(ActivityService);
});

it('should populate recent activities', fakeAsync( () => {
    const activities = [new ActivityBuilder()
        .withId('1').withRecentActivityActionId(RecentActivityActionType.Created).build()
    ];
    spyOn(activityService, 'getActivities').and.returnValue(of(activities));
    component.ngOnInit();
    expect(component.recentActivities.length).toBe(1);
}));

it('should populate recent activities', () => {
    const activities = [new ActivityBuilder()
        .withId('1').withRecentActivityActionId(RecentActivityActionType.Created).build()
    ];
    spyOn(activityService, 'getActivities').and.returnValue(of(activities));
    component.ngOnInit();
    expect(component.recentActivities.length).toBe(1);
});

});