I'm trying to test an Angular service that handles SignalR connections, which takes the code for SignalR as an InjectionToken.
Here's the provider file:
// signalr-provider.ts
import { InjectionToken } from '@angular/core';
export const SIGNALR_TOKEN = new InjectionToken('signalR');
export function signalRFactory() {
return window['signalR'];
}
export const SIGNALR_PROVIDER = [ { provide: SIGNALR_TOKEN, useFactory: signalRFactory } ];
And here's the service:
// signalr-service.ts
import { Injectable, Inject } from '@angular/core';
import { SIGNALR_TOKEN } from './signalr-provider';
import { HubConnection } from '@aspnet/signalr';
import { environment } from '../../../environments/environment';
@Injectable()
export class SignalrService {
private hubConnection: HubConnection;
private baseUrl: string = environment.baseUrl;
constructor(@Inject(SIGNALR_TOKEN) private signalR: any) {
this.init();
}
init(): void {
this.hubConnection = new this.signalR.HubConnectionBuilder().withUrl(`${this.baseUrl}/hubs/test`).build();
}
}
The token is provided in the app module like this:
// From app.module.ts
@NgModule({
declarations: [...],
imports: [...],
providers: [ SIGNALR_PROVIDER, SignalrService ],
bootstrap: [ AppComponent]
})
export class AppModule {}
So far, I'm having no luck trying to mock the InjectionToken in my test file, and end up with a NullInjectorError: No provider for InjectionToken signalR!
message every time.
Here's my first attempt, returning a mocked object from the signalRFactory function with only the properties needed in the initial connection:
// signalr-service.spec.ts
import { TestBed, async } from '@angular/core/testing';
import { InjectionToken } from '@angular/core';
import { SignalrService } from './signalr-service';
const SIGNALR_TOKEN = new InjectionToken('signalR');
function signalRFactory() {
return {
HubConnectionBuilder: () => {
return {
withUrl: (url) => {
return {
build: () => {}
};
}
};
}
};
}
const SIGNALR_PROVIDER = [ { provide: SIGNALR_TOKEN, useFactory: signalRFactory } ];
describe('ConnectionService', () => {
beforeEach(
async(() => {
TestBed.configureTestingModule({
providers: [ SIGNALR_PROVIDER, SignalrService ]
});
})
);
it('should exist', () => {
const connectionService = TestBed.get(SignalrService);
expect(connectionService).toBeTruthy();
});
});
In my second attempt, I assigned the mocked object to a variable:
import { TestBed, async } from '@angular/core/testing';
import { InjectionToken } from '@angular/core';
import { SignalrService } from './signalr-service';
const SIGNALR_TOKEN = new InjectionToken('signalR');
const SIGNALR_VALUE = {
HubConnectionBuilder: () => {
return {
withUrl: (url) => {
return {
build: () => {}
};
}
};
}
};
const SIGNALR_PROVIDER = [ { provide: SIGNALR_TOKEN, useValue: SIGNALR_VALUE } ];
describe('ConnectionService', () => {
beforeEach(
async(() => {
TestBed.configureTestingModule({
providers: [ SIGNALR_PROVIDER, SignalrService ]
});
})
);
it('should exist', () => {
const connectionService = TestBed.get(SignalrService);
expect(connectionService).toBeTruthy();
});
});
In my third attempt, I took out the SIGNALR_PROVIDER and tried to provide the value directly in the providers array by factory and by direct value:
Direct value:
import { TestBed, async } from '@angular/core/testing';
import { InjectionToken } from '@angular/core';
import { SignalrService } from './signalr-service';
const SIGNALR_TOKEN = new InjectionToken('signalR');
const SIGNALR_VALUE = {
HubConnectionBuilder: () => {
return {
withUrl: (url) => {
return {
build: () => {}
};
}
};
}
};
describe('ConnectionService', () => {
beforeEach(
async(() => {
TestBed.configureTestingModule({
providers: [ { provide: SIGNALR_TOKEN, useValue: SIGNALR_VALUE }, SignalrService ]
});
})
);
it('should exist', () => {
const connectionService = TestBed.get(SignalrService);
expect(connectionService).toBeTruthy();
});
});
Factory:
import { TestBed, async } from '@angular/core/testing';
import { InjectionToken } from '@angular/core';
import { SignalrService } from './signalr-service';
const SIGNALR_TOKEN = new InjectionToken('signalR');
function signalRFactory() {
return {
HubConnectionBuilder: () => {
return {
withUrl: (url) => {
return {
build: () => {}
};
}
};
}
};
}
describe('ConnectionService', () => {
beforeEach(
async(() => {
TestBed.configureTestingModule({
providers: [ { provide: SIGNALR_TOKEN, useFactory: signalRFactory }, SignalrService ]
});
})
);
it('should exist', () => {
const connectionService = TestBed.get(SignalrService);
expect(connectionService).toBeTruthy();
});
});
Each attempt still gives me NullInjectorError: No provider for InjectionToken signalR!
I'm stuck. There's obviously something I don't know about the InjectionToken. Could anyone point me in the right direction?
Introduction. Mocking is a great idea for testing Angular apps because it makes maintenance easier and helps reduce future bugs. There are a few complex tools, such as XUnit, for mocking an Angular CLI project. You can execute the mocking methods described in this guide only if you use vanilla Jasmine + Angular Testbed ...
Use an InjectionToken whenever the type you are injecting is not reified (does not have a runtime representation) such as when injecting an interface, callable type, array or parameterized type. InjectionToken is parameterized on T which is the type of object which will be returned by the Injector .
Spy is a feature in Jasmine that allows you to spy on something to achieve the following goals: Monitor if a function is called along with the parameters pass to it. Override function return values or properties to simulate desired situations during tests. Override the implementation of functions completely.
TestBed. configureTestingModule() helps you configure the providers. Configuring the providers means you are letting the Angular dependency injection system know about this dependency which later it can inject in to components when requested through a dependency injection token.
Figured it out--my problem was that I redefined the token in the test file, so my test was providing my mocked object for a completely different token than the one my service was expecting. Changing it to this fixed it:
// signalr-service.spec.ts
import { TestBed, async } from '@angular/core/testing';
import { InjectionToken } from '@angular/core';
import { SignalrService } from './signalr-service';
import { SIGNALR_TOKEN } from './signalr-provider'; // This is the change
function signalRFactory() {
return {
HubConnectionBuilder: () => {
return {
withUrl: (url) => {
return {
build: () => {}
};
}
};
}
};
}
const SIGNALR_PROVIDER = [ { provide: SIGNALR_TOKEN, useFactory: signalRFactory } ];
describe('ConnectionService', () => {
beforeEach(
async(() => {
TestBed.configureTestingModule({
providers: [ SIGNALR_PROVIDER, SignalrService ]
});
})
);
it('should exist', () => {
const connectionService = TestBed.get(SignalrService);
expect(connectionService).toBeTruthy();
});
});
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With