27

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.

6 Answers 6

25

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"
  ]
}
Sign up to request clarification or add additional context in comments.

1 Comment

Thank you. Nothing in the error message to help figure this out. The key is "emitDecoratorMetadata": true
21

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');
	});
  })));
});

14 Comments

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}
So providers: [{provide: XHRBackend: useExisting: MockBackend}, SomeService] should give you the same result.
@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.
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 ;-)
So why do I get ApiConnectorService parameter not resolved? and why only when I start both tests?
|
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.

Comments

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.

Comments

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.

Comments

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.

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.