Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Angular testing how to prevent ngOnInit call to test a method directly

Context

I have a component. Inside of it, the ngOnInit function calls another function of component to retrieve user List. I want to make two series of tets:

  • First test the ngOnInit is triggered properly and populate the user list
  • In a second time I want to test my refresh function which also call getUserList()

The first test, with ngOnInit trigger, when I call fixture.detectChanges() works properly.

Problem

My problem is when testing the refresh function: as soon as I call fixture.detectChanges(), ngOnInit is triggered and then I am unable to know where my results come from and if my refresh() function will be tested properly.

Is there any way, before my second series of tests on refresh() method, to "delete" or "block" the ngOnInit() so it's not called on fixture.detectChanges()?

I tried to look at overrideComponent but it seems it doesn't allow to delete ngOnInit().

Or is there any way to detect changes other than using fixture.detectChanges in my case?

Code

Here is the code for component, stub service and my spec files.

Component

import { Component, OnInit, ViewContainerRef } from '@angular/core';  import { UserManagementService } from '../../shared/services/global.api'; import { UserListItemComponent } from './user-list-item.component';  @Component({   selector: 'app-user-list',   templateUrl: './user-list.component.html' }) export class UserListComponent implements OnInit {   public userList = [];    constructor(     private _userManagementService: UserManagementService,       ) { }    ngOnInit() {     this.getUserList();   }    onRefreshUserList() {     this.getUserList();   }    getUserList(notifyWhenComplete = false) {     this._userManagementService.getListUsers().subscribe(       result => {         this.userList = result.objects;       },       error => {         console.error(error);               },       () => {         if (notifyWhenComplete) {           console.info('Notification');         }       }     );   } } 

Component spec file

import { NO_ERRORS_SCHEMA } from '@angular/core'; import {   async,   fakeAsync,   ComponentFixture,   TestBed,   tick,   inject } from '@angular/core/testing';  import { Observable } from 'rxjs/Observable';  // Components import { UserListComponent } from './user-list.component';  // Services import { UserManagementService } from '../../shared/services/global.api'; import { UserManagementServiceStub } from '../../testing/services/global.api.stub';  let comp:    UserListComponent; let fixture: ComponentFixture<UserListComponent>; let service: UserManagementService;  describe('UserListComponent', () => {   beforeEach(async(() => {     TestBed.configureTestingModule({       declarations: [UserListComponent],       imports: [],       providers: [         {           provide: UserManagementService,           useClass: UserManagementServiceStub         }       ],       schemas: [ NO_ERRORS_SCHEMA ]     })     .compileComponents();   }));    tests(); });  function tests() {   beforeEach(() => {     fixture = TestBed.createComponent(UserListComponent);     comp = fixture.componentInstance;      service = TestBed.get(UserManagementService);   });    it(`should be initialized`, () => {     expect(fixture).toBeDefined();     expect(comp).toBeDefined();   });    it(`should NOT have any user in list before ngOnInit`, () => {     expect(comp.userList.length).toBe(0, 'user list is empty before init');   });    it(`should get the user List after ngOnInit`, async(() => {     fixture.detectChanges(); // This triggers the ngOnInit and thus the getUserList() method      // Works perfectly. ngOnInit was triggered and my list is OK     expect(comp.userList.length).toBe(3, 'user list exists after init');   }));    it(`should get the user List via refresh function`, fakeAsync(() => {     comp.onRefreshUserList(); // Can be commented, the test will pass because of ngOnInit trigger     tick();      // This triggers the ngOnInit which ALSO call getUserList()     // so my result can come from getUserList() method called from both source: onRefreshUserList() AND through ngOnInit().     fixture.detectChanges();       // If I comment the first line, the expectation is met because ngOnInit was triggered!         expect(comp.userList.length).toBe(3, 'user list after function call');   })); } 

Stub service (if needed)

import { Observable } from 'rxjs/Observable';  export class UserManagementServiceStub {   getListUsers() {     return Observable.from([             {         count: 3,          objects:          [           {             id: "7f5a6610-f59b-4cd7-b649-1ea3cf72347f",             name: "user 1",             group: "any"           },           {             id: "d6f54c29-810e-43d8-8083-0712d1c412a3",             name: "user 2",             group: "any"           },           {             id: "2874f506-009a-4af8-8ca5-f6e6ba1824cb",              name: "user 3",             group: "any"           }         ]       }     ]);   } } 

My trials

I tried some "workaround" but I found it to be a little.... verbose and maybe overkill!

For example:

it(`should get the user List via refresh function`, fakeAsync(() => {     expect(comp.userList.length).toBe(0, 'user list must be empty');      // Here ngOnInit is called, so I override the result from onInit     fixture.detectChanges();     expect(comp.userList.length).toBe(3, 'ngOnInit');      comp.userList = [];     fixture.detectChanges();     expect(comp.userList.length).toBe(0, 'ngOnInit');      // Then call the refresh function     comp.onRefreshUserList(true);     tick();     fixture.detectChanges();      expect(comp.userList.length).toBe(3, 'user list after function call'); })); 
like image 418
BlackHoleGalaxy Avatar asked Apr 09 '17 12:04

BlackHoleGalaxy


People also ask

Why would you use a spy in a test in Angular?

Spy is a feature in Jasmine that allows you to spy on something to achieve the following goals: Monitor if a function is called along with the parameters pass to it. Override function return values or properties to simulate desired situations during tests. Override the implementation of functions completely.

Does detectChanges call ngOnInit?

fixture. detectChanges() tells Angular to run change-detection. Finally! Every time it is called, it updates data bindings like ng-if, and re-renders the component based on the updated data. Calling this function will cause ngOnInit to run only the first time it is called.

What is shallow testing in Angular?

Shallow testing skips compiling and rendering child components during test execution, makes a true unit test.


1 Answers

Preventing lifecycle hook (ngOnInit) from being called is a wrong direction. The problem has two possible causes. Either the test isn't isolated enough, or testing strategy is wrong.

Angular guide is quite specific and opinionated on test isolation:

However, it's often more productive to explore the inner logic of application classes with isolated unit tests that don't depend upon Angular. Such tests are often smaller and easier to read, write, and maintain.

So isolated tests just should instantiate a class and test its methods

userManagementService = new UserManagementServiceStub; comp = new UserListComponent(userManagementService); spyOn(comp, 'getUserList');  ... comp.ngOnInit(); expect(comp.getUserList).toHaveBeenCalled();  ... comp.onRefreshUserList(); expect(comp.getUserList).toHaveBeenCalled(); 

Isolated tests have a shortcoming - they don't test DI, while TestBed tests do. Depending on the point of view and testing strategy, isolated tests can be considered unit tests, and TestBed tests can be considered functional tests. And a good test suite can contain both.

In the code above should get the user List via refresh function test is obviously a functional test, it treats component instance as a blackbox.

A couple of TestBed unit tests can be added to fill the gap, they probably will be solid enough to not bother with isolated tests (although the latter are surely more precise):

spyOn(comp, 'getUserList');  comp.onRefreshUserList(); expect(comp.getUserList).toHaveBeenCalledTimes(1);  ...  spyOn(comp, 'getUserList'); spyOn(comp, 'ngOnInit').and.callThrough();  tick(); fixture.detectChanges();   expect(comp.ngOnInit).toHaveBeenCalled(); expect(comp.getUserList).toHaveBeenCalledTimes(1); 
like image 149
Estus Flask Avatar answered Oct 14 '22 07:10

Estus Flask