IONIC Unit testing ready?

Hi there,

I’ve implemented the testing environment documented here and can get basic unit tests working. However when I’m wanting to test modals or anything more complicated, I get a whole bunch of errors. So I’m wondering is unit testing not ready in Ionic 2 or am I doing something wrong?

The main issue I’m having at the moment is when trying to test whether a modal is being called, I get errors along the lines of

Error: Error in ./LoginPage class LoginPage - inline template:8:110 caused by: Cannot read property 'present' of undefined

Even though I have mocked the ModalController. I’ve tried mocking practically everything (purely to find out where the issue is because the unit test with mocks for everything is practically useless) but I still get the error.

Here is the code:

import { TestBed, ComponentFixture, async } from '@angular/core/testing';
import {} from 'jasmine'
import { By } from '@angular/platform-browser';
import { DebugElement } from '@angular/core';
import { IonicModule, NavController, ToastController, LoadingController, ModalController, NavParams, ViewController } from 'ionic-angular';
import { Storage } from '@ionic/storage';
import { MyApp } from '../../app/app.component';
import { LoginPage, HostnamePage } from '../pages';
import { User } from '../../models/user';
import { UserService } from '../../providers/user.service';
import { NavParamsMock, StorageMock, UserServiceMock, ViewControllerMock, ModalControllerMock, ToastControllerMock, LoadingControllerMock } from '../../mocks'

let comp: LoginPage;
let fixture: ComponentFixture<LoginPage>;
let de: DebugElement;
let el: HTMLElement;
 
describe('Page: Login', () => {
 
    beforeEach(async(() => {
 
        TestBed.configureTestingModule({
 
            declarations: [MyApp, LoginPage, HostnamePage],
 
            providers: [
                NavController,
                {
                    provide: LoadingController,
                    useClass: LoadingControllerMock
                },
                {
                    provide: ModalController,
                    useClass: ModalControllerMock
                },
                {
                    provide: NavParams,
                    useClass: NavParamsMock
                },
                {
                    provide: Storage,
                    useClass: StorageMock
                },
                {
                    provide: UserService,
                    useClass: UserServiceMock
                },
                {
                    provide: ViewController,
                    useClass: ViewControllerMock
                },
                {
                    provide: ToastController,
                    useClass: ToastControllerMock
                }
            ],
 
            imports: [
                IonicModule.forRoot(MyApp)
            ]
 
        }).compileComponents();
 
    }));
 
    beforeEach(() => {
 
        fixture = TestBed.createComponent(LoginPage);
        comp    = fixture.componentInstance;
 
    });
 
    afterEach(() => {
        fixture.destroy();
        comp = null;
        de = null;
        el = null;
    });
 
    it('is created', () => {
 
        expect(fixture).toBeTruthy();
        expect(comp).toBeTruthy();

    });
 
    it('changeUrl function creates hostname modal', () => {
         fixture.detectChanges();
         comp.changeUrl();
         let de = fixture.debugElement.query(By.css('.sectionHeading'));
         expect(de).toBe('Change Hostname');
    });
 
});

I’ve also tried something like this:
it(‘changeUrl function creates hostname modal’, () => {
const element = fixture.debugElement.query(By.css("#changeURL"));
element.triggerEventHandler(‘click’, null);
}

From login.html:

        <div id="changeURL" text-right style="padding-top: 41px; word-wrap: normal; text-overflow: ellipsis;" (click)="changeUrl()">
      <span ion-text color="primary">{{hostname}}</span>
    </div>

From login.ts

   changeUrl(){
    let login = {
      userDefinedHostname: this.userDefinedHostname,
      username: this.username,
      password: this.password,
      buildIdentifier: this.buildIdentifier
    }
    let modal = this.modalCtrl.create(HostnamePage, {defaultHostname: this.defaultHostname, login: login});
    modal.present();
  }

From mocks.ts

export class ViewControllerMock {
  public _setHeader(): any { return {} };
  public _setNavbar(): any { return {} };
  public _setIONContent(): any { return {} };
  public _setIONContentRef(): any { return {} };
}

export class ModalControllerMock extends ViewControllerMock {
  public present(): any { return  };
  public create(param): any { return  };
}

Any help would be greatly appreciated. As it stands, I can’t really unit test anything of worth.

1 Like

Not ready, not friendly, not useful. Unfortunately, unit testing is apparently an after-thought in Ionic.

2 Likes

I would suggest not mocking Ionic itself, but rather importing IonicModule in your testbed.

Setting up unit testing in Ionic 2 definitely requires a bit of grunt work at the start, but I’ve found creating automated tests in Ionic 2 works well. Angular has been built with testing in mind.

I have pretty heavily documented the steps I’ve taken, in this tutorial series especially: https://www.joshmorony.com/test-driven-development-in-ionic-2-an-introduction-to-tdd/ which goes beyond simple 2+2=4 style unit tests.

1 Like

I really appreciate your work here, but I ended up having much better results allowing angular-cli to generate the project and then grafting the ionic things on to it than the reverse strategy.

2 Likes

I’ve read all your tutorials @joshmorony and they work on standalone code, but when I add certain ionic components it fails, such as the loading component etc. I unfortunately need to write tests for code that already exists so it’s not always easy to pinpoint what is failing but it always seem to be a problem with functions in ionic components not being found (even when mocked).

I’ve also recently upgraded to the final version on IONIC 2 and now not even the simple tests work :disappointed:

1 Like

Hey @rapropos, do you have any advice on how you would go about this? How do you integrate IONIC into a project built from the angular cli?

Rough outline:

  • let angular-cli build the project with ng new (I used --ng4, but that may be still a bit too bleeding edge for some apps)
  • graft the build/serve entries from the old ionic-generated project’s package.json into the new one, same goes for all Ionic-related dependencies
  • run npm i. If you used --ng4, you will get a bunch of peer dependency warnings. I’m ignoring them for now.
  • remove the “src/environment” directory, for it won’t compile under TypeScript <2.1.
  • bring config.xml and ionic.config.json over from the old project
  • keep polyfills.js, tsconfig.json, and tsconfig.spec.json, but blast away pretty much everything else under src and replace it with the source of your Ionic app

Now we can make an app component instantiation test in app.component.spec.ts next to the app component. Something like this:

import {MyApp} from "./app.component";
import {async, ComponentFixture, TestBed} from "@angular/core/testing";
import {IonicModule} from "ionic-angular";

describe('App: MyApp', () => {
  let app: MyApp;
  let fixture: ComponentFixture<MyApp>;

  beforeEach(() => {
    TestBed.configureTestingModule({
      imports: [IonicModule.forRoot(MyApp)],
      declarations: [MyApp],
    });
    fixture = TestBed.createComponent(MyApp);
    app = fixture.debugElement.componentInstance;
    fixture.detectChanges();
  });

  it('should instantiate', async(() => {
    expect(app).toBeTruthy();
  }));
});

Apologies for typos in that ^^^, it’s adapted by hand from a more elaborate real-world example, but that project is able to run angular-cli’s karma/jasmine tests and still build via npm run ionic:build. It’s still a bit of a WIP, because I haven’t deployed an APK out of it, but it looks a lot more promising than it did when I embarked on this journey a couple of weeks ago.

I haven’t tried integrating a loading component into a test before, but I haven’t run into any issues with any other Ionic components yet - this is also on the final version of Ionic 2 so perhaps there’s an issue in your setup?

In any case, I would just mock/stub the LoadingController anyway, i.e:

    it('should dismiss loading overlay', () => {
 
		let loadingCtrl = fixture.debugElement.injector.get(LoadingController);
        spyOn(loadingCtrl, 'dismiss');

        expect(loadingCtrl.dismiss).toHaveBeenCalled();
 
    });

From a unit testing perspective, there’s no need to include the actual functionality of the LoadingController in your test. Unit tests should be isolated and not rely on other code (i.e. we want to test your unit, not Ionic’s loading controller, so we just assume it to work and use a test double instead).

EDIT:

In the above, you would also use a mock or stub when injecting the provider, e.g:

{
  provide: LoadingController,
  useClass: LoadingControllerStub
}

Hi Josh,

I started a whole new project and set up unit testing as per your tutorials. On one of the pages I added a button that displays a loader and dismisses it after 5 seconds.

  showLoader(){
    let loading = this.loadingCtrl.create({
    content: 'Please wait...'
  });

  loading.present();

  setTimeout(() => {
    loading.dismiss();
    }, 5000);
  }

I created a LoadingController mock,

export class LoadingControllerMock{
  public present(): any { return  };
  public create(param): any { return  };
  public dismiss(): any { return  };
}

added it to test bed, and tried this test:

it('button invokes loader', () => {
    let loadingCtrl = fixture.debugElement.injector.get(LoadingController);
    spyOn(loadingCtrl, 'present');
    comp.showLoader();
    expect(loadingCtrl.present).toHaveBeenCalled();
});

It fails with the following error “Cannot read property ‘present’ of undefined”. The same original issue.

Have you set up your mock in the TestBed configuration, e.g:

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

Yes I have done that.

1 Like

Thanks for this @rapropos. Hopefully Ionic develop some easier and more efficient testing in the near future so we don’t need to go to these lengths. I’m seeing what @joshmorony comes back with after my basic test that fails when a loader is used.

I’m actually working on this right now, and I’ve decided to separate out the mock LoadingController from the mock LoadingComponent. Your client code is probably expecting the controller to return it a component out of create, but it doesn’t look like you’re doing so, so it will choke when it tries to call present on the created component. Here’s what I’m using at the moment:

export class LoadingComponentMock {
  free: boolean = true;
  open: boolean;

  present(): void {
    if (this.free) {
      console.error("attempt to present freed loading component");
    }
    if (this.open) {
      console.error("loading component presented twice");
    }
    this.open = true;
  }
  
  dismiss(): void {
    if (!this.open) {
      console.error("double-dismiss on loading component");
    }
    
    if (this.free) {
      console.error("attempt to dismiss unallocated loading component");
    }
    
    this.open = false;
    this.free = true;
  }
}

export class LoadingControllerMock {
  component: LoadingComponentMock = new LoadingComponentMock();

  create(): LoadingComponentMock {
    if (!this.component.free) {
      console.error("can't have two loading components out at once");
      return null;
    }

    this.component.free = false;
    return this.component;
  }
}
1 Like

Hey @rapropos,

I incorporated your code but found I still needed to keep the present function in LoadingControllerMock or I got the same error. When that’s added, it finds the present function but fails the test, when it should pass.

 Expected spy present to have been called.
        at Object.<anonymous> (webpack:///src/pages/home/home.spec.ts:57:36 <- src/test.ts:105394:37)
        at ZoneDelegate.invoke (webpack:///~/zone.js/dist/zone.js:232:0 <- src/test.ts:116938:26)
        at ProxyZoneSpec.onInvoke (webpack:///~/zone.js/dist/proxy.js:79:0 <- src/test.ts:93188:39)
        at ZoneDelegate.invoke (webpack:///~/zone.js/dist/zone.js:231:0 <- src/test.ts:116937:32)

I can’t be certain, but one theory that might fit with what you are saying is that you’re spying on the wrong thing: present on the controller, not the component. Instead of spying on those methods, what I do is:

let loadings: LoadingControllerMock = TestBed.get(LoadingController);
magicallyFreezeTime();
doSomethingThatShouldPopLoading();
tick();
expect(loadings.component.open).toBe(true);
unfreezeTime();
tick();
expect(loadings.component.open).toBe(false);

Obviously, if you prefer, you can spy on loadings.component.present and dismiss, but it doesn’t make sense to me to spy on those calls to the controller, because the real controller doesn’t have those functions at all.

Ahhh I get you. I changed my test to:

it('button invokes loader', () => {
    let loadingCtrl: LoadingControllerMock = TestBed.get(LoadingController);
    spyOn(loadingCtrl.component, 'present');
    comp.showLoader();
    expect(loadingCtrl.component.present).toHaveBeenCalled();
});

and that works as expected. :+1:

I understand why it might not make sense to do it that way. As you said the controller itself doesn’t have those methods, but the way the controller is used in ionic implies that it does. I didn’t really think of it as creating a component, though it makes sense that that is what is happening. The official documentation doesn’t say it returns a component just a Loading instance. So without digging into core code, one would expect you should be able to test present from the controller because that’s how it’s called and used in the component it contains. That’s how my brain works anyway :stuck_out_tongue:

Thanks a lot for your help :slight_smile: I’m new to testing Ionic and Angular so I may ask a lot of silly questions, but I know from experience, a whole bunch of other people will be thinking similar things :wink:

The terminology can be a source of confusion, but the crucial distinction is that the thingy that LoadingControllers return from create are different from the LoadingController itself, so trying to call present on a LoadingController not only won’t test properly, it won’t work in reality (and your IDE and build process should yell at you for even trying to do that).

Now if you wanted to, you could of course meld the LoadingComponentMock together with the LoadingControllerMock and have create simply return this. I think having them separate makes the code more readable and in turn would catch the bug we’re talking about here (where somebody tries to call present on the controller rather than the component), whereas a FrankenloaderControllerComponent would not.

Anyway, glad you’re making progress and were able to reuse some of my head-banging in this arena.

P.S. EDIT: Adding to the confusion is that in earlier development releases, ISTR LoadingController used to actually have a present method.

The terminology did slip me up a bit, and the updates changing where functions were probably didn’t help haha.

It makes a lot more sense now that you’ve explained it and will probably help me nut out some of the other issues I’ve been having.

I’ll keep the mocks separate though as I agree, it’s easier to read and should emulate what how the core is structured.

I agree. We are using Ionic, but not having tests is forcing us to move to a different framework in our next projects. Or invest time in adding the tests for ourself. Not sure if the effort compensate the benefits of Ionic2.