Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Angular Testing - Mocking An InjectionToken

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?

like image 561
sadq3377 Avatar asked Oct 02 '18 11:10

sadq3377


People also ask

What is mocking in unit testing Angular?

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 ...

What is InjectionToken in Angular?

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 .

Why would you use a spy in a test in Angular?

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.

What is TestBed inject?

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.


1 Answers

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();
    });
});
like image 86
sadq3377 Avatar answered Oct 16 '22 22:10

sadq3377