Testing in Ionic 2

Test Driven Development in Ionic 2: An Introduction to TDD



·

The concept of creating automated tests for your application allows you to improve the quality of your code, protect against regressions when making updates, and speed up the testing and debugging process.

There are many different types of tests you can add to your applications, including:

  • Unit tests which test small isolated chunks of code
  • Integration tests which test integrations between various components
  • End-to-end tests which test the flow of an application from start to finish with realistic scenarios

and there are many different approaches to creating these tests. One common approach is to add tests after the code has already been created, to ensure that it is doing what it is supposed to do. Another, and perhaps better, approach is to create tests before writing any code. The general process involves writing a test that describes what the code should do (which will fail because the code does not exist yet), then writing code to satisfy that test until it passes. This is referred to as Test Driven Development or Behaviour Driven Development (we will briefly discuss the difference later).

Some benefits to a Test Driven Development approach are:

  • Tests can’t be skipped
  • Test coverage is high since a test needs to be written before code is created
  • The tests document the requirements of the application
  • It forces you to plan what your code will do before writing it
  • Issues can be identified and resolved during the development of the application
  • It is easier to identify what tests need to be written

I like the idea of Test Driven Development, but I struggled for a while to come up with an approach that made sense to me… What should my first test be? What kind of tests do I write? How many tests do I write before I start building the app? Adam Wathan’s content helped me a lot with this, as did this video by Misko Hevery. The content I just linked is not specific to Ionic or even to Angular (in fact, Adam’s content is based on the Laravel framework for PHP), but the general testing concepts can be applied just about anywhere – they might just look a little different.

In this tutorial series, we will be walking through some examples of using this style of Test Driven Development. I’ve been able to make a lot of things related to testing “click” in my brain now, but there’s still so much I have to learn about testing, so this series is as much for me as it is for you.

Testing is one of those things where there isn’t really a “one true way” to do things, which makes it a bit more complicated to wrap your head around. There are certainly some guidelines and best practices around what to test and how to test it, but there are different schools of thought and not everybody agrees on what these best practices are. If you have an opinion on the way I am structuring the tests in these tutorials, I would encourage you to share it in the comments (constructively, of course).

If you are completely new to testing, my advice to you would be to just do it and don’t worry about whether your tests follow best practices or not – this will come in time. Your tests are separate to your application, so they aren’t going to break anything, and having any kind of testing is better than having none at all. But also keep in mind that just because you have passing tests, it doesn’t mean that your code is working. The better you get at testing, the more confident you can be that it is.

Before we get into building a real application in Ionic 2 using Test Driven Development, we are going to cover a few basic concepts first.

An Introduction to Unit Testing

Before starting with this series, you should at least understand the basic concept of how to write unit tests for Angular 2 or Ionic 2 applications. If you need a bit a primer before continuing, you can read some of my previous tutorials:

Once you have read through both of those, and preferably give the examples a go yourself, you should be fine to continue on.

Test Driven Development vs Behaviour Driven Development

This series will not strictly be about Test Driven Development (TDD) over Behaviour Driven Development (BDD), we will use a mix of both. The difference between the two approaches is subtle and is also one of those things that does not seem to be fully agreed upon.

Both TDD and BDD are based on the idea that you write tests to drive the development of the application, the general process is:

  1. Write a test
  2. Run the test (it should fail)
  3. Write code to satisfy the test, and test again (it should pass)
  4. Refactor as necessary

I am not going to dive too deep into the different between TDD and BDD. In general, tests created with TDD are more granular and are concerned with the inner workings of the code (i.e. the writer of the tests knows or assumes how the code will be implemented), tests created with a BDD approach take more of a black box approach and are only concerned with what should happen, not how it is achieved.

A test written with a TDD approach might be described as:

it('should remove the last element in the array')

whereas a test written with a BDD approach might be described as:

it('should remove the last grocery item added')

Both of these are testing the same thing, but the TDD approach has knowledge of the internals of the implementation, whereas the BDD approach is only testing for the desired behaviour.

There are benefits to both approaches. With TDD it may be easier to spot why tests are failing because they are more granular, but if the implementation of certain functions change then the tests will need to be updated to reflect the new implementation (which isn’t as much of an issue for BDD).

Getting into the specifics of this is far beyond the scope of this series, I want to keep it simple, but I think it is worth touching on the difference between the two approaches.

Before We Get Started

Before you go through this tutorial, you should have at least a basic understanding of Ionic 2 concepts. You must also already have Ionic 2 installed on your machine.

If you’re not familiar with Ionic 2 already, I’d recommend reading my Ionic 2 Beginners Guide first to get up and running and understand the basic concepts. If you want a much more detailed guide for learning Ionic 2, then take a look at Building Mobile Apps with Ionic 2.

1. Generate a New Ionic 2 Application

First, you will need to create a new Ionic 2 application by running the following command:

ionic start ionic2-tdd-part1 blank --v2

We will be creating some pages and providers for this application, but we will do that later.

2. Set up Testing

The Ionic team is planning to add support for unit testing by default, but until that arrives we are going to have to hack together the set up ourselves.

Please follow this guide for setting up testing in your Ionic 2 project. You do not have to read or complete the entire tutorial, you will just need to follow the ‘2. Set up Testing’ section.

3. The Application

Before we get into building the application, we should discuss what it is that we are going to attempt to build. We’re going to keep it reasonably simple for now, but we will likely expand on the application throughout the series.

For now, we are going to develop an application that displays a list of products to the user. Later, we will expand this to include other functions like adding products to a favourites list, going through a checkout process, loading data dynamically and so on.

4. The First Test

We have everything set up now, and since we are using a Test Driven Development approach our first thought should be about writing our first test. Figuring out what that first test might be can be difficult.

As I mentioned before, Adam Wathan’s content helped a lot with deciding on a testing approach. He talks about writing your first test for a feature of your application that gets to the point of your application, without relying on any complicated dependencies. Starting with authentication wouldn’t be the best idea because it is rather complicated to test, and it doesn’t really have much to do with what your application actually does.

A good starting test for our application might be to test that a user can view a list of products. This is a reasonably simple test, and it goes straight to the core of what the application is about. In an Ionic 2 application, I also think it makes sense to write the first test for the root component since this is the entry point for the application.

Adam also suggests writing your tests in a way that you want them to work in an ideal scenario – don’t just write code to create a passing test as soon as possible. If you are writing a test and you think it would be a good idea to use some provider to handle something that does not yet exist, you should just pretend it does. This will cause your test to fail, which will force you to then create that provider to satisfy the test.

In effect, you will keep facing error after error in your tests until they eventually all pass. This is what TDD is all about.

We are going to start creating our tests now and you will see this in action.

5. Testing the Root Component

As I mentioned, the root component is the entry point for our application so we want to make sure that it is functioning as expected. So our first test is going to check that the Product page is displayed to the user.

Create a file at src/app/app.spec.ts and add the following:

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

let comp: MyApp;
let fixture: ComponentFixture<MyApp>;

describe('Component: Root Component', () => {

    beforeEach(async(() => {

        TestBed.configureTestingModule({

            declarations: [MyApp],

            providers: [

            ],

            imports: [
                IonicModule.forRoot(MyApp)
            ]

        }).compileComponents();

    }));

    beforeEach(() => {

        fixture = TestBed.createComponent(MyApp);
        comp    = fixture.componentInstance;

    });

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

    it('is created', () => {

        expect(fixture).toBeTruthy();
        expect(comp).toBeTruthy();

    });

    it('displays the product page to the user', () => {
        expect(comp['rootPage']).toBe(ProductPage);
    });

});

NOTE: If you do not understand what this code is doing, please make sure to read Introduction to Testing Ionic 2 Applications with TestBed first.

The first test we have is a simple is created test which tests if the component was successfully created – we will be adding this to every file. The second test is what we are interested in.

This checks that the ProductPage is set as the rootPage which is what determines what page will be shown to the user initially. If you were to run this test now using:

npm test

You will run into the following error:

Testing Screenshot

This is great – it’s exactly what we want. We have written a failing test, which means we can move onto the next step in the TDD process which is to make that test pass.

Right now, it is failing is because ProductPage does not exist yet, so let’s create that.

Run the following command to generate the Product page:

ionic g page Product

Modify the src/app/app.module.ts file to reflect the following:

import { NgModule, ErrorHandler } from '@angular/core';
import { IonicApp, IonicModule, IonicErrorHandler } from 'ionic-angular';
import { MyApp } from './app.component';
import { ProductPage } from '../pages/product/product';

@NgModule({
  declarations: [
    MyApp,
    ProductPage
  ],
  imports: [
    IonicModule.forRoot(MyApp)
  ],
  bootstrap: [IonicApp],
  entryComponents: [
    MyApp,
    ProductPage
  ],
  providers: [{provide: ErrorHandler, useClass: IonicErrorHandler}]
})
export class AppModule {}

If you run the tests again now using:

npm test

You will see a different error instead:

Testing Screenshot

The Product page is able to be imported now, but the test is still failing because our rootPage isn’t being set to ProductPage it is being set to the default HomePage (if you like, you can delete the home folder completely because we will not be using it). We are going to have to modify the root component so that it uses the ProductPage instead.

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

import { Component } from '@angular/core';
import { Platform } from 'ionic-angular';
import { StatusBar, Splashscreen } from 'ionic-native';
import { ProductPage } from '../pages/product/product';

@Component({
  templateUrl: 'app.html'
})
export class MyApp {
  rootPage = ProductPage;

  constructor(platform: Platform) {
    platform.ready().then(() => {
      // Okay, so the platform is ready and our plugins are available.
      // Here you can do any higher level native things you might need.
      StatusBar.styleDefault();
      Splashscreen.hide();
    });
  }
}

If you take a look at the tests now, you should see something like this:

Testing Screenshot

Hooray! We have a passing test. What we have done here is a very small example of using a Test Driven Development approach. We created a failing test that described the requirements of the feature, and then we implemented that feature such that it satisfied the test.

This test ensures that the Product page is displayed to the user.

6. Testing the Product Page

Now we are going to move onto the next test which is a little more interesting and will test that products are actually displayed to the user on that page.

Create a file at src/pages/product/product.spec.ts and add the following:

import { TestBed, ComponentFixture, async } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { DebugElement } from '@angular/core';
import { IonicModule, NavController } from 'ionic-angular';
import { MyApp } from '../../app/app.component';
import { ProductPage } from './product';
import { Products } from '../../providers/products';

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

describe('Page: Product Page', () => {

    beforeEach(async(() => {

        TestBed.configureTestingModule({

            declarations: [MyApp, ProductPage],

            providers: [
                NavController, Products
            ],

            imports: [
                IonicModule.forRoot(MyApp)
            ]

        }).compileComponents();

    }));

    beforeEach(() => {

        fixture = TestBed.createComponent(ProductPage);
        comp    = fixture.componentInstance;

    });

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

    it('is created', () => {

        expect(fixture).toBeTruthy();
        expect(comp).toBeTruthy();

    });

    it('displays products containing a title, description, and price in the list', () => {

        let productsService = fixture.debugElement.injector.get(Products);
        let firstProduct = productsService.products[0];

        fixture.detectChanges();

        de = fixture.debugElement.query(By.css('ion-list ion-item'));
        el = de.nativeElement; 

        expect(el.textContent).toContain(firstProduct.title);
        expect(el.textContent).toContain(firstProduct.description);
        expect(el.textContent).toContain(firstProduct.price);

    });


});

The set up for this test is very similar to the previous test, but the test itself is quite a bit more interesting. We are grabbing a reference to the first ion-item element in our template (which does not exist yet) to check if it contains the appropriate data from the first product in the Products provider (which also does not exist yet).

Remember before how I mentioned designing your tests in the way you want them to work? This is an example of that. I don’t have a Products service, but it makes sense to have a provider that handles the product data, so I am pretending that it does exist. When an error forces me to make that provider, I will.

So, let’s run the test and see what happens:

Testing Screenshot

We immediately run into issues because we are attempting to use a provider that doesn’t exist. This is blocking us from continuing, so we are going to switch gears and start building the product service instead, and then once we are done with that we will continue where we left off here.

7. Testing the Products Service

We need to build the Products service to satisfy our Product page test, but we can’t create a new service unless we have a failing test for it first. That’s what we are going to do now.

It isn’t necessary to include the entire TestBed set up to test a simple service like this. Instead, we will be using a more basic set up as we did in the How to Unit Test an Ionic 2 Application tutorial.

Create the provider with the following command:

ionic g provider Products

Modify src/app/app.module.ts to reflect the following:

import { NgModule, ErrorHandler } from '@angular/core';
import { IonicApp, IonicModule, IonicErrorHandler } from 'ionic-angular';
import { MyApp } from './app.component';
import { ProductPage } from '../pages/product/product';
import { Products } from '../providers/products';

@NgModule({
  declarations: [
    MyApp,
    ProductPage
  ],
  imports: [
    IonicModule.forRoot(MyApp)
  ],
  bootstrap: [IonicApp],
  entryComponents: [
    MyApp,
    ProductPage
  ],
  providers: [{provide: ErrorHandler, useClass: IonicErrorHandler}, Products]
})
export class AppModule {}

Create a file at src/providers/products.spec.ts and add the following:

import { Products } from './products';

let productsService;

describe('Provider: Products', () => {

    beforeEach(() => {
      productsService = new Products();
    });

    it('should have a non empty array called products', () => {

        let products = productsService.products;

        expect(Array.isArray(products)).toBeTruthy();
        expect(products.length).toBeGreaterThan(0);

    });

});

This will test if our Products service has an array called products as a member variable, and that the array contains some data. Let’s run the test and see what happens:

Testing Screenshot

We actually get a few errors now. Our Product page test has reached a different error because it can now successfully import the provider, even though it doesn’t do what it needs to yet. We can also see that there is an error for NavParams because we are including it unnecessarily, but we will get to that later.

The error we are interested in now, and the one that is pictured above, is the one for the Products provider. It is trying to access the length property of a member variable that does not exist yet. Now that we have our failing test, let’s implement the service:

Modify src/providers/products.ts to reflect the following:

import { Injectable } from '@angular/core';
import 'rxjs/add/operator/map';

@Injectable()
export class Products {

    products: any;

    constructor() {

        this.products = [
            {title: 'Cool shoes', description: 'Isn\'t it obvious?', price: '39.99'},
            {title: 'Broken shoes', description: 'You should probably get the other ones', price: '89.99'},
            {title: 'Socks', description: 'The essential footwear companion', price: '2.99'}
        ];

    }

}

If you run the tests now you will still see some errors, but those are for the Product page. Our Products provider should now be passing the tests:

Testing Screenshot

8. Testing the Product Page (again)

Since we have finished implementing the Products provider, we can go back to testing the Product page again. Let’s take a look at why it is failing:

Testing Screenshot

It fails right away because we don’t have a provider for NavParams, but we don’t need to use NavParams, it is just included by default in the page generation. Let’s remove it.

Modify src/pages/product/product.ts to reflect the following:

import { Component } from '@angular/core';
import { NavController } from 'ionic-angular';

@Component({
  selector: 'page-product',
  templateUrl: 'product.html'
})
export class ProductPage {

  constructor(public navCtrl: NavController) {}

  ionViewDidLoad() {

  }

}

and then let’s run the test again:

Testing Screenshot

We have a new error now, which is what we like to see. It is saying that it can’t read the nativeElement property. This is because of the following section in the test:

        de = fixture.debugElement.query(By.css('ion-list ion-item'));
        el = de.nativeElement; 

We are attempting to grab a reference to an item in a list, but we haven’t even implemented our template yet so it can’t find it.

Modify src/pages/product/product.html to reflect the following:

<ion-header>

  <ion-navbar>
    <ion-title>Product</ion-title>
  </ion-navbar>

</ion-header>


<ion-content>

    <ion-list>
        <ion-item *ngFor="let product of productsService.products">
            <h3>{{product.title}}</h3>
            <span>{{product.price}}</span>
        </ion-item>
    </ion-list>

</ion-content>

and let’s run the tests again (this should happen automatically, by the way):

Testing Screenshot

This time, we are getting an error about the products property being undefined. This is because we are referencing the Products service in our template, but we haven’t set it up in our TypeScript file yet.

Modify src/pages/product/product.ts to reflect the following:

import { Component } from '@angular/core';
import { NavController } from 'ionic-angular';
import { Products } from '../../providers/products'

@Component({
  selector: 'page-product',
  templateUrl: 'product.html'
})
export class ProductPage {

  constructor(public navCtrl: NavController, public productsService: Products) {}

  ionViewDidLoad() {

  }

}

and, once again, let’s run the tests!

Testing Screenshot

This is an interesting error. In our test, we are checking for the existence of the title, price, and description of the first product in the products service in the list on the Product page. But we’ve forgotten to include the description in the template! Obviously, I did this on purpose, but I think it highlights how testing like this can help you pick up on issues along the way. Let’s fix it.

Modify src/pages/product/product.html to reflect the following:

<ion-header>

  <ion-navbar>
    <ion-title>Product</ion-title>
  </ion-navbar>

</ion-header>

<ion-content>

    <ion-list>
        <ion-item *ngFor="let product of productsService.products">
            <h3>{{product.title}}</h3>
            <span>{{product.price}}</span>
            <p>{{product.description}}</p>
        </ion-item>
    </ion-list>

</ion-content>

…and let’s run the tests again:

Testing Screenshot

Hooray! Everything is all green and all of our tests have passed. This means that we can be reasonably sure that our code is working as intended, and as we add more features to the application (writing tests for them first, of course) we can be confident that we aren’t making breaking changes to the existing code.

If we later decide that we could refactor this code to improve it, we can do so and have our tests making sure that we aren’t breaking anything unintentionally.

Summary

It may seem a bit tedious, and it is a bit of extra work, but TDD is very much an example of “slow and steady wins the race”. You will code slower, but you will make fewer mistakes along the way, and when you finish you will have a robust set of automated tests.

UPDATE: Part 2 is out now.

What to watch next...

  • jamzi

    Nice tutorial Joshua, also liked the links to Adam Wathan’s talks and posts. I have a question. Is it just my setup or the whole including angular-cli to ionic project and all packages tend to slow down the execution of test. In my case these couple of tests are running for like 5-10s. Also, webpack gives me warnings for entrypoint size limit (may impack web performance).

    • Test execution speed is something I plan to look into at some point, as you can see above its taking around 5 seconds to execute the 5 tests, not really too sure at this stage what factors into the execution speed (apart from doing things like hitting servers of course)

  • Thank you Josh, this tutorial worked perfectly except for one minor issue, in that ‘ionic serve’ stopped working with “cannot find name ‘describe'” – this stackoverflow answer solved the problem http://stackoverflow.com/a/39945169/307455

    • Chanoch

      I didn’t get this issue – did you type the code out as in my latest attempt I just copied and pasted the code and it worked perfectly. Strange.

      • Yes it is strange. I copied & pasted the code & definitely got the issue yesterday on my first test file app.spec.ts, which was fixed by importing from jasmine as per stackoverflow, but I noticed in later spec.ts files I didn’t need the import. In my case it’s just app.spec.ts which needs the import for “ionic serve” to continue working.

      • Chanoch

        Excellent – I’m glad you have got that resolved. I find it quite disheartening when things go wrong without explanation.

        You might find that this is related to the rapid changing of the ionic libraries and dependencies – including the test libs. I am finding that even from day to day there are dot versions being released so running the same tutorial on different days will give slightly different results. On the whole, though, very pleased with the stability and quality of the product.

  • Chanoch

    I get the issue Chrome 56.0.2924 (Linux 0.0.0) ERROR
    Uncaught TypeError: Cannot read property ‘ev’ of null
    at webpack:///~/ionic-angular/platform/dom-controller.js:112:0 <- src/test.ts:4969

    This is an ionic-angular bug which is going to be fixed in the next release. You can get the fix by installing the latest 2.0.0 snapshot:

    npm install –save –save-exact [email protected]

    (as per https://github.com/driftyco/ionic/issues/10186 – thanks to Felipe on github)

    • Code Khalifah

      Did you have to do anything after installing the fix? I’m still having the same issue

      • Chanoch

        I think I did npm install. Try that and if it doesn’t work let me know

  • Jens

    Thanks for the tutorial! It helped a lot 🙂
    I have one question:
    As soon as I have slides in my template, everything (the “is created” test) breaks down.
    Do you have any idea how to solve this?
    Thanks 🙂