0

I am new in unit testing and now I am not able to call function which inside *ngFor loop of an angular 12 app. There is a observable that contains an array of objects and it works fine. Whole data is iterated properly.

This is my HTML code.

<div class="table-responsive">
    <table class="table table-striped">
        <thead>
          <tr>
            <th>#</th>
            <th>Name</th>
            <th>Category</th>
            <th>Action</th>
          </tr>
        </thead>
        <tbody>
          <tr *ngFor="let item of games$ | async; let i = index">
            <td>{{ i+1 }}</td>
            <td>{{ item.name }}</td>
            <td class="badge-cell">
               {{ item.categoryId }}
            </td>
            <td class="center action-cell">
              <button type="button"
                class="delete-game btn"
                (click)="deleteGame(item.id)">Delete</button>
            </td>
          </tr>
        </tbody>
    </table>
</div>
<!-- /.table-responsive-->

The problem rises while testing a function call inside loop. Thus, when I try to access nativeElement property of a fixture then the dynamic part of a table is null. Therefore daleteGame function is not available.

Here is the how console.log(fixture.nativeElement) look like:

<div _ngcontent-a-c74="" class="table-responsive">
    <table _ngcontent-a-c74="" id="dataTables-example" class="table table-striped">
        <thead _ngcontent-a-c74="">
            <tr _ngcontent-a-c74="">
                <th _ngcontent-a-c74="">#</th>
                <th _ngcontent-a-c74="">Name</th>
                <th _ngcontent-a-c74="">Category</th>
                <th _ngcontent-a-c74="">Action</th>
            </tr>
        </thead>
        <tbody>
         //----------NO ROWS IN THE LOG------------//
        <!--bindings={
          "ng-reflect-ng-for-of": null
        }-->
        </tbody>
    </table>
</div>

Static data is used to simulate original array(see GAME_DATA). This is how the tests look like:

describe('ListGamesComponent', () => {
  let component: ListGamesComponent;
  let fixture: ComponentFixture<ListGamesComponent>;

  beforeEach(
    waitForAsync(() => {
      TestBed.configureTestingModule({
        imports: [HttpClientTestingModule, CommonModule],
        declarations: [ListGamesComponent],
        providers: [
          RequestService,
        ],
      })
        .compileComponents()
        .then(() => {
          fixture = TestBed.createComponent(ListGamesComponent);
          component = fixture.componentInstance as any;
          component.games$ = from([GAME_DATA]) as unknown as Observable<
            IGameData[]
          >;
        });
    })
  );

  it('should create', () => {
    expect(component).toBeTruthy();
  });

  it('should check if deleteGame was called', fakeAsync(() => {
    spyOn(component, 'deleteGame');
    
    component.games$.subscribe((data: any) => {
     // ---- logged an observable, a proper list of games is returned ----
      console.log('data: ', data);
    });
    
    fixture.detectChanges();

    let button = fixture.debugElement.query(By.css('.delete-game'));
    
    button.triggerEventHandler('click', null);
    fixture.detectChanges();
    tick();

    expect(component.deleteGame).toHaveBeenCalled();
  }));
});

By the way, I logged an observable inside the test and it retrieves whole data from GAME_DATA static file, so I don't understand why the whole table is not generated.

UPDATE

This is how games$ done in component:

export class ListGamesComponent implements OnInit {

  ngOnInit(): void {
    let pager: { index: number; size: number } = {
      index: this.pageIndex,
      size: this.pageSize,
    };
    this.games$ = this.gameService.getAllGames(pager).pipe(
      tap((data) => {
        this.totalRecords = data.totalItems;
        this.gamesList = data.games;
        
        this.setPage(1, false); // generate pagination
      }),
      pluck('games')
    );
  }
}

Any help is appreciated. Thanks!

johannesMatevosyan
  • 1,974
  • 2
  • 30
  • 40

1 Answers1

1

Before we fix it, I will try to explain to you what is going wrong so you get a better understanding.

The first call of fixture.detectChanges() is when ngOnInit is called (very important to remember this).

// we change the definition of games$
component.games$ = from([GAME_DATA]) as unknown as Observable<
            IGameData[]
          >;
// we subscribe and see the data
component.games$.subscribe((data: any) => {
     // ---- logged an observable, a proper list of games is returned ----
      console.log('data: ', data);
    });
// we call fixture.detectChanges(); and this calls ngOnInit
fixture.detectChanges();

The problem is that ngOnInit re-assigns this.games$ to a service call therefore losing the definition previously assigned. That's why you see the issue. I hope that makes sense.

To fix it:

I like using createSpyObj for faking the dependencies of external services, follow the lines with !! in the comments.

describe('ListGamesComponent', () => {
  let component: ListGamesComponent;
  let fixture: ComponentFixture<ListGamesComponent>;
  // !! create a mock game service
  let mockGameService: jasmine.SpyObj<GameService>;

  beforeEach(
    waitForAsync(() => {
      // !! assign mockGameService to a spy object with public method of
      // getAllGames
      mockGameService = jasmine.createSpyObj<GameService>('GameService', ['getAllGames']);
      TestBed.configureTestingModule({
        imports: [HttpClientTestingModule, CommonModule],
        declarations: [ListGamesComponent],
        providers: [
          RequestService,
         // !! provide fake GameService for the real GameService
         { provide: GameService, useValue: mockGameService },
        ],
      })
        .compileComponents()
        .then(() => {
          fixture = TestBed.createComponent(ListGamesComponent);
          component = fixture.componentInstance as any;          
        });
    })
  );

  it('should create', () => {
    expect(component).toBeTruthy();
  });

  it('should check if deleteGame was called', fakeAsync(() => {
    spyOn(component, 'deleteGame');
    
    // !! make the mock return a value before ngOnInit is called
    mockGameService.getAllGames.and.returnValue(from([GAME_DATA]) as unknown as Observable<
            IGameData[]
          >);
   
    fixture.detectChanges();

    let button = fixture.debugElement.query(By.css('.delete-game'));
    
    button.triggerEventHandler('click', null);
    fixture.detectChanges();
    tick();

    expect(component.deleteGame).toHaveBeenCalled();
  }));
});

Check this link out on how to mock external dependencies.

AliF50
  • 16,947
  • 1
  • 21
  • 37
  • Thank you so much for detailed explanation. I tried your approach but unfortunately got `TypeError: Cannot read properties of null (reading 'triggerEventHandler')` error, since `fixture.debugElement.query` again returns `null` for that css selector. – johannesMatevosyan Jan 14 '22 at 16:43
  • Sorry, I am not sure why that is the case. – AliF50 Jan 14 '22 at 16:58