Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Mock custom service in angular2 during unit test

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?

like image 632
Evgeniy Avatar asked Oct 29 '16 12:10

Evgeniy


2 Answers

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}
    ]
  }
});
like image 173
Paul Samsotha Avatar answered Nov 17 '22 09:11

Paul Samsotha


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.

like image 1
Adrián Poplavsky Avatar answered Nov 17 '22 09:11

Adrián Poplavsky