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)
// Service2 is provided / imported in TargetModule
.mock(Service2, {
trigger: () => 'mock2',
})
// Service3 will be provided in TestBed
.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/help-me-mom/ng-mocks/blob/master/examples/TestProvider/test.spec.ts
import { Injectable } from '@angular/core';

import { MockBuilder, MockRender } 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('TestProviderCommon', () => {
// Do not forget to return the promise of MockBuilder.
beforeEach(() => MockBuilder(TargetService));

it('returns value on echo', () => {
const service = MockRender(TargetService).point.componentInstance;

expect(service.echo()).toEqual(service.value);
});
});

Live example with dependencies

https://github.com/help-me-mom/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';
},
});
});

// Resets customizations from MockInstance.
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/help-me-mom/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';
},
});
});

// Resets customizations from MockInstance.
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/help-me-mom/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/help-me-mom/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';
},
});
});

// Resets customizations from MockInstance.
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/help-me-mom/ng-mocks/blob/master/examples/TestProviderWithUseFactory/test.spec.ts
import { Injectable, NgModule } from '@angular/core';

import {
MockBuilder,
MockInstance,
MockRender,
MockReset,
} 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';
},
});
});

// Resets customizations from MockInstance.
afterAll(MockReset);

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');
});
});