4

Angular 2.0.0 - Ionic 2 RC0 - Npm 3.10.8 - Node v4.5.0 - Karma 1.3.0 - Jasmine 2.5.2


I'm trying to test my application with Karma & Jasmine. Now I got to the point where I followed some guides (I'm new to these testing frameworks). But sadly enough I'm getting an error when trying to execute my test.

I'm trying to test EventsPage which doesn't have an Http import but it calls my APICaller.service which does use Http. That's why I created a MockAPICaller but it seems to still want Http (maybe because it is in APICaller's constructor, but I wouldn't know how to fix that).

So I suspect the problem is within MockAPICaller but I don't know for sure.


I'll post MockAPICaller.service, APICaller.service, EventsPage and my events.spec.ts. (in that order so you could maybe skip along if you need/want to.

MockAPICaller

import { SpyObject } from './helper';
import { APICaller } from '../apicaller.service';
import Spy = jasmine.Spy;

export class MockAPICaller extends SpyObject {
    getEventsSpy: Spy;
    searchEventSpy:Spy;
    getParticipantSpy:Spy;
    getEventParticipantsSpy:Spy;
    searchEventParticipantSpy:Spy;
    addNewCommentSpy:Spy;
    updateCommentSpy:Spy;
    deleteCommentSpy:Spy;
    getUsernameSpy:Spy;
    presentSuccessMessageSpy:Spy;

    fakeResponse:any;

    constructor(){
        super( APICaller );
        this.fakeResponse = null;
        this.getEventsSpy = this.spy('getEvents').andReturn(this);
        this.searchEventSpy = this.spy('searchEvent').andReturn(this);
        this.getParticipantSpy = this.spy('getParticipant').andReturn(this);
        this.getEventParticipantsSpy = this.spy('getEventParticipant').andReturn(this);
        this.searchEventParticipantSpy = this.spy('searchEventParticipant').andReturn(this);
        this.addNewCommentSpy = this.spy('addNewComment').andReturn(this);
        this.updateCommentSpy = this.spy('updateComment').andReturn(this);
        this.deleteCommentSpy = this.spy('deleteComment').andReturn(this);
        this.getUsernameSpy = this.spy('getUsername').andReturn(this);
        this.presentSuccessMessageSpy = this.spy('presentSuccessMessage').andReturn(this);
    }

    subscribe(callback: any){
        callback(this.fakeResponse);
    }

    setResponse(json:any):void{
        this.fakeResponse = json;
    }
}

APICaller

import { Injectable, Inject } from '@angular/core';
import { Http } from '@angular/http';
import { ToastController } from 'ionic-angular';
import { Observable } from 'rxjs/Observable';
import 'rxjs/Rx';

import { Event } from '../models/event.model';
import { Participant } from '../models/participant.model';
import { Comment } from '../models/comment.model';

@Injectable()
export class APICaller {
    http : Http;

    //baseUrl to the REST API
    baseUrl : string = "http://some.correct.url:8080/myAPI";

    constructor(public httpService: Http, public toastCtrl:ToastController) {
        this.http = httpService;
    }


    //-------------------EVENTS-----------------------------------//

    //retrieves all the events
    getEvents() : Observable<Array<Event>> {
        return this.http
        .get(`${ this.baseUrl }/events`)
        .map(response => {
            return response.json();
        });
    }

    //searches events with the provided term
    searchEvent(searchTerm : string) : Observable<Array<Event>> {
        return this.http
        .get(`${ this.baseUrl }/events/search/${ searchTerm }`)
        .map(response => {
            return response.json();
        });
    }

    //--------------------PARTICIPANTS-----------------------------------//

    //retrieves the participant from the REST API
    getParticipant(participantId : number) : Observable<Participant>{
        return this.http
        .get(`${ this.baseUrl }/participants/${ participantId }`)
        .map(response => {
            return response.json();
        });
    }

    getEventParticipants(eventId:number) : Observable<Array<Participant>> {
        return this.http
        .get(`${ this.baseUrl }/events/${ eventId }/participants`)
        .map(response => {
            return response.json();
        });
    }

    //searches for deelnemers with the provided term
    searchEventParticipant(eventId : number, searchTerm : string) : Observable<Array<Participant>> {
        return this.http
        .get(`${ this.baseUrl }/events/${ eventId }/participants/search/${ searchTerm }`)
        .map(response => {
            return response.json();
        });
    }


    //-------------------COMMENTS--------------------------------------//

    //adding a new comment to a participant
    addNewComment(participantId : number, content : string) : Observable<Comment> {
        return this.http
        .post(`${ this.baseUrl }/participants/${ participantId }/addComment`
        ,{
            user: this.getUsername("apikey"),
            content: content
        }).map((response) => {
            this.presentSuccessMessage("Comment added");
            return (response.json());
        });
    }

    //updating an existing comment
    updateComment(participantId : number, commentId : number, content : string) : Observable<Comment> {
        return this.http
        .put(`${ this.baseUrl }/participants/${ participantId }/updateComment/${ commentId }`,{
            id: commentId,
            content: content
        }).map(response => {
            this.presentSuccessMessage("Comment updated");
            return response.json();
        });
    }

    //deleting a currently existing comment
    deleteComment(participantId : number, commentId : number) : Observable<Comment> {
        return this.http
        .delete(`${ this.baseUrl }/participants/${ participantId }/deleteComment/${ commentId }`)
        .map(response => {
            this.presentSuccessMessage("Comment deleted");
            return response.json();
        });
    }

    //presents a successmessage for 3 seconds
    presentSuccessMessage(messageContent : string) {
        //defining the message
        let message = this.toastCtrl
        .create({
            message: messageContent,
            duration: 3000
        });
        //showing the message on screen
        message.present();
    }

    //-------------------USER-------------------------------
    getUsername(someRandomKey : string) : string {
        return "developer";
      /*
        return this.http
        .get(`${ this.baseUrl }/getUsername/${ someRandomKey }`)
        .map(response => {
            return ;
        });
        */
    }
}

EventsPage

import { Component } from '@angular/core';

import { NavController, Loading, LoadingController } from 'ionic-angular';

import { APICaller } from '../../services/apicaller.service';
import { EventDetailComponent } from '../event-detail/event-detail.component';
import { Event } from '../../models/event.model';

/*
  Class for Evenementen Overzicht.
*/
@Component({
  selector: 'events-component',
  templateUrl: 'events.component.html',
  providers: [ APICaller ]
})

 /** -------------------------------------------------------------------------------------- */

export class EventsPage {

//list of all events
public events : Array<Event>;
//the event that has been clicked on the page
public selectedEvent : Event;
//boolean to show 'no events' error message
public noEvents:boolean;

 /** -------------------------------------------------------------------------------------- */

  constructor(public navCtrl : NavController, public apiCaller:APICaller, public loadingCtrl : LoadingController) {
    //retrieve all events --> async method, can't use this.events yet.
    this.getEvents();
  }

  /** -------------------------------------------------------------------------------------- */

  /**Get Events - Sets the 'events' variable to all events found by the API. */
  getEvents(){
    //setup a loadingscreen
    let loading = this.loadingCtrl.create({
      content: "Loading..."
    }); 
    //present the loadingscreen
    loading.present();

    //reset the noEvents boolean.
    this.noEvents = true;

    //call the api and get all events
    this.apiCaller.getEvents()
    .subscribe(response => {
      //response is list of events
      this.events = response;
      //if the event is not empty, set noEvents to false.
      if(this.events.length > 0){
        this.noEvents = false;
      }
      //close the loading message.
      loading.dismiss();
    });
  }

 /** -------------------------------------------------------------------------------------- */

  /**Select Event - Sets the selectedEvent variable to the selected item. */
  selectEvent(event: any, eventObj){
    this.selectedEvent = eventObj;
  }

  /**Search Events - Triggers the API and sets events equal to events found by API*/
  searchEvents(ev){
  //reset noEvents
  this.noEvents = true;
  //if the searchfield is not empty, call api
  if(ev.target.value != ''){
    this.apiCaller.searchEvent(ev.target.value)
    .subscribe(response => {

      this.events = response;
      //
      if(this.events.length > 0){
        this.noEvents = false;

      }
    });
  }else{
    //if the searchfield is empty, get all the events
    this.getEvents();
  }
}

/** -------------------------------------------------------------------------------------- */

/*Cancel Search - clears input field and resets noEvents*/
cancelSearch(ev){
  ev.target.value = "";
  this.noEvents = false;
}
 /** -------------------------------------------------------------------------------------- */

 /**Do Refresh - Refreshes the list of  */
doRefresh(refresher) {
    this.getEvents();

    //giving feedback for user (1 sec instead of faster)
    setTimeout(() => {
      //stop the refresher
      refresher.complete();
    }, 1000);
  }

 /** -------------------------------------------------------------------------------------- */

 /**Go to EventDetail - Pushes the EventDetail page on the navigation stack. */
  goToEventDetail(eventOb: any, eventParam){
    this.navCtrl.push(EventDetailComponent
    , {
      event: eventParam
    });
  }

}
 /** -------------------------------------------------------------------------------------- */

events.spec.ts

import { TestBed, inject, tick, fakeAsync } from '@angular/core/testing';
import { BaseRequestOptions, Http, ConnectionBackend, Response, ResponseOptions} from '@angular/http';
import { MockBackend } from '@angular/http/testing';
import { FormsModule } from '@angular/forms';
import { NavController, LoadingController } from 'ionic-angular';
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { mockNavController } from 'ionic-angular/util/mock-providers';
import { EventsPage } from './events.component';
import { MockAPICaller } from '../../services/mocks/apicaller.service';
import { APICaller } from '../../services/apicaller.service';

describe('Component: EventsComponent', () => {
  let mockAPICaller : MockAPICaller = new MockAPICaller();

  beforeEach(() => {
  TestBed.configureTestingModule({
    declarations: [EventsPage],
    schemas: [CUSTOM_ELEMENTS_SCHEMA],//for usage of Ionic2
    providers: [
      {provide: NavController, useValue: mockNavController },
      {provide: LoadingController, useValue: LoadingController},
      {provide: APICaller, useValue: mockAPICaller}
    ],
    imports: [FormsModule]
  });
  });


  it('should return all events', ()=>{
      let fixture = TestBed.createComponent(EventsPage);
      let eventsPage = fixture.debugElement.componentInstance;
      fixture.detectChanges();

      mockAPICaller.setResponse(JSON.stringify(`{
        id: 4,
        title: 'Weekend',
        eventdate: '24/09/2016',
        kind: 'closed',
        startingtime: '18:00',
        endtime: '21:00',
        description: 'Go home'
      }`));
      let results = eventsPage.getEvents();
      expect(results.length).toBe(1);
      expect(results[0].id).toBe(4);

  });
});
Ivar Reukers
  • 7,560
  • 9
  • 56
  • 99

2 Answers2

8

The problem is this

@Component({
  providers: [ APICaller ] <========
})
export class EventsPage {

By having that, the component will try to create its own instance of the APICaller. This overrides any configurations you make in the TestBed (i.e. the mock).

What you can do is override the component before you create it

beforeEach(() => {
  TestBed.configureTestingModule({})

  TestBed.overrideComponent(EventsPage, {
    set: {
      providers: [
        { provide: APICaller, useValue: mockApiCaller }
      ]
    }
  })
})

See Also:

Paul Samsotha
  • 205,037
  • 37
  • 486
  • 720
4

Your problem here is that you forgot to mock the Http service.

In mockAPICaller, you do this in constructor: super( APICaller );

But APICaller needs Http to get created, but you don't have any providers for Http, that's why you get this error.

Now, since Http should never be used in a test, you have to create a mock to be able to provide your own backend to send custom responses:

a good example provided by Peeskillet on Testing - Can't resolve all parameters for (ClassName):

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

As you can see it provides a custom backend, sending custom responses to the tested service, allowing you to test it properly without HttpModule.

Community
  • 1
  • 1
Supamiu
  • 8,501
  • 7
  • 42
  • 76
  • Thanks for your answer, just wondering: Will I need to mock the backend in the `events.spec.ts`? Or can I somehow manage this on `MockApiCaller`? – Ivar Reukers Oct 06 '16 at 09:22
  • You can do both, but if you mock the HttpBackend, you don't have to mock anything else, since you should have the same result with the same response. – Supamiu Oct 06 '16 at 09:24
  • Sorry to bother you with these probably stupid question but I'm kinda new to this and a bit confused. When I'm trying your code I'm mocking my `APICaller` (`service.getSomething` is using APICaller's methods.) How can I use this in my `events.spec.ts` which calls the APICaller? Because this is quite good for testing my Service itself, but how can I through this method test the methods depending on that Service – Ivar Reukers Oct 06 '16 at 09:30
  • Weird question but I don't know how to formulate/implement it. It seems that, with this way I'm testing my Service. But f.e. how can I use this in my MockAPICaller to use this in the constructor / inject it? – Ivar Reukers Oct 06 '16 at 09:32
  • Upvoted for the effort and answer, I still will use this for testing my service. Thanks :) – Ivar Reukers Oct 06 '16 at 10:09