Angular TestBed.inject
Asked Answered
E

2

6

I am doing angular unit testing. Does TestBed.inject(service) create a new instance of the service? I was always under the assumption that TestBed.configureTestingModule() created the service and by inject we just accessed a reference of the created service.

Enenstein answered 3/6, 2021 at 4:40 Comment(0)
R
11

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. In the below example the dependency and token is ValueService.

TestBed.configureTestingModule({ providers: [ValueService] });

Later, you can use ValueService in your test using TestBed.inject. When you say TestBed.inject Angular dependency injection system checks if there is a provider registered against the token, if yes, then it creates an instance of the service and returns it. If it doesn't find that token, you will the famous

No provider for ValueService error.

it('should use ValueService', () => {
  service = TestBed.inject(ValueService);
  expect(service.getValue()).toBe('real value');
});

So it is similar to our application code, where in we first configure the providers and then inject it to our components to grab an instance of that service.

Ryley answered 3/6, 2021 at 5:0 Comment(2)
I have a follow-up question. suppose i have a service as a dependency in a component. If I am doing compileComponents() and creating the component then the service gets created right? Now if i do a testbed.inject(service) will I get a new instance of that service or the same instance used in the component?Enenstein
You should get the same instance in this case. WHat's happening in your case?Ryley
T
0

Based on some testing it looks like the answer to your question is YES and that whilst the primary purpose of the inject method is to return the instance of the service, as a side effect it will - the very first time it is called (because the service instance won't exist at that point) - also instantiate the service and cache it inside the module's injector for subsequent calls.

The test API documentation is a little spartan but there's a pretty good example here.

The demo application doesn't unit test the HeroService but I recreated a minimal test of this service below and added two console.log statements:

import { HttpClient } from '@angular/common/http';
import { TestBed } from '@angular/core/testing';
import { HeroService } from './hero.service';
import { MessageService } from './message.service';
describe('HeroService Test', () => {
  let service: HeroService;
  beforeEach(() => {
    console.log("CALL configureTestingModule");
    TestBed.configureTestingModule({
      providers: [HeroService,
        { provide: HttpClient, useValue: jasmine.createSpyObj('HttpClient', ['get']) },
        { provide: MessageService, useValue: jasmine.createSpyObj('MessageService', ['add']) }],
    });
    console.log("CALL inject");
    service = TestBed.inject(HeroService);
  });
  it('should be created', () => { expect(service).toBeTruthy(); });
});

Then modified the HeroService to add a console log statement to its constructor, superfluous code removed:

export class HeroService {
  private heroesUrl = 'api/heroes';  // URL to web api
  httpOptions = {
    headers: new HttpHeaders({ 'Content-Type': 'application/json' })
  };
  constructor(
    private http: HttpClient,
    private messageService: MessageService) {
      console.log("CREATE HeroService");
    }
  ...

Running the test outputs this in the logs:

✔ Browser application bundle generation complete.
LOG: 'CALL configureTestingModule'
Chrome 128.0.0.0 (Linux x86_64): Executed 0 of 8 SUCCESS (0 secs / 0 secs)
LOG: 'CALL inject'
Chrome 128.0.0.0 (Linux x86_64): Executed 0 of 8 SUCCESS (0 secs / 0 secs)
LOG: 'CREATE HeroService'
Chrome 128.0.0.0 (Linux x86_64): Executed 0 of 8 SUCCESS (0 secs / 0 secs)
Chrome 128.0.0.0 (Linux x86_64): Executed 8 of 8 SUCCESS (0.102 secs / 0.074 secs)
TOTAL: 8 SUCCESS

As can be seen from the logs the HeroService is only created after the call to inject is made, which implies executing configureTestingModule doesn't instantiate the service. Don't believe there's any asynchronous behaviour going on that could compromise this verdict. E.g. provider factory functions can't be asynchronous.

The source code has the definitive answer but this is as far as I got with respect to the inject method implementation, it returns a result from a 'get' method on the TestBed module's injector:

const result = this.testModuleRef.injector.get(token, UNDEFINED, convertToBitFlags(flags));

Whether the injector works in such a lazy fashion when not running in a unit test context, I can't say.

Treble answered 23/9, 2024 at 19:47 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.