Search code examples
angulartypescriptrxjsjasmineangular-test

Angular Integration Testing - jasmine Spy - Can't mock a service method returning Observable


I am new to doing integration testing. And the whole thing is so confusing.

For my first test, Seems like my spying is not returning the data as i intend to return it. Gives and error: Expected 0 to be 3. Will be great if someone could help me understand what am I doing wrong.

Here is my service,page,spec file along with template:

MyService


    import { Data } from './../data/data.model';
    import { Injectable } from '@angular/core';
    import { BehaviorSubject, of } from 'rxjs';
    import { tap } from 'rxjs/operators';

    @Injectable({
      providedIn: 'root',
    })
    export class MyService {
      private _data = new BehaviorSubject<Data[]>([]);

      get data() {
        return this._data;
      }

      constructor() {}

      getAllData() {
        return of([
          {
            id: '1',
            title: 'Rice',
          },
          {
            id: '2',
            title: 'Wheat',
          },
          {
            id: '33',
            title: 'Water',
          },
        ]).pipe(
          tap((data) => {
            this._data.next(data);
          })
        );
      }
    }

DataPage Component


    import { Component, OnInit } from '@angular/core';
    import { BehaviorSubject, Observable, of, Subscription } from 'rxjs';
    import { MyService } from '../services/my.service';
    import { Data } from './data.model';

    @Component({
      selector: 'app-data',
      templateUrl: './data.page.html',
      styleUrls: ['./data.page.scss'],
    })
    export class DataPage implements OnInit {
      allData: Data[];
      dataServiceSub: Subscription;
      isLoading: boolean;

      constructor(private myService: MyService) {}

      ngOnInit() {
        this.dataServiceSub = this.myService.data.subscribe(
          (data) => {
            console.log(data);
            this.allData = data;
          }
        );
      }

      ngOnDestroy() {
        if (this.dataServiceSub) {
          console.log('ngOnDestroy');
          this.dataServiceSub.unsubscribe();
        }
      }

      ionViewWillEnter() {
        this.isLoading = true;
        this.myService.getAllData().subscribe(() => {
          console.log('ionViewWillEnter');
          this.isLoading = false;
        });
      }
    }

DataPage.spec

    import { MyService } from '../services/my.service';
    import { async, ComponentFixture, TestBed } from '@angular/core/testing';
    import { IonicModule } from '@ionic/angular';

    import { DataPage } from './data.page';
    import { of } from 'rxjs';

    describe('DataPage', () => {
      let component: DataPage;
      let fixture: ComponentFixture<DataPage>;
      let serviceSpy: jasmine.SpyObj<MyService>;

      beforeEach(async(() => {
        TestBed.configureTestingModule({
          declarations: [DataPage],
          providers: [
            {
              provide: MyService,
              useClass: MyService
            },
          ],
          imports: [IonicModule.forRoot()],
        }).compileComponents();

        fixture = TestBed.createComponent(DataPage);
        component = fixture.componentInstance;
        fixture.detectChanges();
      }));

      fit('Should show list of data if data is available', () => {
        serviceSpy = TestBed.get(MyService);
        spyOn(serviceSpy, 'getAllData').and.returnValue(of([
          {
            id: '1',
            title: 'Rice',
          },
          {
            id: '2',
            title: 'Wheat',
          },
          {
            id: '33',
            title: 'Water',
          },
        ]));
        fixture.detectChanges();
        const element = fixture.nativeElement.querySelectorAll(
          '[test-tag="dataList"] ion-item'
        );
        console.log(
          fixture.nativeElement.querySelectorAll('[test-tag="dataList"]')
        );
        expect(element.length).toBe(3);
      });
    });

HTML


    <ion-content>
      <div test-tag="empty" class="ion-text-center">
        <ion-text color="danger">
          <h1>No data</h1>
        </ion-text>
      </div>
      <div test-tag="dataList">
        <ion-list>
          <ion-item *ngFor="let data of allData">
            <ion-label test-tag="title">{{data.title}}</ion-label>
          </ion-item>
        </ion-list>
      </div>
    </ion-content>



Solution

  • Ok, so here is the problem:

    You need to call ionViewWillEnter() to set the value of this.allData'

    Reason: Because you are having empty value while creating BehaviorSubject. And to emit value using data (this._data.next(data)) , you need to call getAllData().

     import { MyService } from '../services/my.service';
        import { async, ComponentFixture, TestBed } from '@angular/core/testing';
        import { IonicModule } from '@ionic/angular';
    
        import { DataPage } from './data.page';
        import { of } from 'rxjs';
    
        describe('DataPage', () => {
          let component: DataPage;
          let fixture: ComponentFixture<DataPage>;
          let serviceSpy: jasmine.SpyObj<MyService>;
    
          beforeEach(async(() => {
            TestBed.configureTestingModule({
              declarations: [DataPage],
              providers: [ MyService ],
              imports: [IonicModule.forRoot()],
            }).compileComponents();
    
            fixture = TestBed.createComponent(DataPage);
            component = fixture.componentInstance;
            fixture.detectChanges();
          }));
    
          fit('Should show list of data if data is available', () => {
            component.ionViewWillEnter(); // or create an event which will trigger ionViewWillEnter()
            fixture.detectChanges();
            const element = fixture.nativeElement.querySelectorAll(
              '[test-tag="dataList"] ion-item'
            );
            console.log(
              fixture.nativeElement.querySelectorAll('[test-tag="dataList"]')
            );
            expect(element.length).toBe(3);
          });
        });
    

    Please note that there are few changes that I have done:

    1. Removed UseClass (because you were not using it as it should)
    2. Removed spy (because you already have a hardcoded value in the original service)

    For your better understanding of testing in angular, you can refer my article which has also shown the use of useClass for your reference.


    On a side note: Try to use asObservable(), and also follow the convention of using $ when creating an Observable (this.data$.asObservable()). That is not a compulsion but accepted practice in the JS community.

    get data() {
      return this._data.asObservable();
    }