Testing - Can't resolve all parameters for (ClassName)
Asked Answered
N

6

26

Context

I created an ApiService class to be able to handle our custom API queries, while using our own serializer + other features.

ApiService's constructor signature is:

constructor(metaManager: MetaManager, connector: ApiConnectorService, eventDispatcher: EventDispatcher);
  • MetaManager is an injectable service that handles api's metadatas.
  • ApiConnectorService is a service which is wrapping Http to add our custom headers and signature system.
  • EventDispatcher is basically Symfony's event dispatcher system, in typescript.

Problem

When I test the ApiService, I do an initialization in beforeEach:

beforeEach(async(() => {
    TestBed.configureTestingModule({
        imports  : [
            HttpModule
        ],
        providers: [
            ApiConnectorService,
            ApiService,
            MetaManager,
            EventDispatcher,
            OFF_LOGGER_PROVIDERS
        ]
    });
}));

and it works fine.

Then I add my second spec file, which is for ApiConnectorService, with this beforeEach:

beforeEach(async(() => {
    TestBed.configureTestingModule({
        imports  : [HttpModule],
        providers: [
            ApiConnectorService,
            OFF_LOGGER_PROVIDERS,
            AuthenticationManager,
            EventDispatcher
        ]
    });
}));

And all the tests fail with this error:

Error: Can't resolve all parameters for ApiService: (MetaManager, ?, EventDispatcher).

  • If I remove api-connector-service.spec.ts (ApiConnectorService's spec file) from my loaded tests, ApiService's tests will succeed.
  • If I remove api-service.spec.ts (ApiService's spec file) from my loaded tests, ApiConnectorService's tests will succeed.

Why do I have this error? It seems like the context between my two files are in conflict and I don't know why and how to fix this.

Nathannathanael answered 13/9, 2016 at 14:20 Comment(0)
C
20

It's because the Http service can't be resolved from the HttpModule, in a test environment. It is dependent on the platform browser. You shouldn't even be trying to to make XHR calls anyway during the tests.

For this reason, Angular provides a MockBackend for the Http service to use. We use this mock backend to subscribe to connections in our tests, and we can mock the response when each connection is made.

Here is a short complete example you can work off of

import { Injectable } from '@angular/core';
import { async, inject, TestBed } from '@angular/core/testing';
import { MockBackend, MockConnection } from '@angular/http/testing';
import {
  Http, HttpModule, XHRBackend, ResponseOptions,
  Response, BaseRequestOptions
} from '@angular/http';

@Injectable()
class SomeService {
  constructor(private _http: Http) {}

  getSomething(url) {
	return this._http.get(url).map(res => res.text());
  }
}

describe('service: SomeService', () => {
  beforeEach(() => {
	TestBed.configureTestingModule({
	  providers: [
		{
		  provide: Http, useFactory: (backend, options) => {
			return new Http(backend, options);
		  },
		  deps: [MockBackend, BaseRequestOptions]
		},
		MockBackend,
		BaseRequestOptions,
		SomeService
	  ]
	});
  });

  it('should get value',
	async(inject([SomeService, MockBackend],
				 (service: SomeService, backend: MockBackend) => {

	backend.connections.subscribe((conn: MockConnection) => {
	  const options: ResponseOptions = new ResponseOptions({body: 'hello'});
	  conn.mockRespond(new Response(options));
	});

	service.getSomething('http://dummy.com').subscribe(res => {
	  console.log('subcription called');
	  expect(res).toEqual('hello');
	});
  })));
});
Cristinacristine answered 14/9, 2016 at 6:19 Comment(14)
Why don't you just use MockBackend, BaseRequestOptions instead of { provide: MockBackend, useClass: MockBackend }, { provide: BaseRequestOptions, useClass: BaseRequestOptions },. Aren't these two already provided by HttpModule (I'm pretty sure they are). Also providers: [ { provide: Http, useFactory: (backend, options) => { return new Http(backend, options); }, deps: [MockBackend, BaseRequestOptions] }, can be shortened to {provide: XHRBackend: useExisting: MockBackend}Radiolocation
So providers: [{provide: XHRBackend: useExisting: MockBackend}, SomeService] should give you the same result.Radiolocation
@GünterZöchbauer Yeah you're right. I previously was using XHRBackend as the token, but I forgot I was injecting MockBackend type. So I changed it to MockBackend token. Wasn't thinking about style at the time, it was just a quick fix at the time I was testing.Cristinacristine
As far as I remember the docs at Angular.io also show this long way. Your answer is fine. Just wanted to point it out. I have seen many examples where people use way too complicated configuration and then complain DI is cumbersome ;-)Radiolocation
So why do I get ApiConnectorService parameter not resolved? and why only when I start both tests?Nathannathanael
Maybe because you trying to inject the apiservice and not the connector service, and error tells you what missing from the service you are trying to inject.Cristinacristine
Check the example ;) it's what I'm injecting. I changed HttpModule for a MockBackend and tried this way, still having the same error.Nathannathanael
Can you post a complete example. Enough to reproduce the problem. Something like I did above.Cristinacristine
I tried to create a complete example using the quickstart template, seems like I can't. The issue must be from my code but it's really too heavy to paste it here. Since the providers are all good and I provide a good Http now (with MockBackend) I don't see where this can come from...Nathannathanael
What I would is just add stripped down versions of the classes with only the constructor and a single method to test. The problem is the injection, which can be reproduced without anything else in the classes. If you get it to work with the stripped down versions, start adding things until it breaks. Then you know the problem.Cristinacristine
You can work off of this gist.github.com/psamsotha/2cce26fe4c9a3e73b8c8cbaa0fd738be, which works fineCristinacristine
I just came across this github.com/angular/angular.io/issues/… and thought about you. A shot in the dark, but could this be a possible issue?Cristinacristine
@peeskillet this is exactly the problem I had, I just came here to say it and I saw your comment... :DNathannathanael
My service constructor looks like constructor(private http: Http, baseName: string) { , so where I need to modify in the above code?Bony
A
24

Using Jest?

In case anyone gets here AND you're using Jest to test your Angular app (hopefully we're a growing minority), you will run into this error if you are not emitting decorators ("emitDecoratorMetadata":true). You'll need to update your tsconfig.spec.json file so it looks like:

{
  "extends": "../../tsconfig.json",
  "compilerOptions": {
    "emitDecoratorMetadata": true,
    "outDir": "../../out-tsc/spec",
    "types": [
      "jest",
      "node"
    ]
  },
  "files": [
  ],
  "include": [
    "**/*.spec.ts",
    "**/*.d.ts"
  ]
}
Anubis answered 1/2, 2020 at 23:29 Comment(1)
Thank you. Nothing in the error message to help figure this out. The key is "emitDecoratorMetadata": truePul
C
20

It's because the Http service can't be resolved from the HttpModule, in a test environment. It is dependent on the platform browser. You shouldn't even be trying to to make XHR calls anyway during the tests.

For this reason, Angular provides a MockBackend for the Http service to use. We use this mock backend to subscribe to connections in our tests, and we can mock the response when each connection is made.

Here is a short complete example you can work off of

import { Injectable } from '@angular/core';
import { async, inject, TestBed } from '@angular/core/testing';
import { MockBackend, MockConnection } from '@angular/http/testing';
import {
  Http, HttpModule, XHRBackend, ResponseOptions,
  Response, BaseRequestOptions
} from '@angular/http';

@Injectable()
class SomeService {
  constructor(private _http: Http) {}

  getSomething(url) {
	return this._http.get(url).map(res => res.text());
  }
}

describe('service: SomeService', () => {
  beforeEach(() => {
	TestBed.configureTestingModule({
	  providers: [
		{
		  provide: Http, useFactory: (backend, options) => {
			return new Http(backend, options);
		  },
		  deps: [MockBackend, BaseRequestOptions]
		},
		MockBackend,
		BaseRequestOptions,
		SomeService
	  ]
	});
  });

  it('should get value',
	async(inject([SomeService, MockBackend],
				 (service: SomeService, backend: MockBackend) => {

	backend.connections.subscribe((conn: MockConnection) => {
	  const options: ResponseOptions = new ResponseOptions({body: 'hello'});
	  conn.mockRespond(new Response(options));
	});

	service.getSomething('http://dummy.com').subscribe(res => {
	  console.log('subcription called');
	  expect(res).toEqual('hello');
	});
  })));
});
Cristinacristine answered 14/9, 2016 at 6:19 Comment(14)
Why don't you just use MockBackend, BaseRequestOptions instead of { provide: MockBackend, useClass: MockBackend }, { provide: BaseRequestOptions, useClass: BaseRequestOptions },. Aren't these two already provided by HttpModule (I'm pretty sure they are). Also providers: [ { provide: Http, useFactory: (backend, options) => { return new Http(backend, options); }, deps: [MockBackend, BaseRequestOptions] }, can be shortened to {provide: XHRBackend: useExisting: MockBackend}Radiolocation
So providers: [{provide: XHRBackend: useExisting: MockBackend}, SomeService] should give you the same result.Radiolocation
@GünterZöchbauer Yeah you're right. I previously was using XHRBackend as the token, but I forgot I was injecting MockBackend type. So I changed it to MockBackend token. Wasn't thinking about style at the time, it was just a quick fix at the time I was testing.Cristinacristine
As far as I remember the docs at Angular.io also show this long way. Your answer is fine. Just wanted to point it out. I have seen many examples where people use way too complicated configuration and then complain DI is cumbersome ;-)Radiolocation
So why do I get ApiConnectorService parameter not resolved? and why only when I start both tests?Nathannathanael
Maybe because you trying to inject the apiservice and not the connector service, and error tells you what missing from the service you are trying to inject.Cristinacristine
Check the example ;) it's what I'm injecting. I changed HttpModule for a MockBackend and tried this way, still having the same error.Nathannathanael
Can you post a complete example. Enough to reproduce the problem. Something like I did above.Cristinacristine
I tried to create a complete example using the quickstart template, seems like I can't. The issue must be from my code but it's really too heavy to paste it here. Since the providers are all good and I provide a good Http now (with MockBackend) I don't see where this can come from...Nathannathanael
What I would is just add stripped down versions of the classes with only the constructor and a single method to test. The problem is the injection, which can be reproduced without anything else in the classes. If you get it to work with the stripped down versions, start adding things until it breaks. Then you know the problem.Cristinacristine
You can work off of this gist.github.com/psamsotha/2cce26fe4c9a3e73b8c8cbaa0fd738be, which works fineCristinacristine
I just came across this github.com/angular/angular.io/issues/… and thought about you. A shot in the dark, but could this be a possible issue?Cristinacristine
@peeskillet this is exactly the problem I had, I just came here to say it and I saw your comment... :DNathannathanael
My service constructor looks like constructor(private http: Http, baseName: string) { , so where I need to modify in the above code?Bony
V
9

The issue wasn't really solved in the chosen answer, which is really just a recommendation for writing tests, but rather in the comments, and you have to follow a link and search for it there. Since I had another issue with the same error, I'll add both solutions here.

  1. Solution to the OP's issue:

If you have a barrel (index.ts or multi export file) like this:

export * from 'my.component' // using my.service via DI
export * from 'my.service'

Then you could get an error like EXCEPTION: Can't resolve all parameters for MyComponent: (?).

To fix it, you have to export the service before the component:

export * from 'my.service'
export * from 'my.component' // using my.service via DI
  1. Solution to my issue:

The same error can happen due to a circular dependency which causes an undefined service import. To check, console.log(YourService) after importing it (in your test file - where the issue is happening). If it's undefined, you may have made an index.ts file (barrel) exporting both the service and the file using it (component/effect/whatever you're testing) - by importing the service from the index file where both are exported (making it full circle).

Find that file and import the service you need directly from your.service.ts file instead of the index.

Varietal answered 3/7, 2017 at 10:55 Comment(0)
F
1

[JEST and ANGULAR]

Also, the problem may occur when you use an external module and you do not import it but use it on your service.

Ex:

import { TestBed } from '@angular/core/testing';
import <ALL needed> from '@ngx-translate/core';

import { SettingsService } from '../../../app/core/services/settings/settings.service';


describe('SettingsService', () => {
  let service: SettingsService;

  beforeAll(() => {
    TestBed.configureTestingModule({
      providers: [
        SettingsService,
        // <All needed>
      ]
    });
    service = TestBed.inject<SettingsService>(SettingsService);
  });

  it('should be created', () => {
    expect(service).toBeTruthy();
  });

});

Errors will get you nowhere ... But, if you do that this way:

import { TestBed } from '@angular/core/testing';

import { TranslateModule } from '@ngx-translate/core';

import { SettingsService } from '../../../app/core/services/settings/settings.service';


describe('SettingsService', () => {
  let service: SettingsService;

  beforeAll(() => {
    TestBed.configureTestingModule({
      imports: [TranslateModule.forRoot()], // <---
      providers: [
        SettingsService
      ]
    });
    service = TestBed.inject<SettingsService>(SettingsService);
  });

  it('should be created', () => {
    expect(service).toBeTruthy();
  });

});

Problem disappears.

Faustinafaustine answered 28/10, 2020 at 13:37 Comment(0)
C
1

[JEST and ANGULAR]

In my case, the root cause is the circular dependency, but not the "import service from index" situation. And ng build <project> --prod didn't find the "circular dependency".

Solution:

In the service/component, injecting Injector and injector.get(Service) instead.

Ciracirca answered 28/5, 2021 at 9:26 Comment(0)
D
0

[Jest and Angular] In my case I was creating a dummy component class that inherited a base component which was what I was interested in testing. The problem was that it was set to use the default constuctor, so TestBed didn't have a chance to inject a stubService for me. This is what the code looks like:

class DummyComponent extends MyBaseComponent {
  constructor(localizationService: LocalizationService) {
    super(localizationService) // this is needed constructor
  }
...
let fixture: ComponentFixture<DummyComponent>
beforeEach(() => {
    TestBed.configureTestingModule({
      declarations: [DummyComponent],
      imports: [{ provide: MyService, useValue: MyStubService}],
    })
  })
   fixture = TestBed.createComponent(DummyComponent) // <-- It was failing here
}

Looking back it seems more obvious because a concrete class will have to define the constructor to get the service. I just thought that would be the default constructor.

Dickinson answered 4/2, 2021 at 17:43 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.