lundi 5 octobre 2020

Testing NG Services Observables values

Need help to understand what's happening here :

A basic tour of heroes app will do it to explain,

I wanna setup some tests with Jest to be able to see if the behaviours of a service doesn't change with time.

here is how the testing file look like :

import { TestBed } from '@angular/core/testing';

import { HeroService } from './hero.service';
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { ServicesModule } from '@services/services.module';

describe('HeroService', () => {
  let httpMock: HttpTestingController;
  let service: HeroService;

  beforeEach(() => {
    TestBed.configureTestingModule({
      imports: [
        ServicesModule,
        HttpClientTestingModule
      ],
    });
    httpMock = TestBed.inject(HttpTestingController);
    service = TestBed.inject(HeroService);
  });

  afterEach(() => {
    httpMock.verify();
  });

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

  // Do not pass, timeout error ( spec #1 )
  it('getHeroes: should return a sorted list',  done => {
    service.getHeroes().subscribe(heroes => {
      expect(heroes.length).toBe(10);
      done();
    } );

    // Simulates the asynchronous passage of time

    const req = httpMock.expectOne(`api/heroes`);
    expect(req.request.method).toBe('GET');

  });

  // Do pass but don't check the value ( spec #2 )
  it('getHeroes: should return a sorted list', () => {
    service.getHeroes().subscribe(heroes => {
      expect(heroes.length).toBe(10);
    } );

    const req = httpMock.expectOne(`api/heroes`);
    expect(req.request.method).toBe('GET');

  });

});

Spec 1 error message :

: Timeout - Async callback was not invoked within the 5000 ms timeout specified by jest.setTimeout.
Timeout - Async callback was not invoked within the 5000 ms timeout specified by jest.setTimeout.Error

Spec 2 :

The test show as green & passed but the expect(heroes.length).toBe(10); is not properly checked.

I have a basic InMemoryDbService with it's DB setted up like that ( so the lenght should be 4 )and fail the previous test :

function getDbData() : Db{
  const heroes: any[] = [
    {
      id: 11,
      name: 'Maxwell Smart',
      saying: 'Missed it by that much.'
    },
    {
      id: 12,
      name: 'Bullwinkle J. Moose',
      saying: 'Watch me pull a rabbit out of a hat.'
    },
    {
      id: 13,
      name: 'Muhammad Ali',
      saying: 'Float like a butterfly, sting like a bee.'
    },
    {
      id: 14,
      name: 'Eleanor Roosevelt',
      saying: 'No one can make you feel inferior without your consent.'
    }
  ];

  return {heroes} as Db;
}

Imported like that in the main app module :

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    // Core App Module
    CoreModule,
    // Routing Module
    AppRoutingModule,
    // Angular Specifics Module
    BrowserModule,
    HttpClientModule,
    // Development purpose Only
    HttpClientInMemoryWebApiModule.forRoot(
      InMemoryDataService, {
        dataEncapsulation: false,
        passThruUnknownUrl: true
      }
    ),
  ],
  providers: [ServicesModule],
  bootstrap: [AppComponent]
})

For good mesure : Hero.service.ts :

import { Injectable } from '@angular/core';
import { Hero } from '@models/hero.model';
import { HEROES } from '@services/in-memory-data/mock-heroes.service';


import { Observable, of } from 'rxjs';
import { catchError, map, tap } from 'rxjs/operators';
import { MessageService } from '@services/messages/message.service';
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { ErrorHandlerService } from '@services/error-handler/error-handler.service';

// Marks the class as one that participates in the dependency injection system
// This method don't need a link in service.module
/*//
@Injectable({
  providedIn: 'root'
})
//*/

// This method need a link in service.module
@Injectable()
export class HeroService {

  private heroesUrl = 'api/heroes';  // endpoint of the api service

  // TODO : HTTPInterceptor
  httpOptions = {
    headers: new HttpHeaders({'Content-Type': 'application/json'})
  };

  constructor(
    private http: HttpClient,
    private messageService: MessageService,
    private errorHandlerService: ErrorHandlerService) {
  }

  /** GET heroes from the server */
  getHeroes(): Observable<Hero[]> {
    return this.http.get<Hero[]>(this.heroesUrl)
      .pipe(
        tap(_ => this.messageService.add('fetched heroes')),
        catchError(this.errorHandlerService.handleError<Hero[]>('getHeroes', []))
      );
  }

  /** GET hero by id. Will 404 if id not found */
  getHero(id: number): Observable<Hero> {
    const url = `${this.heroesUrl}/${id}`;
    return this.http.get<Hero>(url).pipe(
      tap(_ => this.messageService.add(`fetched hero id=${id}`)),
      catchError(this.errorHandlerService.handleError<Hero>(`getHero id=${id}`))
    );
  }

  /** PUT: update the hero on the server */
  updateHero(hero: Hero): Observable<any> {
    return this.http.put(this.heroesUrl, hero, this.httpOptions).pipe(
      tap(_ => this.messageService.add(`updated hero id=${hero.id}`)),
      catchError(this.errorHandlerService.handleError<any>('updateHero'))
    );
  }

  /** POST: add a new hero to the server */
  addHero(hero: Hero): Observable<Hero> {
    return this.http.post<Hero>(this.heroesUrl, hero, this.httpOptions).pipe(
      tap((newHero: Hero) => this.messageService.add(`added hero w/ id=${newHero.id}`)),
      catchError(this.errorHandlerService.handleError<Hero>('addHero'))
    );
  }

  /** DELETE: delete the hero from the server */
  deleteHero(hero: Hero | number): Observable<Hero> {
    const id = typeof hero === 'number' ? hero : hero.id;
    const url = `${this.heroesUrl}/${id}`;

    return this.http.delete<Hero>(url, this.httpOptions).pipe(
      tap(_ => this.messageService.add(`deleted hero id=${id}`)),
      catchError(this.errorHandlerService.handleError<Hero>('deleteHero'))
    );
  }

  /* GET heroes whose name contains search term */
  searchHeroes(term: string): Observable<Hero[]> {
    if (!term.trim()) {
      // if not search term, return empty hero array.
      return of([]);
    }
    return this.http.get<Hero[]>(`${this.heroesUrl}/?name=${term}`).pipe(
      tap(x => x.length ?
        this.messageService.add(`found heroes matching "${term}"`) :
        this.messageService.add(`no heroes matching "${term}"`)),
      catchError(this.errorHandlerService.handleError<Hero[]>('searchHeroes', []))
    );
  }
}

Can anyone help me figuring out what's wrong and how I can find the solution ?

Thanks.

Aucun commentaire:

Enregistrer un commentaire