I need to test my service which is using Injector
to inject services instead of constructor()
.
The main reason for which I use this way is the large number of services which extends my common SimpleDataService
. Here are CompanyService
, ProductService
, TagService
etc., and everyone extends SimpleDataService. So I don't want to define more than neccessary parameters to super()
calls.
app.module.ts
import { Injector, NgModule } from '@angular/core';
export let InjectorInstance: Injector;
@NgModule({
// ...
})
export class AppModule {
constructor(private injector: Injector) {
InjectorInstance = this.injector;
}
}
simple-data.service.ts
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { InjectorInstance } from 'src/app/app.module';
import { PaginateInterface } from 'src/app/models/paginate/paginate.interface';
import { environment } from 'src/environments/environment';
export class SimpleDataService<T> {
public url = environment.apiUrl;
private http: HttpClient;
public options: any;
public type: new () => T;
constructor(
private model: T,
) {
this.http = InjectorInstance.get<HttpClient>(HttpClient);
}
getAll(): Observable<PaginateInterface> {
return this.http.get(this.url + this.model.api_endpoint, this.options)
.pipe(map((result: any) => result));
}
}
simple-data.service.spec.ts
import { HttpClient } from '@angular/common/http';
import { TestBed } from '@angular/core/testing';
import { BrowserDynamicTestingModule,
platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing';
import { Tag } from 'src/app/models/tag/tag.model';
import { SimpleDataService } from './simple-data.service';
describe('SimpleDataService', () => {
TestBed.initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting());
const model = new Tag();
const simpleDataService = new SimpleDataService(model);
});
Now I get TypeError: Object prototype may only be an Object or null: undefined
error message. And this occurs because of this line:
this.http = InjectorInstance.get<HttpClient>(HttpClient);
The InjectorInstance
is the undefined
here.
How can I mock an Injector
instance into my InjectorInstance
property avoiding this way?:
constructor(
private model: T,
private injectorInstance: Injector,
) { }
Part of the issue is that you are using the export let
to declare the InjectorInstance
. This type of declaration makes it illegal to modify the field from any other file (as in: test file). One way to change that would be to make the InjectorInstance
a static field of the AppModule
class, as in:
export class AppModule {
static InjectorInstance: Injector;
constructor(private injector: Injector) {
AppModule.InjectorInstace = injector;
}
}
Then you could use the TestBed
in that field, as the interface of Injector
is actually very simple and only contains the get
method. As in :
beforeEach(async(() => {
TestBed.configureTestingModule({(...)});
}));
beforeEach(() => {
AppModule.InjectorInstance = TestBed;
});
In the current implementation you should also be able to simply do:
new AppModule(TestBed)
I think it's less descriptive what this line does, but it leaves your InjectorInstance
safer in production code.
Keep in mind that all this can make your tests affect each other, because once you change that field it will be changed from the perspective of every other test, even in other files.
Wether exchanging the dependency injection pattern for service locator pattern is a good idea is a completely different discussion.