Eric
Eric

Reputation: 43

Angular - unit test failed for a subscribe function in a component

Hello I spend a lot of time trying to find out a solution without posting something but I don't understand what's going wrong, so I decided to post question.

Here the file I want to test : home.page.ts

import { Component, OnInit } from '@angular/core';
import { distinctUntilChanged } from 'rxjs/internal/operators/distinctUntilChanged';
import { AlertController, LoadingController } from '@ionic/angular';
import { DataService } from '../service/data.service';
import { League } from '../interfaces/League';
import { LeaguesImageLink } from '../types/leaguesImgLink.enum';
import { ActivatedRoute, Router } from '@angular/router';

@Component({
  selector: 'app-home',
  templateUrl: 'home.page.html',
  styleUrls: ['home.page.scss'],
})
export class HomePage implements OnInit {
  searchTerm: string = '';
  leaguesItems: League[] = [];

  constructor(
    private apiService: ApiService,
    private route: ActivatedRoute,
    private router: Router,
    private dataService: DataService,
    private loadingCtrl: LoadingController,
    private alertController: AlertController
  ) { }

  ngOnInit() {
    this.loadLeagues();
  }

  async loadLeagues(): Promise<void> {
    (await this.showSpinner()).present
    this.apiService
      .getAllLeagues()
      .pipe(distinctUntilChanged())
      .subscribe((res: League[]) => {
        this.leaguesItems = res;
        this.addImgLink();
      });
    (await this.showSpinner()).dismiss()
  }

  async showSpinner(): Promise<HTMLIonLoadingElement> {
    const loading = await this.loadingCtrl.create({
      message: 'Loading..',
      spinner: 'bubbles',
    });
    return loading;
  }
  addImgLink(): void {
    this.leaguesItems = this.leaguesItems.map((item: League) => {
      return {
        ...item,
        imgLink:
          LeaguesImageLink[
          item.name.split(' ').join('') as keyof typeof LeaguesImageLink
          ],
      };
    });
  }

  async showAlert(): Promise<void> {
    const alert = await this.alertController.create({
      header: "Alert",
      message: "Il n'y a pas encore d'équipes !",
      buttons: ['OK'],
    });

    await alert.present();
  }

  setLeaguesData(league: League): void {
    if (league.teams?.length === 0) {
      this.showAlert()
      return;
    } else {
      this.dataService.setleaguesData(this.leaguesItems);
      this.router.navigate(['../teams', league._id], {
        relativeTo: this.route,
      });
    }
  }
}

here my test file home.page.spect.ts

import { MockLoadingController } from './../../mocks/IonicMock';
import { League } from './../interfaces/League';
import { mockLeagueArray, mockLeagueArrayImageLink } from './../../mocks/mockLeagues';
import { ApiService } from 'src/app/service/api.service';
import { FormsModule } from '@angular/forms';
import { Ng2SearchPipeModule } from 'ng2-search-filter/';
import { HttpClientTestingModule } from '@angular/common/http/testing';
import {
  ComponentFixture,
  fakeAsync,
  TestBed,
  tick,
  waitForAsync,
} from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';

import { HomePage } from './home.page';
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';

import { IonicModule } from '@ionic/angular';

import { of } from 'rxjs';

fdescribe('HomePage', () => {
  let component: HomePage;
  let fixture: ComponentFixture<HomePage>;
  let apiService: ApiService;

  beforeEach(async () => {
    await TestBed.configureTestingModule({
      declarations: [HomePage],
      teardown: { destroyAfterEach: false },
      schemas: [CUSTOM_ELEMENTS_SCHEMA],
      imports: [
        IonicModule.forRoot(),
        HttpClientTestingModule,
        RouterTestingModule,
        Ng2SearchPipeModule,
        FormsModule,
      ],
      providers: [
        ApiService
      ]
    }).compileComponents();

    fixture = TestBed.createComponent(HomePage);
    component = fixture.componentInstance;
    apiService = TestBed.inject(ApiService)

    fixture.detectChanges();
  });

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

  it('should call ngOnInit', () => {
    let loadLeagues = spyOn(component, 'loadLeagues');
    component.ngOnInit();
    expect(loadLeagues).toHaveBeenCalled();
  });


  it('should call getAllLeagues service and fill leaguesItems array', fakeAsync(() => {
    //ARRANGE
    spyOn(apiService, 'getAllLeagues').and.returnValue(of(mockLeagueArray))

    //ACT
    component.loadLeagues()
    tick(1000)
    fixture.detectChanges()

    //ASSERT
    expect(component.leaguesItems).toEqual(mockLeagueArray)
  }))
});

my mock file : mockLeagues.ts

import { League } from "src/app/interfaces/League";

export const mockLeagueArray: League[] = [{
  _id: "1",
  name: "Supercopa de Espana",
  sport: "sport1",
  teams: ["t1", "t11"],
}, {
  _id: "2",
  name: "English Premier League",
  sport: "sport2",
  teams: ["t2", "t22"],
}]

export const mockLeagueArrayImageLink: League[] = [{
  _id: "1",
  name: "Supercopa de Espana",
  sport: "sport1",
  teams: ["t1", "t11"],
  imgLink: "https://www.thesportsdb.com/images/media/league/badge/sp4q7d1641378531.png",
}, {
  _id: "2",
  name: "English Premier League",
  sport: "sport2",
  teams: ["t2", "t22"],
  imgLink: "https://www.thesportsdb.com/images/media/league/badge/pdd43f1610891709.png",
}]

Nothing fancy ! And the response I get when I run ng test :

Chrome 107.0.0.0 (Windows 10) HomePage should call getAllLeagues service and fill leaguesItems array FAILED
        Expected $.length = 0 to equal 2.
        Expected $[0] = undefined to equal Object({ _id: '1', name: 'Supercopa de Espana', sport: 'sport1', teams: [ 't1', 't11' ] }).
        Expected $[1] = undefined to equal Object({ _id: '2', name: 'English Premier League', sport: 'sport2', teams: [ 't2', 't22' ] }).
            at <Jasmine>
            at UserContext.apply (src/app/home/home.page.spec.ts:81:36)
            at UserContext.apply (node_modules/zone.js/fesm2015/zone-testing.js:2030:30)
            at _ZoneDelegate.invoke (node_modules/zone.js/fesm2015/zone.js:372:26)
            at ProxyZoneSpec.onInvoke (node_modules/zone.js/fesm2015/zone-testing.js:287:39)
Chrome 107.0.0.0 (Windows 10): Executed 3 of 12 (1 FAILED) (skipped 9) (0.799 secs / 0.601 secs)
TOTAL: 1 FAILED, 2 SUCCESS

I spend 7 days (reading angular doc, checking on internet..) , now I'm done, someone has a solution ?

Thanks.

Upvotes: 1

Views: 380

Answers (1)

AliF50
AliF50

Reputation: 18809

Should present not have brackets?

// !! Like this
(await this.showSpinner()).present();

Also, this distinctUntilChanged does nothing because what is being emitted is an array and every time it emits, it will be a new array. It's like saying [] === [], it will always be false so it will always emit. Check out With Object Streams here: https://www.leonelngande.com/an-in-depth-look-at-rxjss-distinctuntilchanged-operator/ but for you it is an array. You can do more research and you can pass a callback in distinctUntilChanged to fine tune when you want it to emit and when you don't.

If I were you, I would change loadLeagues like so:

async loadLeagues(): Promise<void> {
    // (await this.showSpinner()).present
    this.apiService
      .getAllLeagues()
       // !! This distinctUntilChanged does nothing since the source is an array
      // .pipe(distinctUntilChanged())
      .subscribe((res: League[]) => {
        this.leaguesItems = res;
        this.addImgLink();
      });
    // (await this.showSpinner()).dismiss()
  }

Comment out this.showSpinner dismiss and present and see if the test passes. If it does, you know those 2 lines could be the issue.

This is a good resource in learning unit testing with Angular: https://testing-angular.com/.

Upvotes: 2

Related Questions