简体   繁体   English

Angular 2 - Mocking - 没有HTTP的提供者

[英]Angular 2 — Mocking - No Provider for HTTP

Angular 2.0.0 - Ionic 2 RC0 - Npm 3.10.8 - Node v4.5.0 - Karma 1.3.0 - Jasmine 2.5.2 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. 我正在尝试用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. 我想测试EventsPage不具有Http进口,但它要求我APICaller.service其确实使用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). 这就是为什么我创建了一个MockAPICaller但它似乎仍然需要Http (也许是因为它在APICaller的构造函数中,但我不知道如何修复它)。

So I suspect the problem is within MockAPICaller but I don't know for sure. 所以我怀疑问题是在MockAPICaller但我不确定。


I'll post MockAPICaller.service , APICaller.service , EventsPage and my events.spec.ts . 我将发布MockAPICaller.serviceAPICaller.serviceEventsPage和我的events.spec.ts (in that order so you could maybe skip along if you need/want to. (按此顺序,如果你需要/想要,你可以跳过。

MockAPICaller 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 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 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 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);

  });
});

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 . 通过这样,组件将尝试创建自己的APICaller实例。 This overrides any configurations you make in the TestBed (ie the mock). 这将覆盖您在TestBed任何配置(即模拟)。

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: 也可以看看:

Your problem here is that you forgot to mock the Http service. 你的问题是你忘了嘲笑Http服务。

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

But APICaller needs Http to get created, but you don't have any providers for Http, that's why you get this error. 但是APICaller需要Http来创建,但是你没有任何Http的提供者,这就是你得到这个错误的原因。

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: 现在,由于Http永远不应该用在测试中,你必须创建一个mock来提供自己的后端来发送自定义响应:

a good example provided by Peeskillet on Testing - Can't resolve all parameters for (ClassName) : Peeskillet在测试中提供的一个很好的例子- 无法解析(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 . 如您所见,它提供了一个自定义后端,向测试服务发送自定义响应,允许您在没有HttpModule情况下正确测试它。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM