Angular-cli: Injecting a service into a Component using TestBed.overrideComponent no longer works

Created on 9 Oct 2017  路  15Comments  路  Source: angular/angular-cli

Bug Report or Feature Request (mark with an x)

- [x ] bug report -> please search issues before submitting
- [ ] feature request

Versions.

@angular/cli: 1.4.4
node: 6.10.0
os: win32 x64
@angular/animations: 4.4.4
@angular/cdk: 2.0.0-beta.11
@angular/common: 4.4.4
@angular/compiler: 4.4.4
@angular/core: 4.4.4
@angular/forms: 4.4.4
@angular/http: 4.4.4
@angular/material: 2.0.0-beta.11
@angular/platform-browser: 4.4.4
@angular/platform-browser-dynamic: 4.4.4
@angular/router: 4.4.4
@angular/cli: 1.4.4
@angular/compiler-cli: 4.4.4
@angular/language-service: 4.4.4
typescript: 2.3.4

Repro steps.

I am injecting a service into a component using the @Component annotation / providers property:

@Component({
    providers: HelloWorldService
})
export class HelloWorldComponent {
    constructor(private helloWorldService: HelloWorldService) {}
}

After updating to @angular/[email protected], using the TestBed's overrideComponent function to provide the service throws an error:

Error: No provider for HelloWorldService!

TestBed.configureTestingModule({
    declarations: [
        HelloWorldComponent
    ]
}).overrideComponent(HelloWorldComponent, {
    set: {
        providers: [
            { provide: HelloWorldService, useFactory: helloWorldService => new HelloWorldService(null) }
        ]
    }
}).compileComponents().then(() => {
    // ...
});

Providing the service using the configureTestingModule function also throws an error:

TestBed.configureTestingModule({
    declarations: [
        HelloWorldComponent
    ],
    providers: [
        { provide: HelloWorldService, useFactory: helloWorldService => new HelloWorldService(null) }
    ]
}).compileComponents().then(() => {
    // ...
});

No provider for Http!

I can only get the component to compile if I provide the service in both the configureTestingModule function and the overrideComponent function.

The log given by the failure.

Desired functionality.

Mention any other details that might be useful.

Most helpful comment

The TestBed only knows about it's providers. The component has it's own injector as well. If you were to use the component way of providing your service, use the following:

.then(() => {
  const componentFixture = TestBed.createComponent(HelloWorldComponent);
  const service = componentFixture.componentRef.injector.get(HelloWorldService);
});

All 15 comments

@chris-jones-pixitmedia,

This looks to be an issue with the way you are testing, not the CLI.

This issue tracker is not suitable for support requests, please repost your issue on StackOverflow using tag angular-cli.

If you are wondering why we don't resolve support issues via the issue tracker, please check out this explanation.

That said, try this:

beforeEach(async(() => {
  TestBed
    .configureTestingModule({
      providers: [
        { provide: HelloWorldService, useValue: new HelloWorldService(null) }
      ],
      declarations: [HelloWorldComponent],
    })
    .compileComponents();
}));

Thanks for responding.

I'm using the approach suggested in the official documentation:

https://angular.io/guide/testing

and the code worked until I updated the CLI.

Your code results in the following error:

'No provider for Http!'

Without seeing the full stack trace, I would bet your HelloWorldService spec is not set up properly. It needs, since it depends on, Http to be injected.

import { HttpModule } from '@angular/http';

beforeEach(() => {   
  TestBed.configureTestingModule({
    imports: [HttpModule],
    providers: [HelloWorldService]
  });
});

Hi Stephen,

Thanks for your response. So, I have managed to get this working, but the changes I've had to make don't seem right to me.

I have a component with a dependency on a service, which in turn has a dependency on Http.

You used to be able to do this:

providers: [
    { provide: HelloWorldService, useFactory: helloWorldService => new HelloWorldService(null) }
]

Where you pass null or undefined to the constructor instead of Http, then spy on or stub the service's functions.

With the latest version of the CLI, this now throws a 'No provider for Http!' error. In order to compile the component, you have to explicitly define the provider for Http:

providers: [
    { provide: Http, useValue: undefined }.
    HelloWorldService
]

Even if you use a test double / mock class with no dependencies instead of the service, you still have to explicitly define the provider for Http:

providers: [
    { provide: Http, useValue: undefined }.
    { provide: HelloWorldService, useClass: HelloWorldServiceMock }
]

Is that right?

I would need to see a repo because I am getting 0 errors when I run ng test with https://github.com/delasteve/angular-cli-7977.

Also note, I changed HttpClient to Http in my last post. Slight oversight on the http you were using.

Hi Steve,

Thanks for your response. I appreciate you spending so much time on this!

I've checked out your repo and the tests do indeed pass, but if you try and render your HelloWorldComponent (e.g. by adding to app.component.html), an error is thrown (Error: No provider for HelloWorldService!) as the service isn't being provided to the component.

If you provide the service to the component:

@Component({
  selector: 'app-hello-world',
  templateUrl: './hello-world.component.html',
  styleUrls: ['./hello-world.component.css'],
  providers: [HelloWorldService]
})
export class HelloWorldComponent implements OnInit {

  constructor(
    private service: HelloWorldService
  ) { }

  ngOnInit() {
  }

}

The tests fail with 'No provider for Http!'.

I've created a repo to demonstrate: https://github.com/chris-jones-pixitmedia/angular-cli-7977

Providers ideally should be provided at the NgModule level, not the individual component level. Again, ideally. Should you however provide it on the component, here's the passing code to make it work:

  beforeEach(async(() => {
    TestBed
      .configureTestingModule({
        declarations: [HelloWorldComponent]
      })
      .overrideComponent(HelloWorldComponent, {
        set: {
          providers: [
            { provide: HelloWorldService, useValue: new HelloWorldService(null) }
          ]
        }
      })
      .compileComponents();
  }));

But if you use the NgModule way (edited for brevity):

app.module.ts

@NgModule({
...
  providers: [
    HelloWorldService
  ],
...
})
export class AppModule { }

hello-world.component.ts

@Component({
  selector: 'app-hello-world',
  templateUrl: './hello-world.component.html',
  styleUrls: ['./hello-world.component.css']
})
export class HelloWorldComponent implements OnInit {

hello-world.component.spec.ts

TestBed
  .configureTestingModule({
    providers: [
      { provide: HelloWorldService, useValue: new HelloWorldService(null) }
    ],
    declarations: [HelloWorldComponent]
  })
  .compileComponents();

I made a couple PRs to show you all the code changes. Both have all tests passing, while using HelloWorldComponent in app.component.ts.

https://github.com/delasteve/angular-cli-7977/pull/1/files
https://github.com/delasteve/angular-cli-7977/pull/2/files

NOTE: the usage of app-hello-world in app.component.ts is a red herring. You would have seen this error if you just changed hello-world.component.ts's providers. Angular's TestBed makes testing localized to that component (or directive, service, etc). The TestBed was simply setup using a different configuration than the one expected.

Hi Steve,

Yeah, I get what your saying about overriding the provider at the component level using the overrideComponent function:

  beforeEach(async(() => {
    TestBed
      .configureTestingModule({
        declarations: [HelloWorldComponent]
      })
      .overrideComponent(HelloWorldComponent, {
        set: {
          providers: [
            { provide: HelloWorldService, useValue: new HelloWorldService(null) }
          ]
        }
      })
      .compileComponents();
  }));

That's how my code was written originally. The tests compile, but try getting a reference to the injected service once you've compiled the component:

  beforeEach(async(() => {
    TestBed
      .configureTestingModule({
        declarations: [HelloWorldComponent]
      })
      .overrideComponent(HelloWorldComponent, {
        set: {
          providers: [
            { provide: HelloWorldService, useFactory: () => new HelloWorldService(null) }
          ]
        }
      })
      .compileComponents().then(() => {
        const testBed: TestBed = getTestBed();
        // Throws an error: 'No provider for HelloWorldService!'
        const injectedService = testBed.get(HelloWorldService);
      });
  }));

i've updated the repo to demonstrate: https://github.com/chris-jones-pixitmedia/angular-cli-7977

The TestBed only knows about it's providers. The component has it's own injector as well. If you were to use the component way of providing your service, use the following:

.then(() => {
  const componentFixture = TestBed.createComponent(HelloWorldComponent);
  const service = componentFixture.componentRef.injector.get(HelloWorldService);
});

Thanks Steve, my tests are passing now.

Is that a recent change? You were able to reference component providers from the test bed up until recently.

There are actually two injectors in play. The TestBed injector (acting like NgModule's injector) and the component's injector. TestBed.get() uses the providers array in configureTestingModule while the componentRef.injector is using the component's injector which uses the component level providers, which is overrode by the set in overrideComponent.

Closing this as fixed.

This issue has been automatically locked due to inactivity.
Please file a new issue if you are encountering a similar or related problem.

Read more about our automatic conversation locking policy.

_This action has been performed automatically by a bot._

Was this page helpful?
0 / 5 - 0 ratings