Mock custom service in angular2 during unit test

19,293

Solution 1

It's because of this

@Component({
  providers: [PonyService]  <======
})

This makes it so that the service is scoped to the component, which means that Angular will create it for each component, and also means that it supercedes any global providers configured at the module level. This includes the mock provider that you configure in the test bed.

To get around this, Angular provides the TestBed.overrideComponent method, which allows us to override things like the @Component.providers and @Component.template.

TestBed.configureTestingModule({
  declarations: [PoniesComponent, PonyComponent]
})
.overrideComponent(PoniesComponent, {
  set: {
    providers: [
      {provide: PonyService, useClass: MockPonyService}
    ]
  }
});

Solution 2

Another valid approach is to use tokens and rely on Intefaces instead of base classes or concrete classes, which dinosaurs like me love to do (DIP, DI, and other SOLID Blablahs). And allow your component to have its dependencies injected instead of providing it yourself in your own component.

Your component would not have any provider, it would receive the object as an interface in its constructor during angular's magic dependency injection. See @inject used in the constructor, and see the 'provide' value in providers as a text rather than a class.

So, your component would change to something like:

constructor(@Inject('PonyServiceInterface') private ponyService: IPonyService) {
   this.ponies = this.ponyService.getPonies(2); }

In your @Component part, you would remove the provider and add it to a parent component such as "app.component.ts". There you would add a token:

providers: [{provide: 'PonyServiceInterface', useClass: PonyService}]

Your unit test component (the analog to app.component.ts) would have: providers: [{provide: 'PonyServiceInterface', useClass: MockPonyService}]

So your component doesn't care what the service does, it just uses the interface, injected via the parent component (app.component.ts or your unit test component).

FYI: The @inject approach is not very widely used, and at some point it looks like angular fellows prefer baseclasses to interfaces due to how the underlying javascript works.

Share:
19,293
Evgeniy
Author by

Evgeniy

Updated on June 04, 2022

Comments

  • Evgeniy
    Evgeniy about 2 years

    I'm trying to write a unit test for component used in my service. Component and service work fine.

    Component:

    import {Component} from '@angular/core';
    import {PonyService} from '../../services';
    import {Pony} from "../../models/pony.model";
    @Component({
      selector: 'el-ponies',
      templateUrl: 'ponies.component.html',
      providers: [PonyService]
    })
    export class PoniesComponent {
      ponies: Array<Pony>;
      constructor(private ponyService: PonyService) {
        this.ponies = this.ponyService.getPonies(2);
      }
      refreshPonies() {
        this.ponies = this.ponyService.getPonies(3);
      }
    }
    

    Service:

    import {Injectable} from "@angular/core";
    import {Http} from "@angular/http";
    import {Pony} from "../../models/pony.model";
    @Injectable()
    export class PonyService {
      constructor(private http: Http) {}
      getPonies(count: number): Array<Pony> {
        let toReturn: Array<Pony> = [];
        this.http.get('http://localhost:8080/js-backend/ponies')
        .subscribe(response => {
          response.json().forEach((tmp: Pony)=> { toReturn.push(tmp); });
          if (count && count % 2 === 0) { toReturn.splice(0, count); } 
          else { toReturn.splice(count); }
        });
        return toReturn;
      }}
    

    Component unit test:

    import {TestBed} from "@angular/core/testing";
    import {PoniesComponent} from "./ponies.component";
    import {PonyComponent} from "../pony/pony.component";
    import {PonyService} from "../../services";
    import {Pony} from "../../models/pony.model";
    describe('Ponies component test', () => {
      let poniesComponent: PoniesComponent;
      beforeEach(() => {
        TestBed.configureTestingModule({
          declarations: [PoniesComponent, PonyComponent],
          providers: [{provide: PonyService, useClass: MockPonyService}]
        });
        poniesComponent = TestBed.createComponent(PoniesComponent).componentInstance;
      });
      it('should instantiate component', () => {
        expect(poniesComponent instanceof PoniesComponent).toBe(true, 'should create PoniesComponent');
      });
    });
    
    class MockPonyService {
      getPonies(count: number): Array<Pony> {
        let toReturn: Array<Pony> = [];
        if (count === 2) {
          toReturn.push(new Pony('Rainbow Dash', 'green'));
          toReturn.push(new Pony('Pinkie Pie', 'orange'));
        }
        if (count === 3) {
          toReturn.push(new Pony('Fluttershy', 'blue'));
          toReturn.push(new Pony('Rarity', 'purple'));
          toReturn.push(new Pony('Applejack', 'yellow'));
        }
        return toReturn;
      };
    }
    

    Part of package.json:

    {
      ...
      "dependencies": {
        "@angular/core": "2.0.0",
        "@angular/http": "2.0.0",
        ...
      },
      "devDependencies": {
        "jasmine-core": "2.4.1",
        "karma": "1.2.0",
        "karma-jasmine": "1.0.2",
        "karma-phantomjs-launcher": "1.0.2",
        "phantomjs-prebuilt": "2.1.7",
        ...
      }
    }
    

    When I execute 'karma start' I get this error

    Error: Error in ./PoniesComponent class PoniesComponent_Host - inline template:0:0 caused by: No provider for Http! in config/karma-test-shim.js

    It looks like karma uses PonyService instead of mocking it as MockPonyService, in spite of this line: providers: [{provide: PonyService, useClass: MockPonyService}].

    The question: How I should mock the service?