2

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>


Shashank Vivek
  • 16,888
  • 8
  • 62
  • 104
RBz
  • 896
  • 3
  • 17
  • 34

2 Answers2

3

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();
}
Shashank Vivek
  • 16,888
  • 8
  • 62
  • 104
  • This works. I was under the assumption we shouldn't be calling the lifecycle hooks ourselves and detectchanges will be making the framework do it. Doesn't this beats the purpose of testing? – RBz Jan 24 '21 at 22:48
  • @RBz: Gald to hear that. Can you please mark it as an answer so that it can help others as well. :) – Shashank Vivek Jan 25 '21 at 04:47
  • Done! Could you share your thoughts on manually calling a lifecycle hook in test? Is it a good practice? – RBz Jan 25 '21 at 06:10
  • @RBz: It can be done depending on the testing scenarios. But i would like to highlight that `fixture.detectChanges` is different than calling `ngOnInit`. Also, in the provided example, I have not called lifecycle hook. The `detectChanges` was called to let the angular know that a certain value has changed and it should accordingly refresh the template. This helped you to `const element` in the `it` block – Shashank Vivek Jan 25 '21 at 08:11
1

To avoid pain with observables, I would recommend to use a mocking library like ng-mocks, and to check its article "How to mock observable streams in Angular tests" https://ng-mocks.sudo.eu/extra/mock-observables.

In your case the test might look like that:

describe('DataPage', () => {
  // mocks everything except DataPage
  beforeEach(() => {
    return MockBuilder(DataPage)
      .mock(IonicModule.forRoot())
      .mock(MyService);
  });

  // We need to stub it because of subscription in ionViewWillEnter.
  // in a mock service, ionViewWillEnter does not return anything.
  // But we need to tell it to return an empty observable stream
  // to avoid errors like cannot call .subscribe on undefined.
  // This line can be removed along with the debugging from the
  // component.
  beforeEach(() => MockInstance(MyService, 'getAllData', () => EMPTY));

  it('Should show list of data if data is available', () => {
    // spies the getter of the property the component uses.
    MockInstance(MyService, 'data', jasmine.createSpy(), 'get')
      .and.returnValue(of([
        {
          id: '1',
          title: 'Rice',
        },
        {
          id: '2',
          title: 'Wheat',
        },
        {
          id: '33',
          title: 'Water',
        },
      ]));

    // render (already with detected changes)
    const fixture = MockRender(DataPage);

    // assertions
    const element = fixture.nativeElement.querySelectorAll(
      '[test-tag="dataList"] ion-item'
    );
    console.log(
      fixture.nativeElement.querySelectorAll('[test-tag="dataList"]')
    );
    expect(element.length).toBe(3);
  });
});
satanTime
  • 12,631
  • 1
  • 25
  • 73