Search code examples
angularangular-test

Angular tour-of-heroes: no provider for HttpClient when testing a component


I've seen many threads about this error but in my case, I don't have any error with the hero.service.spec.ts (at least, nothing shows up) but I have the No provider for HttpClient when I test a component which depends on the Hero service (the Hero service depends on HttpClient, the component itself, no).

I get the same error with all the components which depend on Hero service.

screenshot from Jasmine

This is my heroes.component.spec.ts

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

import { HeroesComponent } from './heroes.component';

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

  beforeEach(async () => {
    await TestBed.configureTestingModule({
      declarations: [ HeroesComponent ]
    })
    .compileComponents();
  });

  beforeEach(() => {
    fixture = TestBed.createComponent(HeroesComponent);
    component = fixture.componentInstance;
    fixture.detectChanges();
  });

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

And my hero.service.spec.ts

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

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

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

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

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

I'm new to Angular tests and this is quite annoying when you just try to run the ng test from the official tutorial and it doesn't work :-)

Any help appreciated !


Solution

  • You need to import the HttpClientTestingModule in your component test's Angular testing module like so:

    import { ComponentFixture, TestBed } from '@angular/core/testing';
    
    import { HeroesComponent } from './heroes.component';
    
    describe('HeroesComponent', () => {
      let component: HeroesComponent;
      let fixture: ComponentFixture<HeroesComponent>;
    
      beforeEach(async () => {
        await TestBed.configureTestingModule({
          declarations: [ HeroesComponent ],
          imports: [ HttpClientTestingModule ],  // 👈 Add this line
        })
        .compileComponents();
      });
    
      beforeEach(() => {
        fixture = TestBed.createComponent(HeroesComponent);
        component = fixture.componentInstance;
        fixture.detectChanges();
      });
    
      it('should create', () => {
        expect(component).toBeTruthy();
      });
    });
    

    The HeroesComponent depends on the HeroService which depends on the HttpClient. Angular must be able to resolve all dependencies during runtime and tests.

    During tests, we can break this dependency hierarchy at any point by providing a dependency with a test double.

    For example, we could replace the HeroService with a fake object like so:

    const fakeHeroService = {
      getHeroes(): Observable<Hero[]> {
        return of([]);
      }
    };
    
    TestBed.configureTestingModule({
      providers: [
        {
          provide: HeroesService,
          useValue: fakeHeroService,
        },
      ],
    });
    

    or we could replace the HttpClient with a test double like so (we rarely want to do this):

    const fakeHttpClient = {
      get(url, options): Observable<Hero[]> {
        return of([]);
      }
    };
    
    TestBed.configureTestingModule({
      providers: [
        {
          provide: HttpClient,
          useValue: fakeHttpClient,
        },
      ],
    });
    

    The HttpClient has a lot of dependencies. HttpClientTestingModule provides fake versions of some of those dependencies and actual versions of other dependencies.

    This Angular module also provides HttpTestingController which allows us to fake HTTP responses and set up expectations about the HTTP requests that were made during a tests.

    It depends on what type of test you're writing and what you're trying to achieve. If you're doing an isolated unit test suite for HeroesComponent, you should replace all its direct dependencies with test doubles.

    Here's an example of a stub that can replace the hero service.

    export const femaleMarvelHeroes: Hero[] = [
      { id: 1, name: 'Black Widow' },
      { id: 2, name: 'Captain Marvel' },
      { id: 3, name: 'Medusa' },
      { id: 4, name: 'Ms. Marvel' },
      { id: 5, name: 'Scarlet Witch' },
      { id: 6, name: 'She-Hulk' },
      { id: 7, name: 'Storm' },
      { id: 8, name: 'Wasp' },
      { id: 9, name: 'Rogue' },
      { id: 10, name: 'Elektra' },
      { id: 11, name: 'Gamora' },
      { id: 12, name: 'Hawkeye (Kate Bishop)' },
    ];
    
    const heroServiceStub = jasmine.createSpyObj<HeroService>(
      HeroService.name,
      [
        'addHero',
        'deleteHero',
        'getHeroes',
      ]);
    heroServiceStub.addHero
      .and.callFake(({ name }: Partial<Hero>) => observableOf({
        id: 42,
        name,
      }, asapScheduler))
      .calls.reset();
    heroServiceStub .deleteHero
      .and.callFake((hero: Hero) => of(hero, asapScheduler))
      .calls.reset();
    heroServiceStub .getHeroes
      .and.returnValue(of(femaleMarvelHeroes, asapScheduler))
      .calls.reset();
    

    This snippet was extracted from my GitHub repository LayZeeDK/ngx-tour-of-heroes-mvp.

    Learn more techniques for verifying collaborator integration in Testing Angular container components.