0

Just started writing test cases for Angular's Tour of Heroes, however when I run them I get a ERROR in src/app/hero.service.spec.ts:36:5 - error TS2532: Object is possibly 'undefined. I am very new to angular and I am struggling to find out what the problem is. I have looked at several implementations I found online but my they're identical to my code. Any help is appreciated.

Terminal output

ng test --code-coverage
    10% building 2/2 modules 0 active11 09 2020 16:07:32.845:WARN [karma]: No captured browser, open http://localhost:9876/
    11 09 2020 16:07:32.850:INFO [karma-server]: Karma v5.0.9 server started at http://0.0.0.0:9876/
    11 09 2020 16:07:32.852:INFO [launcher]: Launching browsers Chrome with concurrency unlimited
    11 09 2020 16:07:32.899:INFO [launcher]: Starting browser Chrome
    
    ERROR in src/app/hero.service.spec.ts:36:5 - error TS2532: Object is possibly 'undefined'.
    
    36     this.mockHeroes = [...mockData];
           ~~~~
    src/app/hero.service.spec.ts:37:5 - error TS2532: Object is possibly 'undefined'.
    
    37     this.mockHero = this.mockHeroes[0];
           ~~~~
    src/app/hero.service.spec.ts:37:21 - error TS2532: Object is possibly 'undefined'.
    
    37     this.mockHero = this.mockHeroes[0];
                           ~~~~
    src/app/hero.service.spec.ts:38:5 - error TS2532: Object is possibly 'undefined'.
    
    38     this.mockId = this.mockHero.id;
           ~~~~
    src/app/hero.service.spec.ts:38:19 - error TS2532: Object is possibly 'undefined'.
    
    38     this.mockId = this.mockHero.id;
         

hero.service.spec.ts

import { TestBed, inject } from '@angular/core/testing';
import { HttpClientModule, HttpClient, HttpResponse } from '@angular/common/http';
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { MessageService } from './message.service';

import { HeroService } from './hero.service';
import { Hero } from './hero';

//mock data to be used when testing
const mockData = [
  { id: 1, name: 'Hulk' },
  { id: 2, name: 'Thor'},
  { id: 3, name: 'Iron Man'}
] as Hero[];

describe('HeroService', () => {
  let heroService;
  let messageService: MessageService;
  let httpClient: HttpClient;

  let service;
  let httpTestingController: HttpTestingController;


  //before each test, configure a new test med as if it was the hero service
  beforeEach(() => {

    TestBed.configureTestingModule({
      imports: [
        HttpClientTestingModule
      ],
      providers: [HeroService, MessageService]
    });
    httpTestingController = TestBed.get(HttpTestingController);

    this.mockHeroes = [...mockData];
    this.mockHero = this.mockHeroes[0];
    this.mockId = this.mockHero.id;
    heroService = TestBed.get(HeroService);
  });

  //no pending http requests
  afterEach(() => {
    httpTestingController.verify();
  });


  //test, service should be able to be started
  it('should exist', () => {
    expect(heroService).toBeTruthy();
  })


});

hero.service.ts (in case the problem may lie here)

import { MessageService } from './message.service';
import { HEROES } from './mock-heroes';
import { Injectable } from '@angular/core';
import { Hero } from './hero';
import { Observable, of } from 'rxjs';

import { HttpClient, HttpHeaders } from '@angular/common/http';
import { catchError, map, tap } from 'rxjs/operators';

@Injectable({
  providedIn: 'root'
})
export class HeroService {

  myHero: string = 'hero';

  private heroesUrl = 'api/heroes';  // URL to web api
  httpOptions = {
    headers: new HttpHeaders({ 'Content-Type': 'application/json' })
  };

  constructor(
    private http: HttpClient,
    private MessageService: MessageService) { }

  getHeroes(): Observable<Hero[]> {
    this.MessageService.add(`HeroService: Fetched Heroes`);
    return this.http.get<Hero[]>(this.heroesUrl)
      .pipe(
        tap(_ => this.log('fetched heroes')),
        catchError(this.handleError<Hero[]>('getHeroes', []))
      )
  }

  getString() {
    return this.myHero;
  }

  getHero(id: number): Observable<Hero> {
    const url = `${this.heroesUrl}/${id}`;
    return this.http.get<Hero>(url).pipe(
      tap(_ => this.log(`fetched hero id=${id}`)),
      catchError(this.handleError<Hero>(`getHero id=${id}`))
    );
  }

  /** Log a HeroService message with the MessageService */
  private log(message: string) {
    this.MessageService.add(`HeroService: ${message}`);
  }

  /**
  * Handle Http operation that failed.
  * Let the app continue.
  * @param operation - name of the operation that failed
  * @param result - optional value to return as the observable result
  */
  private handleError<T>(operation = 'operation', result?: T) {
    return (error: any): Observable<T> => {

      // TODO: send the error to remote logging infrastructure
      console.error(error); // log to console instead

      // TODO: better job of transforming error for user consumption
      this.log(`${operation} failed: ${error.message}`);

      // Let the app keep running by returning an empty result.
      return of(result as T);
    };
  }

  updateHero(hero: Hero): Observable<any> {
    return this.http.put(this.heroesUrl, hero, this.httpOptions).pipe(
      tap(_ => this.log(`updated hero id=${hero.id}`)),
      catchError(this.handleError<any>("updateHero"))
    );
  }

  addHero(hero: Hero): Observable<Hero> {
    return this.http.post<Hero>(this.heroesUrl, hero, this.httpOptions).pipe(
      tap((newHero: Hero) => this.log(`added hero w /id=${newHero.id}`)),
      catchError(this.handleError<Hero>('addHero'))
    );
  }

  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.log(`deleted hero id=${id}`)),
      catchError(this.handleError<Hero>('deleteHero'))
    );
  }

  searchHeroes(term: string): Observable<Hero[]>{
    if(!term.trim()) {
      return of([]);
    }
    return this.http.get<Hero[]>(`${this.heroesUrl}/?name=${term}`).pipe(
      tap(x => x.length ?
        this.log(`found heroes matching "${term}"`) :
        this.log(`no heroes matching "${term}"`)),
      catchError(this.handleError<Hero[]>('searchHeroes', []))
    )
  };
}
Apollocy
  • 67
  • 2
  • 8
  • What do you expect `this` to be in the test? – jonrsharpe Sep 11 '20 at 15:21
  • Arrow functions like the ones you used in `describe` and `beforeEach` don't have their own scope so the `this` keyword refers to the top level scope of your test file. In a web browser context, `this` refers to the top level `Window` object. My guess is that you need to tell tsconfig that this project is a web project by specifying `lib: ["dom"]` in your tsconfig file? – teddybeard Sep 11 '20 at 16:47
  • @jonrsharpe I'm not really sure. I found someone's github's testing files and I was trying to replicate the test results. The test files seem to work fine for some people, but for some reason, they dont work for me. – Apollocy Sep 14 '20 at 07:27
  • @teddybeard seems like the lib: ["dom"] was already generated by default, so that unfortunately does not solve the problem – Apollocy Sep 14 '20 at 07:29
  • Maybe you should base it more on the official docs, then: https://angular.io/guide/testing – jonrsharpe Sep 14 '20 at 07:31

1 Answers1

0

Removing the following lines should fix the error without changing the behavior of the tests

this.mockHeroes = [...mockData];
this.mockHero = this.mockHeroes[0];
this.mockId = this.mockHero.id;
teddybeard
  • 1,964
  • 1
  • 12
  • 14