Introduction to Testing Ionic Applications with TestBed

Introduction to Testing Ionic Applications with TestBed

Follow Josh Morony on

Creating automated tests for your application is a great way to improve the quality of your code, protect against code breaking regressions when implementing new features, and speed up testing and debugging.

In a JavaScript environment, the Jasmine framework is often used for writing tests and Karma is used to run them. This is still the case for testing Ionic/Angular applications, but Angular has introduced the concept of the TestBed. If you are using Angular to build your Ionic applications, you can also make use of TestBed to help create your tests.

The general concept is that TestBed allows you to set up an independent module, just like the @NgModule that lives in the app.module.ts file, for testing a specific component. This is an isolated testing environment for the component you are testing. A unit test focuses on testing one chunk of code in isolation, not how it integrates with any other parts of the code (there are different tests for this), so it makes sense to create an environment where we get rid of any outside influences.

Before we go any further, here’s a look at what an implementation using TestBed looks like:

import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { async, ComponentFixture, TestBed } from '@angular/core/testing';

import { HomePage } from './home.page';

describe('HomePage', () => {
  let component: HomePage;
  let fixture: ComponentFixture<HomePage>;

  beforeEach(async(() => {
    TestBed.configureTestingModule({
      declarations: [ HomePage ],
      schemas: [CUSTOM_ELEMENTS_SCHEMA],
    }).compileComponents();
  }));

  beforeEach(() => {
    fixture = TestBed.createComponent(HomePage);
    component = fixture.componentInstance;
    fixture.detectChanges();
  });

  afterEach(() => {
    fixture.destroy();
    component = null;
  })

  it('should create', () => {
    expect(component).toBeTruthy();
  });
});

We are going to walk through building this later, but I want you to have some idea of what using TestBed looks like right away.

You don’t always need to use TestBed when creating unit tests, you can test your providers and services without it - in fact, that is the recommended way. If you want more of an introduction to Jasmine and Karma themselves (which we won’t be covering in this blog post), you should take a look at How to Unit Test an Ionic Application which also covers how to create a simple test for a service without TestBed.

I’m going to quickly cover some important points to understand when working with TestBed, but this guide goes into a lot more detail. I would highly recommend reading through at least the beginning sections when you have time.

  • The beforeEach sections will run before each of the tests are executed (each it block is a test). The afterEach will run after the test has completed.
  • We can use configureTestingModule to set up the testing environment for the component, including any imports and providers that are required.
  • We can use createComponent to create an instance of the component we want to test after configuring it
  • We need to use compileComponents when we need to asynchronously compile a component, such as one that has an external template (one that is loaded through templateUrl and isn’t inlined with template). This is why the beforeEach block that this code runs in uses async - it sets up an asynchronous test zone for the compileComponents to run inside.
  • In order to wait for compileComponents to finish (so we can set up references to the component we created), we can either chain a then() onto it and then call createComponent from inside of there, or we can just create a second beforeEach which will wait until the component has finished compiling before running.
  • A Component Fixture is a handle on the testing environment that was created for the component, compared to the componentInstance which is a reference to the component itself (and is what we use for testing).
  • Change detection, like when you change a variable with two-way data binding set up, is not automatic when using TestBed. You need to manually call fixture.detectChanges() when testing changes. If you would prefer, there is a way to set up automatic change detection for tests.

You don’t need to understand all of the above concepts right away, I just want them to be in your mind as we walk through some examples.

In this tutorial, we are going to set up a testing environment that allows us to make use of TestBed when testing an Ionic/Angular application. We will also set up some simple real-world scenario tests to test our root component and the home page of the application.

Before We Get Started

Last updated for Ionic/Angular 4.7.1

Before you go through this tutorial, you should have a reasonably strong understanding of Ionic/Angular concepts. If you need more introductory content for Ionic, I would recommend taking a look at my beginner tutorials.

1. Generate a New Ionic/Angular Application

We are going to generate a new blank Ionic application to test. Our testing is going to be very simple in this example, so we won’t need to generate any additional pages or providers.

Run the following command to generate a new Ionic application:

ionic start ionic-angular-testbed blank --type=angular

Once the application has finished generating, make it your working directory by running the following command:

cd ionic-angular-testbed

2. Create a Test for the Root Component

We are going to create a test for the root component, so to do that we will modify the app.component.spec.ts file.

NOTE: When this tutorial was originally written testing was not set up by default, but now it is. This means that you will find that there are already some tests in the file we are about to edit. So that we can follow along with the example, I would recommend replacing the existing code with the code we are about to add - but before you do that, have a bit of a look around at the code that is there. We will be implementing some simpler tests, but it is a good opportunity to learn from the default code that is there.

Modify src/app/app.component.spec.ts to reflect the following:

import { CUSTOM_ELEMENTS_SCHEMA } from "@angular/core";
import { TestBed, ComponentFixture, async } from "@angular/core/testing";

import { Platform } from "@ionic/angular";
import { SplashScreen } from "@ionic-native/splash-screen/ngx";
import { StatusBar } from "@ionic-native/status-bar/ngx";

import { AppComponent } from "./app.component";

describe("Component: Root Component", () => {
  let comp: AppComponent;
  let fixture: ComponentFixture<AppComponent>;
  let statusBarSpy, splashScreenSpy, platformReadySpy, platformSpy;

  beforeEach(async(() => {
    // Configure spies (these replace/mock the actual implementations)
    statusBarSpy = jasmine.createSpyObj("StatusBar", ["styleDefault"]);
    splashScreenSpy = jasmine.createSpyObj("SplashScreen", ["hide"]);
    platformReadySpy = Promise.resolve();
    platformSpy = jasmine.createSpyObj("Platform", { ready: platformReadySpy });

    TestBed.configureTestingModule({
      declarations: [AppComponent],
      schemas: [CUSTOM_ELEMENTS_SCHEMA],
      providers: [
        { provide: StatusBar, useValue: statusBarSpy },
        { provide: SplashScreen, useValue: splashScreenSpy },
        { provide: Platform, useValue: platformSpy }
      ]
    }).compileComponents();
  }));

  beforeEach(() => {
    fixture = TestBed.createComponent(AppComponent);
    comp = fixture.componentInstance;
  });

  afterEach(() => {
    fixture.destroy();
    comp = null;
  });

  it("is created", () => {
    expect(fixture).toBeTruthy();
    expect(comp).toBeTruthy();
  });
});

We first describe our test suite giving it a title of Component: Root Component. Inside of that test suite, we add our beforeEach blocks. The first one is what configures TestBed for us with the appropriate dependencies required to run the component.

Since our app root component injects the SplashScreen, StatusBar, and Platform, we need to provide implementations of those (otherwise the testing module won’t be able to compile). We don’t want to use the real implementations of these services because we want our unit tests to be isolated to just the functionality of the app root component - we don’t want anything else interfering with our tests. If the SplashScreen functionality is broken, we don’t care about that here, and it shouldn’t have an impact on this test. To deal with this situation, we use jasmine.createSpyObj to create “fakes” or “mocks” of these services. This replaces the real functionality with a basic object that simulates the real service. The benefit of using spies like this is that we can also check in our tests how those services have been interacted with (e.g. did our root component make a call to SplashScreen.hide()?).

It is also worth noting that this beforeEach is using an async parameter which will allow us to use the asynchronous compileComponents method.

The second beforeEach will trigger once the TestBed configuration has finished. We use this block to create a reference to our fixture (which references the testing environment TestBed creates) and a reference to the actual component to be tested, which we store as comp. Then in the afterEach we clear all of these references.

As I mentioned before, beforeEach will run before every test and afterEach will run after every test, so since we have two tests in this file, the process would look something like this:

  1. Create TestBed testing environment
  2. Set up references
  3. Run is created test
  4. Clean up references
  5. Create TestBed testing environment
  6. Set up references
  7. Run the next test (if it exists)
  8. Clean up references

Our test here is quite simple. The first test we have created just makes sure that the fixture and comp have been created successfully. If you were to run npm test now, you should see a result like this:

TOTAL: 2 SUCCESS

3. Create a Test for the Home Page

We’re going to set up a separate test for the Home Page component now. It is going to be very similar, but we are going to do a slightly more complex test.

Modify src/app/home/home.page.spec.ts to reflect the following

import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { DebugElement } from '@angular/core';
import { IonicModule } from '@ionic/angular';

import { HomePage } from './home.page';

describe('HomePage', () => {

  let comp: HomePage;
  let fixture: ComponentFixture<HomePage>;
  let de: DebugElement;
  let el: HTMLElement;

  beforeEach(async(() => {
    TestBed.configureTestingModule({
      declarations: [ HomePage ],
      imports: [IonicModule.forRoot()]
    }).compileComponents();

    fixture = TestBed.createComponent(HomePage);
    comp = fixture.componentInstance;
    fixture.detectChanges();
  }));

  beforeEach(() => {
    fixture = TestBed.createComponent(HomePage);
    comp = fixture.componentInstance;
  });

  afterEach(() => {
    fixture.destroy();
    comp = null;
    de = null;
    el = null;
  });

  it('should create', () => {
    expect(comp).toBeTruthy();
  });

  it('initialises with a title of My Page', () => {
    expect(comp['title']).toEqual('My Page');
  });

  it('can set the title to a supplied value', () => {
    de = fixture.debugElement.query(By.css('ion-title'));
    el = de.nativeElement;  

    comp.changeTitle('Your Page');
    fixture.detectChanges();
    expect(comp['title']).toEqual('Your Page');
    expect(el.textContent).toContain('Your Page');
  });

});

Much of this test is the same, except that we are also setting up references for a DebugElement and a HTMLElement, and the third test is a little bit more complex.

This page is going to provide the ability to change the title of the page through a function, and we want to test that it works. To do that, we want to check two things when this function is called:

  1. That the title member variable gets changed to the appropriate value
  2. That the interpolation set up to render the title inside of the <ion-title> tag updates in the DOM correctly

To do that, we grab a reference to the DOM element by querying the fixture using the CSS selector ion-title. We then call the changeTitle method on the component with a test value, and call detectChanges() to trigger change detection. Remember that by default change detection is not automatic when using TestBed, so if we did not manually trigger change detection here then the test would never run successfully.

To test that the component is now in the correct state, we check that the title member variable has been updated to Your Page and we also check that the content of <ion-title> in the DOM has been updated to reflect Your Page. Just because the member variable has been updated it doesn’t necessarily mean that the title would display correctly in the template, perhaps we could have spelled the binding wrong in the template, so it is important to check the element itself for this test to be accurate.

If you run npm test now the second two tests should fail because we haven’t actually implemented that functionality yet:

ERROR in src/app/home/home.page.spec.ts:49:10 - error TS2339: Property 'changeTitle' does not exist on type 'HomePage'.
49     comp.changeTitle("Your Page");

Let’s fix that.

Modify src/app/home/home.page.ts to reflect the following:

import { Component } from "@angular/core";

@Component({
  selector: "app-home",
  templateUrl: "home.page.html",
  styleUrls: ["home.page.scss"]
})
export class HomePage {
  public title: string = "My Page";

  constructor() {}

  changeTitle(title) {
    this.title = title;
  }
}

We will also need to set up the interpolation for title in the template.

Modify src/app/home/home.page.html to reflect the following:

<ion-header>
  <ion-toolbar>
    <ion-title>
      {{ title }}
    </ion-title>
  </ion-toolbar>
</ion-header>

<ion-content>
  <div class="ion-padding">
    The world is your oyster.
    <p>
      If you get lost, the
      <a target="_blank" rel="noopener" href="https://ionicframework.com/docs/">docs</a> will be
      your guide.
    </p>
  </div>
</ion-content>

Now if you run the tests again using npm test you should see them pass:

TOTAL: 4 SUCCESS

Summary

This post only scratches the surface of using TestBed to test Ionic/Angular applications. There is a variety of different types of tests you will need to learn how to implement, but this should at least get you started on the right path. I will be following up this tutorial with many more in the future which will cover various aspects of testing.

If you want a more thorough introduction into creating automated tests for Ionic/Angular applications, I would recommend checking out my advanced Elite Ionic (Angular) course.

Check out my latest videos: