How to mock Injector instance in Angular / Jasmine tests?
Asked Answered
U

1

6

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,
  ) { }
Unclinch answered 12/6, 2020 at 10:15 Comment(0)
R
4

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.

Resemble answered 12/6, 2020 at 13:50 Comment(5)
just for clarify, when you say "change that field" you say "change the InjectorInstance field", right? If this right, then it isn't has any sideeffect, because the original Injector instance never changing. I only use it's get() method.Unclinch
Yes, in the real app the injector instance will not change. Just keep in mind it may change between tests as there will be a new TestBed created between them.Resemble
yes, thanks! This is great advice and answer too! I need to implement it, then it's working I will mark as "solve my problem". Thanks again!Unclinch
So here is a fast result: I put the static InjectorInstance: Injector; code into my AppModule class, then in my service changed to this: this.http = AppModule.InjectorInstance.get<HttpClient>(HttpClient); and the build is OK, but in browser I get Cannot read property 'get' of undefined error message. Of course the this.InjectorInstance = this.injector; not working, because of this.InjectorInstance field is static... and undefined. The static InjectorInstance: Injector = new Injector(); not working, because it's an abstract class... How can I define it?Unclinch
I edited the answer to show how to set the static field.Resemble

© 2022 - 2024 — McMap. All rights reserved.