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 (eachit
block is a test). TheafterEach
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 throughtemplateUrl
and isn’t inlined withtemplate
). This is why thebeforeEach
block that this code runs in usesasync
- it sets up an asynchronous test zone for thecompileComponents
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 athen()
onto it and then callcreateComponent
from inside of there, or we can just create a secondbeforeEach
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 thecomponentInstance
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:
- Create TestBed testing environment
- Set up references
- Run
is created
test - Clean up references
- Create TestBed testing environment
- Set up references
- Run the next test (if it exists)
- 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:
- That the
title
member variable gets changed to the appropriate value - 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.