IONIC Unit testing ready?

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.

I did this :

let service = fixture.debugElement.injector.get(Service);

but I get the original service not the mock :confounded:

O my god, this is really a pain.

Creating stubs and mocks on any part of code result just too much confuse. I got 2 days to get a test to work just cuz the hardcore that this is on ionic. :frowning:

Could you please share your the mocks that works for this test, i’m having the same problem to test a modal launching :slight_smile:
Thank you

Hey there,

I’m just using what @rapropos posted.

@lisatassone Check out this repo: https://github.com/lathonez/clicker.

Been using it for the past 6 months. Very helpful starter project to use with a good community on testing.

Hi lisatassone,

I am also new to testing.working on Ionic 3 + Angular 4. Could you please help.
The scenario which needs to be tested is exactly same as you mentioned here but am getting error when I use “.component” in locadingCtrl.component statement. Do we need to set component as soemthing ?