Seeking assistance in understanding the current situation:
I believe a simple tour of heroes app would be helpful in clarifying,
I am looking to set up some tests using Jest to verify if the behavior of a service remains consistent over time.
This is how the testing file is structured:
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();
});
// Fails due to 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');
});
// Passes but does not properly 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');
});
});
Error message for Spec 1 :
: 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 displays as green and passed, but the expect(heroes.length).toBe(10); is not thoroughly verified.
I have an InMemoryDbService with its DB configured like this (which should have a length of 4) causing the previous test to fail:
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 so 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]
})
Additionally, Hero.service.ts for reference:
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', []))
);
}
}
If anyone has insights on what might be going wrong and how I could approach finding a solution, that would be greatly appreciated.
Thank you.