Unit Testing in Ionic

How to Unit Test an Ionic 2 Application



·

Everybody (hopefully) tests their applications in some manner before submitting them to the app stores, and for a lot of people, this will include playing around with the app and trying it on different devices. That alone is a poor and inefficient testing process, however.

Unit testing, on the other hand, is a very efficient testing process. Unit testing is where you take small testable chunks (units) of an application and test that they work properly. Essentially, you write code to automatically test your code for you.

Angular 2 and Ionic 2 are very modular by nature (i.e. most features in an Ionic 2 app are their own little independent components that work together with other components) so unit testing is quite easy to set up.

This obviously requires more development effort, as you need to write tests as well as the code itself. So why might we want to invest in doing that? The main benefits of adding unit tests to your application are:

  • Documentation – unit tests are set out in such a way that they accurately describe the intended functionality of the application
  • Testing – of course, this is a great way to test that your application is working as intended
  • Regression Testing – when you make changes to your application you can be more confident that you haven’t broken anything else, and if you have you’ll likely know about it
  • Sleep – you’ll be less of a nervous wreck when deploying your application to the app store

Arguably, someone like myself who is a freelancer working primarily on small mobile applications won’t see as much benefit from setting up automated tests as a large team developing the next Dropbox. But even as a freelancer I’ve taken on some projects that started out small and well defined enough but grew into monsters that I spent hours manually testing and fixing for every update. If I had’ve taken the time to set up unit testing my life could have been a lot easier.

If you’re looking for a little more background on unit testing in general, take a look at: An Introduction to Unit Testing in AngularJS Applications.

In this tutorial I am going to show you how you can set up simple unit testing with Jasmine and Karma in your Ionic 2 applications. Here’s what we’ll be building:

Ionic 2 Magic Ball

We’re going to start off by setting up a really simple test for one service in the application, but we will likely expand on this in future tutorials.

UPDATE: If you would like a more advanced introduction to testing Ionic 2 application, I would recommend reading my Test Driven Development in Ionic 2 series.

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 set up 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

The application we are going to build will simulate a “Magic 8 Ball”. Basically, the user will be able to write a question, hit a button, and an answer to their question will be “magically” calculated (i.e. chosen at random).

Let’s start off by generating a new application with the following command:

ionic start ionic2-magic-ball blank --v2

Once that has finished generating make it your current directory:

cd ionic2-magic-ball

and then generate the “MagicBall” provider with the following command:

ionic g provider MagicBall

So that we can use this provider throughout the application, we will need to add it to the app.module.ts file.

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 { HomePage } from '../pages/home/home';
import { MagicBall } from '../providers/magic-ball';

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

2. An Introduction to Jasmine

As I mentioned before, we will be using Jasmine and Karma to unit test our application. Jasmine is what we use to create the unit tests, and Karma is what runs them. We will need to set both of these up in our application before we can use them, but first, let’s talk a little bit about how Jasmine works.

Jasmine is a framework for writing code that tests your code. It does that primarily through the following three functions: describe, it, and expect:

  • describe() defines a suite of tests (or “specs”)

  • it() defines a test or “spec”, and it lives inside of a suite (describe()). This is what defines the expected behaviour of the code you are testing, i.e. “it should do this”, “it should do that”

  • expect() defines the expected result of a test and lives inside of it().

So a skeleton for a test might look something like this:

describe('My Service', () => {

    it('should correctly add numbers', () => {

        expect(1 + 1).toBe(2);

    });

});

In this example we have a test suite called “My Service” that contains a test for correctly adding numbers. We use expect to check that the result of 1 + 1 is 2. To do this we use the toBe() matcher function which is provided by Jasmine. There’s a whole range of these methods available for testing different scenarios, for example:

  • expect(fn).toThrow(e);
  • expect(instance).toBe(instance);
  • expect(mixed).toBeDefined();
  • expect(mixed).toBeFalsy();
  • expect(number).toBeGreaterThan(number);
  • expect(number).toBeLessThan(number);
  • expect(mixed).toBeNull();
  • expect(mixed).toBeTruthy();
  • expect(mixed).toBeUndefined();
  • expect(array).toContain(member);
  • expect(string).toContain(substring);
  • expect(mixed).toEqual(mixed);
  • expect(mixed).toMatch(pattern);

and if you want to get really advanced you can even define your own custom matchers. The example we have used is just for demonstration and is a bit silly, because we have manually supplied values which will of course pass the test. When we create tests for a real world scenario shortly it should help clarify how you might actually create a unit test for your application.

3. Setting up Jasmine and Karma

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.

4. Create and Run a Unit Test

It’s finally time to create our first test. You may have heard of the term “Test Driven Development”. Basically it’s a development process where the tests are written first, and then the actual code is written afterwards. This helps define requirements and ensures that tests are always created. So to stick with the theme, we’re going to have a go at that ourselves. The process goes like this:

  1. Write a test
  2. Run the test (it will fail)
  3. Write your code
  4. Run the test (it will pass, hopefully)

Create a new file at providers/magic-ball.spec.ts and add the following:

import { MagicBall } from './magic-ball';

describe('Magic 8 Ball Service', () => {

    it('should do nothing', () => {

        expect(true).toBeTruthy();

    });

});

We’ve set up a really basic test here. We import our MagicBall service and then we have created a test that will always pass. If we were to run npm test now we would see something like this:

Unit Test 1

Modify magic-ball.spec.ts to reflect the following:

import { MagicBall } from './magic-ball';

describe('Magic 8 Ball Service', () => {

    it('should do nothing', () => {

        expect(true).not.toBeTruthy(); //can also use .toBeFalsy();

    });

});

Now we have negated the test using .not (alternatively, we could have used toBeFalsy), so that it will always fail. Now if we ran npm test we would see something like this:

Unit Test 2

Modify magic-ball.spec.ts to reflect the following:

import { MagicBall } from './magic-ball';

describe('Magic 8 Ball Service', () => {

    it('should do nothing', () => {

        expect(true).toBeTruthy();
        expect(1 + 1).toBe(2);
        expect(2 + 2).toBe(5); //this will fail

    });

});

One last example before we get into the real test. You can add multiple conditions to each test and if any of them fail the test will fail. In this case we have two that will pass, but one that will fail. If we were to run npm test with this we would see the following:

Unit Test 3

Notice that the error messages says “Expected 4 to be 5”, which was our failing test. Now let’s define our real tests.

Modify magic-ball.spec.ts to reflect the following:

import { MagicBall } from './magic-ball';

let magicBall = null;

describe('Magic 8 Ball Service', () => {

    beforeEach(() => {
      magicBall = new MagicBall();
    });

    it('should return a non empty array', () => {

            let result = magicBall.getAnswers();

            expect(Array.isArray(result)).toBeTruthy;
            expect(result.length).toBeGreaterThan(0);
        }
    ));

    it('should return one random answer as a string', () => {
            expect(typeof magicBall.getRandomAnswer()).toBe('string');
        }
    ));

    it('should have both yes and no available in result set', () => {

            let result = magicBall.getAnswers();

            expect(result).toContain('Yes');
            expect(result).toContain('No');

        }
    ));

});

We have defined three tests here, which:

  • Test that the getAnswers method returns a non-empty array
  • Test that getRandomAnswer returns a string
  • Test that both ‘Yes’ and ‘No’ are in the result set

Also, notice that the tests are a bit more complicated now. Since we are using the MagicBall service it needs to be injected into our tests. We do this by using beforeEach (which runs before each of the tests) to create a fresh instance of our magic ball service for each of the tests. We’re making use of various matchers here, including toContain and toBeGreaterThan.

If you were to run the tests now using npm test you will just get a whole bunch of errors because we haven’t even defined the methods for MagicBall yet. This is what we want, though. We have some tests that don’t pass, now we just need to make them pass.

4. Build the App

Now we’re going to build out the rest of the app, which will involve implementing the MagicBall service, and also creating a simple layout to display the answer.

Modify magic-ball.ts to reflect the following:

import { Injectable } from '@angular/core';

@Injectable()
export class MagicBall {

  answers: any;

  constructor(){

    this.answers = [
      'Yes',
      'No',
      'Maybe',
      'All signs point to yes',
      'Try again later',
      'Without a doubt',
      'Don\'t count on it',
      'Most likely',
      'Absolutely not'
    ];

  }

  getAnswers(){
    return this.answers;
  }

  getRandomAnswer(){
    return this.answers[this.getRandomInt(0, this.answers.length-1)];
  }

  getRandomInt(min, max){
    return Math.floor(Math.random() * (max - min + 1)) + min;
  }

}

Pretty simple stuff here, we’ve just manually defined an array with some magic answers, and created a couple of methods to access those.

Modify home.html to reflect the following:

<ion-content padding>

  <ion-list>

    <ion-item>
      <ion-input type="text" [(ngModel)]="question" placeholder="Ask the mystic 8 ball anything..."></ion-input>
    </ion-item>

    <button ion-button full color="dark" (click)="showAnswer()">Click to Decide Fate</button>

    <h2>{{answer}}</h2>

  </ion-list>

</ion-content>

This sets up an input field, a button to trigger fetching an answer, and a spot to display the answer. We will also need to set up our class definition to handle fetching and displaying the answers.

Modify home.ts to reflect the following:

import { Component } from '@angular/core';
import { MagicBall } from '../../providers/magic-ball';

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

  answer: any = "..."

  constructor(public magicBall: MagicBall) {

  }

  showAnswer(){
    this.answer = this.magicBall.getRandomAnswer();
  }

}

and let’s also pretty things up a bit:

Modify home.scss to reflect the following

page-home {

    .scroll-content {
        background-color: #8e44ad;
    }

    h2 {
        margin-top: 50%;
        text-align: center;
        color: #fff;
    }

}

5. Run the Tests Again

We’ve finished implementing our MagicBall service now, and if you run it through ionic serve everything seems to be working. However, to make sure we can now run npm test and we should see something like the following:

Unit Test 4

All three tests have passed! If you were to change the answers array so that it didn’t include “Yes” or “No” and re-ran the tests, you would see that one fails.

Summary

Unit tests are a great way to test your application, but that isn’t the end of the story. Whilst individual components in an application might work flawlessly, when working together they might fail. This is where Integration Testing comes in, but we will get to that in a future tutorial.

This is also just a very basic example of unit testing, so we will cover some more advanced scenarios in future tutorials as well.

UPDATE: I now have a more advanced series of tutorials on testing available: Test Driven Development in Ionic 2.

What to watch next...

  • Florian Hübner

    Great article! Looking forward to the next part. Will testing be added to your Ionic 2 book in the future?

    • Sagar Mody

      Would love to see this as a part of your book as well. So that becomes our Bible 🙂

  • Jb

    Good tutorial! Can you please write what version of beta your tutorials are? so it will be easier to make changes

  • Yo Joshua… Nice article! I’m here wondering if there is a simple solution to setup Ionic 2 Unit Testing without TypeScript. Looks like everyone is using TS, but we’re developing without it for now and I’m having some trouble in configure karma to recognize es6 code. Thanks in advance.

    By the way… I got your book, but one more time everything is using TypeScript 🙁

    • If you’ve been following my older blog posts you’ll know I used to use ES6 and only recently switched to TypeScript. I’d highly suggest switching to TypeScript as you’ll keep running into issues like this where most resources you find will be in TypeScript, and some things are just a lot easier with TypeScript too. TypeScript is an extension of ES6, so you don’t even need to change anything – you can just create a new TS project, copy and paste your old code into it, and it should all just work.

  • foobar123

    Hello,

    thank you for this helpful tutorial, it works great!
    I noticed just one problem that occurs when trying to inject something that depends on Platform:
    When adding Platfom (from ionic-angular) to the dependencies like this:
    beforeEachProviders(() => [Messages, Platform]);
    I get this error: Error: Cannot resolve all parameters for ‘Platform'(?). Make sure that all the parameters are decorated with Inject or have valid type annotations and that ‘Platform’ is decorated with Injectable.

    Do you know how this can be fixed?

    • foobar123

      ok, I found a solution: it is possible to mock Platform:

      import {Platform as PlatformOrig} from ‘ionic-angular’;
      import {provide} from ‘@angular/core’;

      var _Platform = {
      is: function() {}
      };
      export var Platform = provide(PlatformOrig, {useFactory: () => _Platform});

      then the new Platform object can be injected.

      • Remberto Martinez

        I created PlatformMock and injected in my constructor with no problem. But when I start really needing some functions I need to mock them all.

        win() and registerBackButtonAction() any idea how to solve it?

  • Fedor Usakov

    Perfect introduction to testing! Great job! Thank you very much.
    Looking forward to the next part and info on how to test visual appearance of the Ionic 2 application.

  • Fedor Usakov

    Will appreciate for any tutorials on how to customize karma error report?
    I tried to install and configurate this one: https://www.npmjs.com/package/karma-mocha-reporter
    But it looks like it has a problem with a browserify and one need special knowledge to configure and customize reports…

  • Hugh Hou

    Owned the highest level of your book. Would love to see a chapter dedicated to unit tests and end-to-end testing of a real app that implement Test Driven Development principle. As matter of fact, if all the example app start with TDD of writing test before development like what you did here, it will be super 🙂 Thank for great tutorials as always.

  • Tushar Patekar

    Great tutorial as always, i am new to unit testing in ionic 2, currently i am struggling in testing an EventEmitter(@Output) in a component. it would be great to have a advance version of this tutorial.

    Thanks for the tutorial.

  • macdasi

    Keep getting errors when I run the test :
    ” [framework.browserify]: bundle error , TypeScript error:…/@angular/core/src/ application_ref.d.ts(79,88): Error TS2304: Cannot find name ‘Promise’.”

    • Al Ex

      npm i –save-dev @types/es6-promise

  • David W. Gray

    Nice post. It definitely got me up and running with jasmine/karma in no time. I’m pretty new to the whole npm ecosystem and I’m wondering why you did manual edits to the package.json file rather than using npm installs? I got a little stuck because the karma version that was pulled in initially with the npm install was different than the one you specified, so I tried just doing npm installs to pull in the various packages.

    npm install –save-dev karma browserify karma-browserify jasmine-core browserify-istanbul tsify isparta

    seems to get a workable setup with karma/jasmine and all the other bits you need.

    • Thanks! I had to add watchify to the npm-install

  • David W. Gray

    Have you tried debugging these tests? For me there seems to be something funky going on with ts compilation/source mapping. For the magic-ball.spec.ts everything works fine in chrome (hit the debug button on the karma page, open up developer tools, that file magic-ball.spec.ts is available and I can set breakpoints/step) – but magic-ball.ts isn’t mapped: I get a file that looks like the attached picture: Looks like a compiled TS file with source mappings embedded. Can you see magic-ball.ts in the debugger? Any thoughts on diagnosing this? (BTW I’ve tried this both with the npm-install version of things I used in my last comment and the direct package.json munging that you used – same result both ways).

    • David W. Gray

      To answer my own question (with some help from https://github.com/lathonez as he just re-fixed this bug in the project that you referenced): The problem is that you’re doing a transform to embed code coverage info into the javascript file. I was stumped because from the 1000 foot view code coverage and debug mapping information look pretty similar. The quick fix for your project is to remove the transform element from the browserify section of karma.conf.js. Since you don’t actually use the code coverage data, there is some further cleanup that one could do, but that change gets you to be able to debug the code under test.

  • Alex Mishyn

    Thanks for the post. Do you use Cointinious Integration, if so, which service ?

  • Yuriy Mamchur

    Hi, thanks for your post. It’s great. But I have a problem then I need to inject Http provider. https://uploads.disquscdn.com/images/1d7cba6b60c6d6b189a9031d007477cbbca2e8ec6cbdefd351ac1bcdb34cd15f.png
    Can you help me? How can I do it?

  • 0v3rst33r

    Hi Josh, have you perhaps gotten around to porting the testing code to Ionic 2 RC? If so it would be great if you can update this post/article (or even create a new one). Seems like things have changed quite a bit (even on Angular 2 side, with imports not available anymore, etc).

  • Lukas Jakob

    Hi Josh, great article. I guess things have changed a bit for the karma setup for Ionic2 rc.1 Do you have any infos about that. Would be great to share, thanks!

  • Emily Xiong

    You need to install typings for jasmine
    Need to include @types/jasmine in npm package

  • Emily Xiong

    i got this error
    “ParseError: ‘import’ and ‘export’ may appear only with ‘sourceType: module'”

  • Ryan Corbin

    Does this work in ionic RC3? Are there any major changes that I should know before I start?

  • Hi josh
    If service needed http or other providers how should we mock them?

  • ianholden123

    Thanks for writing this article! I have been waiting for an article on unit testing in Ionic 2 that just works. This has been a great help.

  • Hi, Josh. I followed this tutorial, but I’m getting an error. Whenever I try to serve the app, the transpiling ends with an error

    https://uploads.disquscdn.com/images/c0324f899f01213a2899cb602b5434b7de3fafeb92ea7e06316f23d75437bcb7.png

    And if I try to open up the browser, It shows these errors in more detail

    https://uploads.disquscdn.com/images/ddea73e9cbf14517e4f992c75207326895b618741e33022fd4df0462a398f79e.png

    Thanks for the blog, it is amazing! I’ve been learning so much from it. Keep up the great work!

    • So, I’ve searched around, and I’ve found this https://github.com/DefinitelyTyped/DefinitelyTyped/issues/14569#issuecomment-279306281. According to this, If I update my typescript module to above 2.0.X it should work. So I went and edited my package.json to allow packages above 2.0.0 (“typescript”: “^2.0.0”), ran an npm update and it worked out great!

      • Gemma Stephen

        This didn’t help me, still getting same errors!

      • Rachna Adwani

        I am already having the version “typescript”: “2.0.9” in my package.json, still facing the same issue 🙁
        Can you help ?

      • Ted Wei

        “typescript”: “2.2.1” works

  • yatendra

    hello, I am creating this tutorial, and setup according to “https://www.joshmorony.com/how-to-unit-test-an-ionic-2-application/”, but when i run npm test getting following output

    Can not load “webpack”!
    TypeError: Cannot read property ‘plugin’ of undefined
    at PathsPlugin.apply (/Users/kipl/Desktop/Projects/Dummy/ionic2-magic-ball/node_modules/@ngtools/webpack/src/paths-plugin.js:75:18)
    at Resolver.apply (/Users/kipl/Desktop/Projects/Dummy/ionic2-magic-ball/node_modules/tapable/lib/Tapable.js:375:16)
    at /Users/kipl/Desktop/Projects/Dummy/ionic2-magic-ball/node_modules/enhanced-resolve/lib/ResolverFactory.js:249:12
    at Array.forEach (native)
    at Object.exports.createResolver (/Users/kipl/Desktop/Projects/Dummy/ionic2-magic-ball/node_modules/enhanced-resolve/lib/ResolverFactory.js:248:10)
    at WebpackOptionsApply.process (/Users/kipl/Desktop/Projects/Dummy/ionic2-magic-ball/node_modules/webpack/lib/WebpackOptionsApply.js:283:47)
    at webpack (/Users/kipl/Desktop/Projects/Dummy/ionic2-magic-ball/node_modules/webpack/lib/webpack.js:37:48)
    at new Plugin (/Users/kipl/Desktop/Projects/Dummy/ionic2-magic-ball/node_modules/karma-webpack/lib/karma-webpack.js:68:16)
    at invoke (/Users/kipl/Desktop/Projects/Dummy/ionic2-magic-ball/node_modules/di/lib/injector.js:75:15)
    at Array.instantiate (/Users/kipl/Desktop/Projects/Dummy/ionic2-magic-ball/node_modules/di/lib/injector.js:59:20)
    at get (/Users/kipl/Desktop/Projects/Dummy/ionic2-magic-ball/node_modules/di/lib/injector.js:48:43)
    at /Users/kipl/Desktop/Projects/Dummy/ionic2-magic-ball/node_modules/di/lib/injector.js:71:14
    at Array.map (native)
    at Array.invoke (/Users/kipl/Desktop/Projects/Dummy/ionic2-magic-ball/node_modules/di/lib/injector.js:70:31)
    at Injector.get (/Users/kipl/Desktop/Projects/Dummy/ionic2-magic-ball/node_modules/di/lib/injector.js:48:43)
    at instantiatePreprocessor (/Users/kipl/Desktop/Projects/Dummy/ionic2-magic-ball/node_modules/karma/lib/preprocessor.js:55:20)
    at /Users/kipl/Desktop/Projects/Dummy/ionic2-magic-ball/node_modules/karma/lib/preprocessor.js:106:17
    at Array.forEach (native)
    at /Users/kipl/Desktop/Projects/Dummy/ionic2-magic-ball/node_modules/karma/lib/preprocessor.js:103:27
    at module.exports (/Users/kipl/Desktop/Projects/Dummy/ionic2-magic-ball/node_modules/karma/node_modules/isbinaryfile/index.js:28:12)
    at /Users/kipl/Desktop/Projects/Dummy/ionic2-magic-ball/node_modules/karma/lib/preprocessor.js:84:7
    at /Users/kipl/Desktop/Projects/Dummy/ionic2-magic-ball/node_modules/graceful-fs/graceful-fs.js:78:16
    at FSReqWrap.readFileAfterClose [as oncomplete] (fs.js:503:3)
    05 10 2017 12:57:29.652:WARN [karma]: No captured browser, open http://localhost:9876/
    05 10 2017 12:57:29.662:INFO [karma]: Karma v1.7.1 server started at http://0.0.0.0:9876/
    05 10 2017 12:57:29.662:INFO [launcher]: Launching browser Chrome with unlimited concurrency
    05 10 2017 12:57:29.663:ERROR [karma]: Found 1 load error

    My system config is:-
    cli packages: (/usr/local/lib/node_modules)

    @ionic/cli-utils : 1.12.0
    ionic (Ionic CLI) : 3.12.0

    local packages:

    @ionic/app-scripts : 2.1.4
    Ionic Framework : ionic-angular 3.6.1

    System:

    Node : v8.1.0
    npm : 5.0.3
    OS : macOS Sierra

    Misc:

    backend : legacy