In the first three articles in this series, you built a simple mobile app and server using the Ionic Framework and StrongLoop LoopBack. In this article, you’ll learn about one key advantage of building mobile apps with Ionic: access to the JavaScript testing ecosystem. Testing native apps is hard. TravisCI has beta support for Android builds, and setting up iOS testing on Travis is a nightmarish tangle of accounts and certificates. Since your Ionic app is just JavaScript, however, you can test your AngularJS code in a browser. In this article, you’ll use karma and PhantomJS to test the directives in your mobile app.

Karma Basics

TLDR; See a diff for this step on GitHub

Karma is a browser automation tool for testing. The general purpose of karma is to start a browser, run some code, and log the output to the shell. In this section, you’ll learn enough about karma to run a basic directive test in Travis; if you’re interested in learning more, I wrote a more detailed guide to Karma on the StrongLoop blog.

Karma is like gulp; it’s a lightweight core that’s highly pluggable, but needs a plugin to do anything non-trivial. In particular, you need plugins to enable karma to launch specific browsers. For this article, you’ll use 2 karma plugins: one that enables karma to launch PhantomJS, and one that provides an adapter for the mocha testing framework.

devDependencies: {
  "karma": "0.13.15",
  "karma-mocha": "0.2.0",
  "karma-phantomjs-launcher": "0.2.1",
  "mocha": "2.3.3",
  "phantomjs": "1.9.18"
}

Once you’ve installed karma, you’ll need to do a little extra work to make your tests easy to run. You’ll need to create a separate AngularJS module that contains all the stopwatch-specific logic from the third article in this series. Why? Getting the whole Ionic bundle to run in a browser is unnecessary in this case, because the stopwatch directives don’t touch any Ionic-specific code. All you really need to test is how the directives work in conjunction with the HTTP interceptors and other Ionic-specific configuration.

Once you’ve created this `stopwatch.js` file that contains an AngularJS module with all of your stopwatch-specific directives, it’s time to set up a karma config file. Karma config files can be intimidating at first, but they make sense once you remember the 3 things that karma is responsible for: starting browsers, running some JavaScript, and reporting output.

module.exports = function(config) {
  config.set({
    // Start these browsers
    browsers: ['PhantomJS'],
    // Load these files
    basePath: '../',
    files: [
      'http://code.jquery.com/jquery-1.9.1.js',
      'https://cdnjs.cloudflare.com/ajax/libs/chai/3.4.0/chai.js',
      'https://ajax.googleapis.com/ajax/libs/angularjs/1.4.7/angular.js',
      'https://ajax.googleapis.com/ajax/libs/angularjs/1.4.7/angular-resource.js',
      'https://ajax.googleapis.com/ajax/libs/angularjs/1.4.7/angular-mocks.js',
      'https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.10.6/moment.js',
      'www/js/stopwatch.js',
      'www/js/directives/index.js',
      'www/js/templates/index.js',
      // These are all your test files
      'test/*.test.js'
    ],
    frameworks: ['mocha'],
    // And a couple other details
    port: 9876,
    singleRun: true
  });
};

The above config assumes that all the tests will be in the `test/` directory. Let’s write a basic mocha test and run it.

describe('test', function() {
  it('works', function() {
    assert.equal('A', 'A');
  });
});

In order to make karma easier to run, you can create a script in your package.json:

"scripts": {
  "test": "karma start test/karma.phantom.conf.js"
}

Now, once you run `npm test`, you should see output that looks like this:

$ npm test

> stopwatch-example@1.0.0 test
> karma start test/karma.phantom.conf.js

05 11 2015 18:43:30.691:INFO [karma]: Karma v0.13.15 server started at http://localhost:9876/
05 11 2015 18:43:30.699:INFO [launcher]: Starting browser PhantomJS
05 11 2015 18:43:31.357:INFO [PhantomJS 1.9.8 (Linux 0.0.0)]: Executed 1 of 1 SUCCESS (0.038 secs / 0 secs)

Testing AngularJS Directives

TLDR; See a diff for this step on GitHub

No offense to Aristotle, but a test that asserts that A is A is not particularly interesting. Part of what makes AngularJS so powerful is the ability to test directives from the level of user interactions without a server. In other words, you can test your directives using jQuery’s `.click()` and `.val()` methods without having to set up a server.

How does this work? The key is AngularJS’ `$compile` service. If you don’t understand the `$compile` docs, don’t worry, I don’t either. At a high level, the `$compile` service takes in an HTML string and an AngularJS scope, and compiles the HTML into a DOM element attached to the given scope. In other words, the `$compile` service lets you instantiate your directives.

var injector = angular.injector(['stopwatch', 'ngMockE2E']);

injector.invoke(function($rootScope, $compile) {
  parentScope = $rootScope.$new();

  var html = '<timer on-time-saved="onSaved(time);"></timer>';
  element = $compile(html)(parentScope);
  // Can now do things like `element.css('display')` and
  // `element.find('button').click();`
});

The `angular.injector()` function is what you use to generate your `$compile` service. This creates a new AngularJS dependency injector from the given modules. Since the injector knows about the stopwatch module, the `$compile` service will know about the `timer` directive and give you a jQuery handle to an instantiated `timer` directive.

In order to really run this test, you’re going to need three more details. First, you’re going to need to define the `onSaved()` function. This is just going to be a stub for testing.

parentScope.onSaved = function(time) {
  parentScope.onSaved.calls.push(time);
};
parentScope.onSaved.calls = [];

Secondly, recall that the `timer` directive creates a new AngularJS scope. The `parentScope` variable is useful, but what if we want access to the directive’s internals? Thankfully, a scope has a `$$childHead` member that points to its first child scope.

scope = parentScope.$$childHead;

Finally, you’re going to want to test that clicking on the ‘Save’ button triggers the correct HTTP request. Thankfully, that’s what AngularJS’ `$httpBackend` service (part of the `ngMockE2E` module you saw above) is for.

injector.invoke(function($rootScope, $compile, $httpBackend) {
  parentScope = $rootScope.$new();
  httpBackend = $httpBackend;
});

With that, you’re ready to write your first test. The full code can be found on GitHub. Let’s walk through the high-level concepts in this test. Now that you have the element, you first want to click on the ‘Start’ button in the `timer` directive and make sure the directive reacted correctly.

// Click the 'Start' button
element.find('button[ng-click="startTimer()"]').click();
// Make sure the internal state updated
assert.equal(scope.state, 'RUNNING');
assert.equal(scope.ms, 0);
// And make sure the 'Start' button no longer appears
assert.ok(element.find('button[ng-click="startTimer()"]').
  hasClass('ng-hide'));

Now, you want to wait a while and make sure the timer updates every second correctly.

setTimeout(function() {
  assert.equal(scope.ms, 1000);
}, 1100);

Once the timer has updated to 1 second, your test is going to click the stop button and assert that the directive reacted correctly.

// Click on the 'Stop' button
element.find('button[ng-click="stopTimer()"]').click();
// Make sure the internal state updated correctly
assert.equal(scope.state, 'STOPPED');

// Tell AngularJS to expect an HTTP POST request whose body satisfies
// this function
var validateData = function(data) {
  assert.deepEqual(JSON.parse(data), { time: 1000 });
  return true;
};
httpBackend.expectPOST('http://localhost:3000/api/Times', validateData).
  respond(200, { result: 'success' });

// Click on the 'Save' button and trigger the request
element.find('button[ng-click="save()"]').click();

// Tell AngularJS to respond to the HTTP POST request
httpBackend.flush();

// Make sure the internal state updated correctly
assert.equal(scope.state, 'SUCCESS');

If you’re interested in learning more, there’s a detailed guide to testing AngularJS directives on my blog.

Setting Up Travis

TLDR; See a diff for this step on GitHub

Once you have tests for your mobile app, you can set up Travis to run your tests on every commit to GitHub. The tests are the hard part, setting up Travis to work with Node is much easier than Android or iOS. All you need to do is set up an account on Travis, add your Ionic app’s GitHub repo, and add the below `.travis.yml` file to your repo.

language: node_js
node_js:
  - "4"
script: "npm test"

Easy, right? If you need extra help setting up Travis, there’s a detailed guide to setting up Travis for Node.js on the StrongLoop blog.

Conclusion

That’s a wrap! You’ve now built a LoopBack REST API, a desktop web client, and an Ionic Framework mobile app. You even put together a CI setup for your mobile app. LoopBack and it’s corresponding AngularJS SDK enabled you to generate a REST API and it’s corresponding UI components with a few commands and minimal coding. The Ionic Framework let you leverage these browser-based UI components to build an easily-testable mobile app. Looks like JavaScript isn’t just for the browser (or the server) anymore.