Search code examples
angularunit-testingjasmineangular-unit-test

Why am I obtaining this error on my Jasmine unit test?


I am trying to implement an Angular Unit Test using Jasmine for the first time following some examp,e and I am finding some problem.

So I have this PeopleListComponent class implmenting the logic of a compontent:

    import { Component, OnInit, ElementRef, ViewChild } from '@angular/core';
    import { EventService } from '../event.service';
    import interactionPlugin, { Draggable } from '@fullcalendar/interaction';

    interface WorkShiftTypes {
      name: string;
      value: string;
    }

    @Component({
      selector: 'app-people-list',
      templateUrl: './people-list.component.html',
      styleUrls: ['./people-list.component.css']
    })
    export class PeopleListComponent implements OnInit {

      people: any[];

      //cities: City[];

      workShiftTypes: WorkShiftTypes[];
      selectedShift: WorkShiftTypes;

      @ViewChild('draggable_people') draggablePeopleExternalElement: ElementRef;

      constructor(private eventService: EventService) { }

      ngOnInit(): void {
        this.eventService.getPeople().then(people => {this.people = people;});

        this.selectedShift = {name: 'Mattina', value: 'Mattina'};

        this.workShiftTypes = [
          {name: 'Mattina', value: 'Mattina'},
          {name: 'Pomeriggio', value: 'Pomeriggio'},
          {name: 'Notte', value: 'Notte'},
          {name: 'Custom', value: 'Custom'}
        ];
      }

      ngAfterViewInit() {
        console.log("PEOPLE LIST ngAfterViewInit() START !!!")
        var self = this

        new Draggable(this.draggablePeopleExternalElement.nativeElement, {
          itemSelector: '.fc-event',
          eventData: function(eventEl) {
            console.log("DRAG !!!");
            //console.log("SELECTED SHIFT: " + self.selectedShift.value);
            return {
              title: eventEl.innerText,
              startTime: "17:00",
              duration: { hours: 8 }
            };
          }
        });

      }

      createEventObject() {
        return 1;
      }

    }

As you can see it contains this very simple createEventObject() method, that at the moment only return 1 (I want to take all as simple as possible). My first unit test has to test this method simply checking that the returned value is 1.

As you can see the previous method takes an EventService service instance injected in the constructor.

This is the EventSerive class code:

    import { Injectable } from '@angular/core';
    //import { Http } from '@angular/http';
    import { HttpClientModule, HttpClient } from '@angular/common/http'

    @Injectable()
    export class EventService {

      private events = [
        {id: 1, title: 'All Day Event', start: '2017-02-01'},
        {id: 2, title: 'Long Event', start: '2017-02-07', end: '2017-02-10'},
        {id: 3, title: 'Repeating Event', start: '2017-02-09T16:00:00'},
      ];

      private people = [
        {id: 1, name: "PERSONA 1"},
        {id: 2, name: "PERSONA 2"},
        {id: 3, name: "PERSONA 3"},
        {id: 4, name: "PERSONA 4"},
        {id: 5, name: "PERSONA 5"},
      ]


      constructor(private http: HttpClient) {}

      /*
      getEvents(): Promise<any[]> {
        return this.http.get('assets/json_mock/calendarevents.json')
          .toPromise()
          .then(res => JSON.parse(JSON.stringify(res)).data)
          .then(res => {
            console.log(res);
            // you returned no value here!
            return res;
          })
      }
      */

    getEvents(): Promise<any[]> {
      return Promise.all(this.events)
      .then(res => {
        console.log(res);
        // you returned no value here!
        return res;
      })
    }

    addEvent(event) {
      //this.events.push(event);
      //console.log("EVENT:")
      //console.log(event.event.title);
      console.log(event.event.start);
      console.log(event);
      const newEvent = {id: 5, title: event.event.title, start: event.event.start, end: event.event.end};
      this.events.push(newEvent);

      console.log(this.events);

    }

      getPeople(): Promise<any[]> {
        return Promise.all(this.people)
            .then(res => {
              console.log(res);
              return res;
            })
      }

    }

As you can see this method itself take another object injected into the constructor (the HttpClient to perform HTTP request).

Ok, so now I want to implement the createEventObject() method test into my unit test. So I have this people-list.component.spec.ts file:

    import {async, ComponentFixture, TestBed} from '@angular/core/testing';
    import { PeopleListComponent } from "./people-list.component"
    import { EventService } from '../event.service';
    import {HttpClientTestingModule} from '@angular/common/http/testing';

    describe('people-list', () => {
        let component: PeopleListComponent;
        let fixture: ComponentFixture<PeopleListComponent>;
        let eventServiceSpy : jasmine.SpyObj<EventService>;
            beforeEach(async(() => {
              const eventServiceSpyObj = jasmine.createSpyObj('EventService',['getPeople'])

              TestBed.configureTestingModule({
                  declarations: [PeopleListComponent],
                  imports: [HttpClientTestingModule],
                  providers : [{ provide : EventService, useValue : eventServiceSpyObj }]

            });

            // Create a testing version of my PeopleListComponent:
            fixture = TestBed.createComponent(PeopleListComponent);
            eventServiceSpy = TestBed.inject(EventService);
            component = fixture.componentInstance;
            fixture.detectChanges();
        }));

        it('createEventObject()  return 1', () => {
            expect(component.createEventObject()).toBe(1)
        })
    })

I am absolutely not sure about if it is logically correct...

The problem is that at the moment the IDE give me an error on this line:

eventServiceSpy = TestBed.inject(EventService);

The error is:

Type 'EventService' is not assignable to type 'SpyObj<EventService>'.
  Type 'EventService' is not assignable to type '{ getEvents: (() => Promise<any[]>) & Spy<() => Promise<any[]>>; addEvent: ((event: any) => void) & Spy<(event: any) => void>; getPeople: (() => Promise<any[]>) & Spy<...>; }'.
    Types of property 'getEvents' are incompatible.
      Type '() => Promise<any[]>' is not assignable to type '(() => Promise<any[]>) & Spy<() => Promise<any[]>>'.
        Type '() => Promise<any[]>' is missing the following properties from type 'Spy<() => Promise<any[]>>': and, calls, withArgsts(2322)

What is wrong? How can I correctly test my component method?


Solution

  • The TypeScript tools are not that smart.

    let eventServiceSpy : jasmine.SpyObj<EventService>;
    eventServiceSpy = TestBed.inject(EventService);
    

    eventServiceSpy is of type jasmine.SpyObj<EventService>;. According to typing the method TestBed.inject(EventService); is expected to return object of type EventService while with Angular DI it is possible to provide any object for any injection token (in this case the injection token is class EventService). Above you provided the object of type jasmine.SpyObj<EventService>:

    providers : [{ provide : EventService, useValue : eventServiceSpyObj }
    

    So now the method TestBed.inject(EventService); would provide object of type jasmine.SpyObj<EventService>. But development tools are not that smart enough so far to handle typing here properly. A workaround would be casting:

    eventServiceSpy = <jasmine.SpyObj<EventService>><any>TestBed.inject(EventService);