Skip to main content

How to test a provider in Angular application

Usually, you do not need TestBed if you want to test a simple provider, the best way would be to write isolated pure unit tests.

Nevertheless, MockBuilder might help here too. If a provider has complex dependencies, or you want to verify that its module creates the provider in a particular way, then simply pass the provider as the first parameter and its module as the second one.

beforeEach(() => MockBuilder(TargetService, TargetModule));

In a test you need to use the global injector for getting an instance of the service to assert its behavior:

const service = TestBed.get(TargetService);expect(service.echo()).toEqual(service.value);

What might be useful here is knowledge of how to customize the dependencies. There are 3 options: .mock, .provide and MockInstance. All of them are a good choice:

beforeEach(() =>  MockBuilder(TargetService, TargetModule)    .mock(Service2, {      trigger: () => 'mock2',    })    .provide({      provide: Service3,      useValue: {        trigger: () => 'mock3',      },    }));
beforeAll(() => {  MockInstance(Service1, {    init: instance => {      instance.trigger = () => 'mock1';    },  });});

Despite the way providers are created: useClass, useValue etc. Their tests are quite similar.

Live example for common case#

https://github.com/ike18t/ng-mocks/blob/master/examples/TestProvider/test.spec.ts
import { Injectable } from '@angular/core';import { TestBed } from '@angular/core/testing';import { MockBuilder } from 'ng-mocks';
// A simple service, might have contained more logic,// but it is redundant for the test demonstration.@Injectable()class TargetService {  public readonly value = true;
  public echo(): boolean {    return this.value;  }}
describe('TestProvider', () => {  // Do not forget to return the promise of MockBuilder.  beforeEach(() => MockBuilder(TargetService));
  it('returns value on echo', () => {    const service = TestBed.get(TargetService);
    expect(service.echo()).toEqual(service.value);  });});

Live example with dependencies#

https://github.com/ike18t/ng-mocks/blob/master/examples/TestProviderWithDependencies/test.spec.ts
import { Injectable, NgModule } from '@angular/core';import {  MockBuilder,  MockInstance,  MockRender,  MockReset,} from 'ng-mocks';
// Dependency 1@Injectable()class Service1 {  protected name = 'service1';
  public trigger(): string {    return this.name;  }}// Dependency 2@Injectable()class Service2 {  protected name = 'service2';
  public trigger(): string {    return this.name;  }}
// A simple service, it might have contained more logic,// but it is redundant for the test demonstration.@Injectable()class TargetService {  public readonly value1: string;  public readonly value2: string;
  public constructor(dep1: Service1, dep2: Service2) {    this.value1 = dep1.trigger();    this.value2 = dep2.trigger();  }}
// A module that provides all services.@NgModule({  providers: [Service1, Service2, TargetService],})class TargetModule {}
describe('TestProviderWithDependencies', () => {  // Because we want to test the service, we pass it as the first  // parameter of MockBuilder. To correctly satisfy its dependencies  // we need to pass its module as the second parameter.  // Do not forget to return the promise of MockBuilder.  beforeEach(() => MockBuilder(TargetService, TargetModule));
  beforeAll(() => {    // Let's customize a bit behavior of the mock copy of Service1.    MockInstance(Service1, {      init: instance => {        instance.trigger = () => 'mock1';      },    });    // Let's customize a bit behavior of the mock copy of Service2.    MockInstance(Service2, {      init: instance => {        instance.trigger = () => 'mock2';      },    });  });
  afterAll(MockReset);
  it('creates TargetService', () => {    // Creates an instance only if all dependencies are present.    const service = MockRender(TargetService).point.componentInstance;
    // Let's assert behavior.    expect(service.value1).toEqual('mock1');    expect(service.value2).toEqual('mock2');  });});

Live example for useClass#

https://github.com/ike18t/ng-mocks/blob/master/examples/TestProviderWithUseClass/test.spec.ts
import { Injectable, NgModule } from '@angular/core';import {  MockBuilder,  MockInstance,  MockRender,  MockReset,} from 'ng-mocks';
// A service we want to replace via useClass.@Injectable()class Service1 {  public constructor(public name: string) {}}
// A service replacing the Service1.@Injectable()class Service2 extends Service1 {  public readonly flag = true;}
// A service we want to test and to replace via useClass.@Injectable()class Target1Service {  public constructor(public readonly service: Service1) {}}
// A service replacing the Target1Service.@Injectable()class Target2Service extends Target1Service {  public readonly flag = true;}
// A module that provides all services.@NgModule({  providers: [    {      provide: 'real',      useValue: 'real',    },    {      deps: ['real'],      provide: Service1,      useClass: Service2,    },    {      deps: [Service1],      provide: Target1Service,      useClass: Target2Service,    },  ],})class TargetModule {}
describe('TestProviderWithUseClass', () => {  // Because we want to test the service, we pass it as the first  // parameter of MockBuilder. To correctly satisfy its dependencies  // we need to pass its module as the second parameter.  // Do not forget to return the promise of MockBuilder.  beforeEach(() => MockBuilder(Target1Service, TargetModule));
  beforeAll(() => {    // Let's customize a bit behavior of the mock copy of Service1.    MockInstance(Service1, {      init: instance => {        instance.name = 'mock1';      },    });  });
  afterAll(MockReset);
  it('respects all dependencies', () => {    const service = MockRender<      Target1Service & Partial<Target2Service>    >(Target1Service).point.componentInstance;
    // Let's assert that service has a flag from Target2Service.    expect(service.flag).toBeTruthy();    expect(service).toEqual(jasmine.any(Target2Service));
    // And let's assert that Service1 has been replaced with its mock copy    // and its name is undefined.    expect(service.service.name).toEqual('mock1');    expect(service.service).toEqual(jasmine.any(Service1));
    // Because we use a mock module, Service1 has been replaced with    // a mock copy based on its `provide` class, deps and other    // values are ignored by building mocks logic.    expect(service.service).not.toEqual(jasmine.any(Service2));  });});

Live example for useValue#

https://github.com/ike18t/ng-mocks/blob/master/examples/TestProviderWithUseValue/test.spec.ts
import { Injectable, NgModule } from '@angular/core';import { MockBuilder, MockRender } from 'ng-mocks';
// A simple service, it might have contained more logic,// but it is redundant for the test demonstration.@Injectable()class TargetService {  public readonly name = 'target';}
// A module that provides all services.@NgModule({  providers: [    {      provide: TargetService,      // an empty object instead      useValue: {        service: null,      },    },  ],})class TargetModule {}
describe('TestProviderWithUseValue', () => {  // Because we want to test the service, we pass it as the first  // parameter of MockBuilder. To correctly satisfy its initialization  // we need to pass its module as the second parameter.  // Do not forget to return the promise of MockBuilder.  beforeEach(() => MockBuilder(TargetService, TargetModule));
  it('creates TargetService', () => {    const service = MockRender<TargetService>(TargetService).point      .componentInstance;
    // Let's assert received data.    expect(service as any).toEqual({      service: null,    });  });});

Live example for useExisting#

https://github.com/ike18t/ng-mocks/blob/master/examples/TestProviderWithUseExisting/test.spec.ts
import { Injectable, NgModule } from '@angular/core';import {  MockBuilder,  MockInstance,  MockRender,  MockReset,} from 'ng-mocks';
// A service we want to use.@Injectable()class Service1 {  public name = 'target';}
// A service we want to replace.@Injectable()class Service2 {  public name = 'target';}
// A service we want to test and to replace via useExisting.@Injectable()class TargetService {}
// A module that provides all services.@NgModule({  providers: [    Service1,    {      provide: Service2,      useExisting: Service1,    },    {      provide: TargetService,      useExisting: Service2,    },  ],})class TargetModule {}
describe('TestProviderWithUseExisting', () => {  // Because we want to test the service, we pass it as the first  // parameter of MockBuilder. To correctly satisfy its initialization  // we need to pass its module as the second parameter.  // Do not forget to return the promise of MockBuilder.  beforeEach(() => MockBuilder(TargetService, TargetModule));
  beforeAll(() => {    // Let's customize a bit behavior of the mock copy of Service1.    MockInstance(Service2, {      init: instance => {        instance.name = 'mock2';      },    });  });
  afterAll(MockReset);
  it('creates TargetService', () => {    const service = MockRender<      TargetService & Partial<{ name: string }>    >(TargetService).point.componentInstance;
    // Because Service2 has been replaced with a mock copy,    // we are getting here a mock copy of Service2 instead of Service1.    expect(service).toEqual(jasmine.any(Service2));    // Because we have kept TargetService we are getting here a    // mock copy of Service2 as it says in useExisting.    expect(service.name).toEqual('mock2');  });});

Live example for useFactory#

https://github.com/ike18t/ng-mocks/blob/master/examples/TestProviderWithUseFactory/test.spec.ts
import { Injectable, NgModule } from '@angular/core';import { MockBuilder, MockInstance, MockRender } from 'ng-mocks';
// Dependency 1.@Injectable()class Service1 {  public name = 'target';}
// A service we want to use.@Injectable()class TargetService {  public constructor(public readonly service: Service1) {}}
// A module that provides all services.@NgModule({  providers: [    Service1,    {      deps: [Service1],      provide: TargetService,      useFactory: (service: Service1) => new TargetService(service),    },  ],})class TargetModule {}
describe('TestProviderWithUseFactory', () => {  // Because we want to test the service, we pass it as the first  // parameter of MockBuilder. To correctly satisfy its initialization  // we need to pass its module as the second parameter.  // Do not forget to return the promise of MockBuilder.  beforeEach(() => MockBuilder(TargetService, TargetModule));
  beforeAll(() => {    // Let's customize a bit behavior of the mock copy of Service1.    MockInstance(Service1, {      init: instance => {        instance.name = 'mock1';      },    });  });
  it('creates TargetService', () => {    const service = MockRender(TargetService).point.componentInstance;
    // Because Service1 has been replaced with a mock copy, we should get mock1 here.    expect(service.service.name).toEqual('mock1');  });});