How to test a service/component which depends on another service which in turn depends on Http service?

Viewed 1200

I have question about unit testing a component with service as a dependency and this server depends on Http. I am reading this docs: Test a component with an async service

I have really same code like in this example:

  ngOnInit(): void {
    this.twainService.getQuote().then(quote => this.quote = quote);
  }

By the way here is my code: code

The docs says that when I test component which depends on another service, I have to:

  1. set this service as module provider
  2. get service injected into the component: twainService = fixture.debugElement.injector.get(TwainService);

  3. setup spy on it: spy = spyOn(twainService, 'getQuote').and.returnValue(Promise.resolve(testQuote));

I do same: Here is my spec file: spec file. I provide service in line 21, I get injected service in 29 and I setup spy in 32.

So the question: If I do everything like docs says I am getting error: Error: No provider for Http!. Obviously this error appears because my GoodsDataService depends on Http service. How should I handle it? I did this: I created simple javascript object and I mocked my real GoodsDataService with it. I also added getGoods method stub in this object. All this things allow me to test main component without injecting real GoodsDataService. But I am not sure at all about this solution. I think it is dirty and not correct. What is correct way of unit testing component/service which depends on another service which in turn depends on Http service? Any thoughts?

2 Answers

Summary: Yes, use a combination of the dependency stub and async service Angular doc examples.

Turns out that the TwainService does not actually use Http or other dependencies at all, it just returns a Promise:

getQuote(): Promise<string> {
    return new Promise(resolve => {
      setTimeout( () => resolve(this.nextQuote()), 500 );
    });
}

So because in our cases, the component under test depends on a service that has potentially a multitude of its own dependencies, it really is best to mock that service via the WelcomeComponent/userServiceStub example in the Angular docs (keep reading for the caveat). The stub object will have a property for the real method name, and its value is an unnamed function with its statements.

Now, it's easy to mistakenly implement a method in the stub that returns synchronously, even though the real method returns asynchronously. You don't want to do that though because the component under test likely contains a .then() block to handle the real asynchronous method's response. Therefore, make sure your stub method returns your testing-return-value wrapped in a promise: return Promise.resolve(mySwappableTestingReturnValue).

Finally, because we are indeed testing an asynchronous method (the caveat), the it block should also use one of the three options in the TwainService example: angular's async()/whenStable.then(), angular's fakeAsync()/tick(), or jasmine's done. I also found that promise's regular .then() block appended to fixture.componentInstance.myAsyncMethod() works equally well (and likely js's async/await too).

~~~

As a Spy rookie, I also made another mistake where .and.callThrough() may help you.

For example (humor me, it's short and common): LoginComponent has a submit-btn whose onclick is handled by a logMeIn method who calls this.authService.askAuthServer. askAuthServer uses Http.

The spec that tests if a button click correctly calls logMeIn is synchronous, we don't care about the returned value from logMeIn, we only care that it was called (and UI events are handled synchronously aka on the main thread, if that helps you). The spec that tests the decision logic within logMeIn DOES need the returned value so needs to be tested asynchronously, so say we use whenStable().then().

So, you happily create a spyOn logMeIn in the beforeEach, but the click spec succeeds and the async logic spec fails. In the click spec, you'll notice that askAuthServer doesn't execute, although our spy successfully .toHaveBeenCalled(). What is going on? It's because the spy prevented the execution of logMeIn, even before we chose not to wait around for logMeIn's return. So in the logic spec, although it might not use the logMeInSpy, the spy is still there preventing the execution of logMeIn (and askAuthServer too) even though we've setup the asynchronous parts correctly.

Understanding that, either use .and.callThrough() when creating the spy in beforeEach, to allow the execution of logMeIn during both specs, or move the spy into the click spec where it won't affect the logic spec.

Related