67

Let's say I have a component that subscribes to a service function:

export class Component {

   ...

    ngOnInit() {
        this.service.doStuff().subscribe(
            (data: IData) => {
              doThings(data);
            },
            (error: Error) => console.error(error)
        );
    };
};

The subscribe call takes two anonymous functions as parameters, I've managed to set up a working unit test for the data function but Karma won't accept coverage for the error one.

enter image description here

I've tried spying on the console.error function, throwing an error and then expecting the spy to have been called but that doesn't quite do it.

My unit test:

spyOn(console,'error').and.callThrough();

serviceStub = {
        doStuff: jasmine.createSpy('doStuff').and.returnValue(Observable.of(data)),
    };

    serviceStub.doStuff.and.returnValue(Observable.throw(

        'error!'
    ));

serviceStub.doStuff().subscribe(

    (res) => {

        *working test, can access res*
    },
    (error) => {

      console.error(error);
      console.log(error);  //Prints 'error!' so throw works.
      expect(console.error).toHaveBeenCalledWith('error!'); //Is true but won't be accepted for coverage.
    }
);

What's the best practice for testing anonymous functions such as these? What's the bare minimum to secure test coverage?

isherwood
  • 58,414
  • 16
  • 114
  • 157
user3656550
  • 758
  • 1
  • 5
  • 8
  • 1
    Your test makes no sense; you're replacing the callback you say you want to test. Just testing that `subscribe` calls its second argument when an error occurs isn't testing your code, it's testing the framework, which is an anti pattern. Also you provide two different outcomes for your mock call on consecutive lines. – jonrsharpe Oct 10 '16 at 14:07
  • Okay, so what would be a good way of testing it? Throwing an error in a separate it-block? – user3656550 Oct 10 '16 at 14:12
  • 1
    You need to call the actual implementation, but you haven't shown enough of a [mcve] to guess how that could be achieved. – jonrsharpe Oct 10 '16 at 14:13
  • any update for this question? – AnJu Feb 21 '17 at 11:35

4 Answers4

118

You can simply mock Observable throw error object like Observable.throw({status: 404})and test error block of observable.

const xService = fixture.debugElement.injector.get(SomeService);
const mockCall = spyOn(xService, 'method').and.returnValue(Observable.throw({status: 404}));

Update 2019 :

Since some people are lazy to read comment let me put this here : It's a best practice to use errors for Rxjs

import { throwError } from 'rxjs'; // make sure to import the throwError from rxjs
const xService = fixture.debugElement.injector.get(SomeService);
const mockCall = spyOn(xService,'method').and.returnValue(throwError({status: 404}));

Update 2022: Use of throwError in the aforementioned way is deprecated. Instead, use throwError(() => new Error({status: 404})):

import { throwError } from 'rxjs'; // make sure to import the throwError from rxjs
const xService = fixture.debugElement.injector.get(SomeService);
const mockCall = spyOn(xService,'method').and.returnValue(throwError(() => new Error({status: 404})));
Nickofthyme
  • 3,032
  • 23
  • 40
Aniruddha Das
  • 20,520
  • 23
  • 96
  • 132
  • 36
    Use `import { throwError } from 'rxjs'` and then `returnValue(throwError({status: 404}));` for rxjs >= v6. – bersling Mar 12 '19 at 13:48
  • @Aniruddha, how can I return an error object instead of a string? I need to return `{ error: { message: 'Error' }}` – s.khan Jul 19 '23 at 11:57
21

Not sure exactly the purpose of the code you are showing, which is trying to test a mock service. The coverage problem is with the component and the error callback to not have been called (which is only called when there is an error).

What I usually do for most of my observable services, is to create a mock whose methods just returns itself. The mock service has a subscribe method that accepts the next, error, and complete callbacks. The user of the mock gets to configure it to add an error so the error function gets called, or add data, so the next method gets called. The thing I like most about this is that it's all synchronous.

Below is something like what I normally use. It's just an abstract class for other mocks to extend. It provides the basic functionality that an observable provides. The extending mock service should just add the methods it needs, returning itself in the method.

import { Subscription } from 'rxjs/Subscription';

export abstract class AbstractMockObservableService {
  protected _subscription: Subscription;
  protected _fakeContent: any;
  protected _fakeError: any;

  set error(err) {
    this._fakeError = err;
  }

  set content(data) {
    this._fakeContent = data;
  }

  get subscription(): Subscription {
    return this._subscription;
  }

  subscribe(next: Function, error?: Function, complete?: Function): Subscription {
    this._subscription = new Subscription();
    spyOn(this._subscription, 'unsubscribe');

    if (next && this._fakeContent && !this._fakeError) {
      next(this._fakeContent);
    }
    if (error && this._fakeError) {
      error(this._fakeError);
    }
    if (complete) {
      complete();
    }
    return this._subscription;
  }
}

Now in your tests you just do something like

class MockService extends AbstractMockObservableService {
  doStuff() {
    return this;
  }
}

let mockService;
beforeEach(() => {
  mockService = new MockService();
  TestBed.configureTestingModule({
    providers: [{provide: SomeService, useValue: mockService }],
    declarations: [ TestComponent ]
  });
});
it('should call service success', () => {
  mockService.content = 'some content';
  let fixture = TestBed.createComponent(TestComponent);
  // test component for success case
});
it('should call service error', () => {
  mockService.error = 'Some error';
  let fixture = TestBed.createComponent(TestComponent);
  // test component for error case
  // this should handle your coverage problem
});

// this assumes you have unsubscribed from the subscription in your
// component, which you should always do in the ngOnDestroy of the component
it('should unsubscribe when component destroyed', () => {
  let fixture = TestBed.createComponent(TestComponent);
  fixture.detectChanges();
  fixture.destroy();
  expect(mockService.subscription.unsubscribe).toHaveBeenCalled();
})
Paul Samsotha
  • 205,037
  • 37
  • 486
  • 720
3

Update 2022

Since of() seems to be deprecated, I'm using it like this:

jest.spyOn(service, 'query').mockReturnValue(throwError(() => new Error("a error message")));
Daniel Gerber
  • 3,226
  • 3
  • 25
  • 32
0

I had mocked the fake error response in service.ts

By commenting previous http call and

let error:any = new Error("failed"); return new Observable(error);