19

I have a service (ChildService) which depends on another service (InteractWithServerService). The latter service (InteractWithServerService) is used to make server calls and return an observable of "any" type. For simplicity let's assume it returns an observable. I am trying to write a unit test for ChildService.

ChildService

@Injectable()
export class ApplicationService {
    constructor(private  interactWithServerService:InteractWithServerService){;}

    public GetMeData():string {
        var output:string;       
        this.interactWithServerService.get("api/getSomeData").
           subscribe(response =>{console.log("server response:", response);
           output=response});        
         return output;
    }
}

ServerInteractionService

@Injectable()
export class InteractWithServerService {        
    constructor(private http: Http) {
        ;
    }    
    get(url: string): Observable<any> {        
        return this.http.get(this.url);
    }       
}

The test case works fine when I mock the dependent service. i.e.,

class MockInteractWithServerService {
    get() {
        return Observable.of("some text");
    }           
}

describe('Service:ChildService', () => {
    let childService: ChildService;

    beforeEach(() => {
        TestBed.configureTestingModule({
            providers: [
             { provide: InteractWithServerService, useClass: MockInteractWithServerService },
                ChildService],
        });


    beforeEach(inject([ChildService], (actualService: ChildService) => {
        childService= actualService;        
    }));

    fit('should call server-call testCall()', () => {
        let actualReturnvalue= childService.GetMeData();        
        expect(actualReturnvalue).toBe("some text");
    });
});

The above method is not preferred as I might end up writing "n" mock classes for "n" dependencies. So I want to create my unit tests using spyOn. However, the test case doesn't work and throws "Error: No provider for Http!". While I understand what the error is, I would like to know why it is thrown although I am spying on the dependent service. Looks like the "spyOn" is not working.

describe('Service:ChildService', () => {
    let childService: ChildService;

    beforeEach(() => {
        TestBed.configureTestingModule({
            providers: [
             InteractWithServerService,
                ChildService],
        });

        spyOn(InteractWithServerService.prototype, 'get').and
             .callFake(()=>      
          {return Observable.of("some text");});       
    });
    beforeEach(inject([ChildService], (actualService: ChildService) => {
        childService= actualService;        
    }));

    fit('should call server-call testCall()', () => {
        let actualReturnvalue= childService.GetMeData();        
        expect(actualReturnvalue).toBe("some text");
    });
});

Am I missing something obvious?

risingTide
  • 1,754
  • 7
  • 31
  • 60
Sudhir V
  • 243
  • 1
  • 2
  • 8

3 Answers3

24

However, the test case doesn't work and throws "Error: No provider for Http!".

Because you still have the service in the providers, so Angular is trying to create it still

providers: [
 InteractWithServerService,
    ChildService],

What you can do instead of creating a mock class is to just do something like

providers: [
  { 
    provide: InteractWithServerService,
    useValue: { get: Observable.of(..) }
  }
]

Here's you're using useValue which provide any object. That will be the value used when injected. In the case above, it is just some arbitrary object with your mock method.

If you want to spy so that you can provide different values, you could inject the InteractWithServerService, and then do

spyOn(service, 'get').and.returnValue(Observable.of(...))
// do test

Another thing you could do is mock the Http with a dummy object

{ provide: Http, useValue: {} }

Now the InteractWithServerService will work (just adding the class to the providers` like you currently have). And you can just spy on it

let service = TestBed.get(InteractWithServerService);
spyOn(service, 'get').and.returnValue(..)
// do test
Paul Samsotha
  • 205,037
  • 37
  • 486
  • 720
  • Thanks @peeskillet. Works like a charm. Only thing that concerns is "useValue" and returning a arbitrary object for each method you will be calling. It definitely looks dirty (rather not so clean). Is this the way angular wants us to write the tests? – Sudhir V Feb 22 '17 at 04:14
  • An "arbitrary object" is not really any different from using a mock class. Only difference is that one is encapsulated in a class. In the end they are both just arbitrary objects. Your class does not extend the real service interface. What makes that not arbitrary? All that matters is that the object has the method that will be called. – Paul Samsotha Feb 22 '17 at 04:37
  • No way I am arguing that mock classes are better than useValue. My point is both - mock class and useValue solution doesn't look clean when compared to old way just injecting the dependent classes and spying on them. – Sudhir V Feb 22 '17 at 06:04
  • 1
    What if the service has 5 dependencies, and what if those dependencies have dependencies, and so on? You will have to configure the infrastructure to inject all of those services. It's a lot of work, when all you need to do is mock it. If you look at the Angular docs for testing, you will see that they do make use of mocks. – Paul Samsotha Feb 22 '17 at 06:14
  • 1
    I am trying to use `spyOn(service, 'get').and.returnValue(Observable.of(...))` but having a `Cannot find name 'Observable'` error. What is the import I am missing? – Fábio Roberto Teodoro Jul 10 '17 at 11:50
  • Found it out. To use `Observable.of()` is needed this imports: `import { Observable} from 'rxjs/Observable'; import 'rxjs/add/observable/of';` – Fábio Roberto Teodoro Jul 10 '17 at 12:08
  • Hello, i wanted to know that can we call returnValue 2 times inside the same "it()" with different values such that the component gets and renders both of them, or will it override some? – KL_KISNE_DEKHA_HAI May 14 '20 at 11:05
3

Using jasmin 2.6.2: get is a function so you need to add the arrow function notation to the answer above:

providers: [
  { 
    provide: InteractWithServerService,
    useValue: { get: () => Observable.of(..) }
  }
]
dataphile
  • 344
  • 1
  • 5
  • 13
1

Another more elegant way I found is to spy on the observable property and return a value, after which you flush within a fakeAsync. This will allow you to test the code inside your subscribe.

Simplified Version

.ts File

   private isThereWork(): void {
    this.subscription.add(this.work$
    .subscribe((hasWork: boolean) => {
      this.displayWorkItems = hasWork;
      })
    );
  }

.spec.ts

fit('should work be displayed', fakeAsync(() => {
    spyOnProperty<any>(component, 'work$', 'get').and.returnValue(of(false));

    component['isThereWork']();
    flush();
    expect(component.displayWorkItems ).toBe(false);
  }));

Hope this helps. I thought this a game changer when I found out.

L1ghtk3ira
  • 3,021
  • 6
  • 31
  • 70