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. This is not really an effective testing process, as it is not rigorous and mostly ad-hoc.
Unit tests - and automated tests in general - are a very efficient way to help verify that your codebase is behaving correctly. The general concept of unit tests is that you take small testable chunks (units) of an application’s functionality and test that it works as you expect using additional testing code (as opposed to verifying the behaviour manually yourself). Essentially, you write code to automatically test your code for you.
Angular and Ionic are very modular by nature (i.e. most features in an Ionic/Angular application are their own little independent components that work together with other components). This structure already lends itself quite nicely to setting up unit tests. A key part of creating an effective unit test is that we are able to isolate a single bit of functionality, and so if our application is organised and modular, it is going to be easier to do this.
Creating these automated tests 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 (and other types of automated 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
- Verification - you can have greater confidence that your applications code is behaving the way you 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 there is a greater chance that you will find out about it, as you can run the same tests against the new code
- Code Quality - it is difficult to write effective tests for poorly designed/organised applications, whereas it is much easier to write tests for well-designed code. Naturally, writing automated tests for your applications will force you into writing good code
- Sleep - you’ll be less of a nervous wreck when deploying your application to the app store, because you are going to have a greater degree of confidence that your code works (and the same goes for when you are updating your application)
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 back then to learn and set up automated tests 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/Angular applications.
Here’s what we’ll be building:
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.
If you would like a more advanced introduction to testing Ionic/Angular application, I would recommend reading my Test Driven Development in Ionic series or my Elite Ionic (Angular) course.
Before we Get Started
Last updated for Ionic/Angular 4.1.0
Before you go through this tutorial, you should have at least a basic understanding of Ionic/Angular concepts.
If you’re not familiar with Ionic/Angular already, I’d recommend reading my beginner tutorials first to get up and running and understand the basic concepts. If you want a much more detailed guide for learning Ionic/Angular, then take a look at Building Mobile Apps with Ionic & Angular.
1. Generate a New Ionic 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 ionic-magic-ball blank --type=angular
Once that has finished generating make it your current directory:
cd ionic-magic-ball
and then generate the “MagicBall” provider with the following command:
ionic g service services/MagicBall
2. An Introduction to Jasmine
As I mentioned before, we will be using Jasmine and Karma to run the unit tests in our application. In previous versions of Ionic/Angular we had to set this up manually, but fortunately everything we need for automated testing is now included by default.
Jasmine is what we use to create the unit tests, and Karma is what runs them. 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 (e.g. a “collection”) of tests (or “specs”)
- it() defines a specific test or “spec”, and it lives inside of a suite (describe()). This is what defines the expected behaviour of the code you are testing, e.g. “it should do this”, “it should do that”
- expect() defines the expected result of a test and lives inside of it()
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. 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 automated tests are written first, and then the actual code is written afterward. This helps define requirements and ensures that tests are always created. We’re going to have a go at that ourselves (in a very loose sense of the definition of Test Driven Development - we are going to keep things very basic for now). The process goes like this:
- Write a test
- Run the test (it will fail)
- Write your code
- Run the test (it will pass, hopefully)
Modify the file at src/services/magic-ball.service.spec.ts and add the following:
import { MagicBallService } from './magic-ball.service';
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:
Chrome 75.0.3770 (Mac OS X 10.14.4): Executed 4 of 4 SUCCESS (0.283 secs / 0.193 secs)
TOTAL: 4 SUCCESS
TOTAL: 4 SUCCESS
NOTE: The other successful tests are due to the default tests included for the home page and root component
Modify src/services/magic-ball.service.spec.ts to reflect the following:
import { MagicBallService } from './magic-ball.service';
describe('Magic 8 Ball Service', () => {
it('should do nothing', () => {
expect(true).not.toBeTruthy();
});
});
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
(or if you still have the tests running from before) we would see something like this:
Chrome 75.0.3770 (Mac OS X 10.14.4): Executed 4 of 4 (1 FAILED) (0.267 secs / 0.24 secs)
TOTAL: 1 FAILED, 3 SUCCESS
TOTAL: 1 FAILED, 3 SUCCESS
Modify src/services/magic-ball.service.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 entire 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:
Chrome 75.0.3770 (Mac OS X 10.14.4) Magic 8 Ball Service should do nothing FAILED
Expected 4 to be 5.
at UserContext.<anonymous> (src/app/services/magic-ball.service.spec.ts:20:19)
at ZoneDelegate../node_modules/zone.js/dist/zone.js.ZoneDelegate.invoke (node_modules/zone.js/dist/zone.js:391:1)
at ProxyZoneSpec.onInvoke (node_modules/zone.js/dist/zone-testing.js:289:1)
Chrome 75.0.3770 (Mac OS X 10.14.4): Executed 4 of 4 (1 FAILED) (0.174 secs / 0.162 secs)
TOTAL: 1 FAILED, 3 SUCCESS
TOTAL: 1 FAILED, 3 SUCCESS
Notice that the error message says Expected 4 to be 5
, which was our failing test - this gives us a good indication of what has gone wrong. Now let’s define our real tests.
Modify src/services/magic-ball.service.spec.ts to reflect the following:
import { MagicBallService } from './magic-ball.service';
let magicBall = null;
describe('Magic 8 Ball Service', () => {
beforeEach(() => {
magicBall = new MagicBallService();
});
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/failures because we haven’t even defined the methods for MagicBallService 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 src/services/magic-ball.service.spec.ts to reflect the following:
import { Injectable } from "@angular/core";
@Injectable({
providedIn: "root"
})
export class MagicBallService {
public answers: string[] = [
"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 src/app/home/home.page.html to reflect the following:
<ion-content class="ion-padding">
<ion-list>
<ion-item>
<ion-input
type="text"
[(ngModel)]="question"
placeholder="Ask the mystic 8 ball anything..."
></ion-input>
</ion-item>
<ion-button expand="full" color="dark" (click)="showAnswer()"
>Click to Decide Fate</ion-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 src/app/home/home.page.ts to reflect the following:
import { Component } from "@angular/core";
import { MagicBallService } from "../services/magic-ball.service";
@Component({
selector: "app-home",
templateUrl: "home.page.html",
styleUrls: ["home.page.scss"]
})
export class HomePage {
public answer: string = "...";
constructor(public magicBall: MagicBallService) {}
showAnswer() {
this.answer = this.magicBall.getRandomAnswer();
}
}
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:
Chrome 75.0.3770 (Mac OS X 10.14.4): Executed 6 of 6 SUCCESS (0.052 secs / 0.046 secs)
TOTAL: 6 SUCCESS
TOTAL: 6 SUCCESS
All three tests that we have created 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 other forms of testing (automated and otherwise) comes in, but we will get to that in future tutorials.
This is also just a very basic example of unit testing, so we will cover some more advanced scenarios in future tutorials as well. Of course, if you would like to dive head first into adding automated tests to your Ionic/Angular applications, check out Elite Ionic (Angular).