Testing AngularJS With Protractor and Karma - Part 2

This article details how to test a simple AngularJS application using unit tests and end-to-end (E2E) tests.

angular + protractor

  • Part 1 - In the first part we’ll look at unit tests, which ensure that small, isolated pieces of code (e.g., a unit) behave as expected.
  • Part 2 - In part two we’ll address E2E tests, which verify that all the pieces of code (units) fit together by simulating the user experience through browser automation. (current)

Contents:

  1. Project Setup
  2. The Tests
  3. Conclusion

Having finished up unit testing, let’s now turn our attention to e2e testing using Protractor, which is a testing framework built specifically for AngularJS apps. Essentially, it runs tests against an app in the browser via Selenium Webdriver, interacting with the app from an end user’s perspective.

protractor components

Since e2e tests are much more expensive than unit tests - e.g., they generally take more time to run and are harder to write and maintain - you should almost always focus the majority of your testing efforts on unit tests. It’s good to follow the 80/20 rule - 80% of your tests are unit tests, while 20% are e2e tests. That said, this tutorial series breaks this rule since the goal is to educate. Keep this in mind as you write your own tests against your own application.

Also, make sure you test the most important aspects/functions of your application with your e2e tests. Don’t waste time on the trivial. Again, they are expensive, so make each one count.

The repo includes the following tags:

  1. v1 - project boilerplate
  2. v2 - adds testing boilerplate/configuration
  3. v3 - adds unit tests
  4. v4 - adds E2E tests

Project Setup

Assuming you followed the first part of this tutorial, checkout the third tag, v3, and then run the current test suite starting with the unit tests:

1
2
3
4
5
6
7
8
9
10
$ git checkout tags/v3
$ gulp unit

[23:30:01] Using gulpfile ~/angular-testing-tutorial/Gulpfile.js
[23:30:01] Starting 'unit'...
INFO [karma]: Karma v0.12.31 server started at http://localhost:9876/
INFO [launcher]: Starting browser Chrome
INFO [Chrome 42.0.2311 (Mac OS X 10.10.2)]: Connected on socket i04LmGbgt7P1lNIUTgIJ with id 48442826
Chrome 42.0.2311 (Mac OS X 10.10.2): Executed 12 of 12 SUCCESS (0.236 secs / 0.051 secs)
[23:30:06] Finished 'unit' after 4.43 s

For the e2e tests, you’ll need to open two new terminal windows. In the first new window, run webdriver-manager start. In the second, navigate to your project directory and then run the app - gulp.

Finally, back in the original window, run the tests:

1
2
3
4
5
6
7
8
9
10
11
12
13
$ gulp e2e

[23:31:11] Using gulpfile ~/angular-testing-tutorial/Gulpfile.js
[23:31:11] Starting 'e2e'...
Using the selenium server at http://localhost:4444/wd/hub
[launcher] Running 1 instances of WebDriver
.

Finished in 1.174 seconds
1 test, 1 assertion, 0 failures

[launcher] 0 instance(s) of WebDriver still running
[launcher] chrome #1 passed

Everything look good?

The Tests

Open the test spec, spec.js, within the “tests/e2e” directory. Let’s look at the first test:

1
2
3
4
5
6
7
8
describe('myController', function () {

  it('the dom initially has a greeting', function () {
    browser.get('http://localhost:8888/#/one');
    expect(element(by.id('greeting')).getText()).toEqual('Hello, World!');
  });

});

Notice how we’re still using Mocha and Chai to manage/structure the test so that it simply opens http://localhost:8888/#/one and then asserts that the text within the HTML element with an ID of greeting is Hello, World!. Simple, right?

Let’s take a quick look at the Angular services that we’re using:

  1. browser - loads the page in the browser
  2. element - interacts with the page
  3. by - finds elements within the page

Finally, one important thing to note is how these tests run. Notice that there’s no callbacks and/or promises in the test. How does that work with asynchronous code? Simple: Protractor continues to check each assertion until it passes or a certain amount of time passes. There also is a promise attached to most methods that can be access using then.

With that, let’s write some tests on our own.

TestOneController

Just like in the first part, open the controller code:

1
2
3
4
5
6
7
8
9
myApp.controller('TestOneController', function($scope) {
  $scope.greeting = "Hello, World!";
  $scope.newText = undefined;
  $scope.changeGreeting = function() {
    if ($scope.newText !== undefined) {
      $scope.greeting = $scope.newText;
    }
  };
});

How about the HTML?

1
2
3
4
<h2>Say something</h2>
<input type="text" ng-model="newText">
<button class="btn btn-default" ng-click="changeGreeting()">Change!</button>
<p id="greeting"></p>

Looking at the Angular code along with the HTML, we know that on the button click, greeting is updated with the user supplied text from the input box. Sound right? Test this out: With the app running via Gulp, navigate to http://localhost:8888/#/one and manually test the app to ensure that the controller is working as it should.

Now since we already tested the initial state of greeting, let’s write the test to ensure that the state updates on the button click:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
describe('TestOneController', function () {

  beforeEach(function() {
    browser.get('http://localhost:8888/#/one');
  });

  it('initially has a greeting', function () {
    expect(element(by.id('greeting')).getText()).toEqual('Hello, World!');
  });

  it('clicking the button changes the greeting if text is inputed', function () {
    element(by.css('[ng-model="newText"]')).sendKeys('Hi!');
    element(by.css('.btn-default')).click();
    expect(element(by.id('greeting')).getText()).toEqual('Hi!');
  });

  it('clicking the button does not change the greeting if text is not inputed', function () {
    element(by.css('.btn-default')).click();
    expect(element(by.id('greeting')).getText()).toEqual('Hello, World!');
  });

});

So, in both new test cases we’re targeting the input form - via the global element function - and adding text to it with the sendKeys() method - Hi! in the first test and no text in the second. Then after clicking the button, we’re asserting that the text contained within the HTML element with an id of “greeting” is as expected.

Run the tests. If all went well, you should see:

1
2
3
4
5
6
7
8
9
10
11
12
[06:15:45] Using gulpfile ~/angular-testing-tutorial/Gulpfile.js
[06:15:45] Starting 'e2e'...
Using the selenium server at http://localhost:4444/wd/hub
[launcher] Running 1 instances of WebDriver
...

Finished in 3.606 seconds
3 tests, 3 assertions, 0 failures

[launcher] 0 instance(s) of WebDriver still running
[launcher] chrome #1 passed
Michaels-MacBook-Pro-3:angular-testing-tutorial michael$

Did you see Chrome open in a new window and run the tests, then close itself? It’s super fast!! Want to run the tests in Firefox (or a different browser) as well? Simply update the Protractor config file, protractor.conf.js, like so:

1
2
3
4
5
6
7
8
9
exports.config = {
  seleniumAddress: 'http://localhost:4444/wd/hub',
  specs: ['tests/e2e/*.js'],
  multiCapabilities: [{
    browserName: 'firefox'
  }, {
    browserName: 'chrome'
  }]
};

Test it again. You should now see the tests run in both Chrome and Firefox simultaneously. Nice.

Finally, to simplify the code and speed up the tests (so we only search the DOM once per element), we can assign each element to a variable:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
describe('TestOneController', function () {

  var greeting = element(by.id('greeting'));
  var textInputBox = element(by.css('[ng-model="newText"]'));
  var changeGreetingButton = element(by.css('.btn-default'));

  beforeEach(function() {
    browser.get('http://localhost:8888/#/one');
  });

  it('initially has a greeting', function () {
    expect(greeting.getText()).toEqual('Hello, World!');
  });

  it('clicking the button changes the greeting if text is inputed', function () {
    textInputBox.sendKeys('Hi!');
    changeGreetingButton.click();
    expect(greeting.getText()).toEqual('Hi!');
  });

  it('clicking the button does not change the greeting if text is not inputed', function () {
    textInputBox.sendKeys('');
    changeGreetingButton.click();
    expect(greeting.getText()).toEqual('Hello, World!');
  });

});

Test one last time to ensure that this refactor didn’t break anything.

TestTwoController

Again, start with the code.

Angular:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
myApp.controller('TestTwoController', function($scope) {
  $scope.total = 6;
  $scope.newItem = undefined;
  $scope.items = [1, 2, 3];
  $scope.add = function () {
    if(typeof $scope.newItem == 'number') {
      $scope.items.push($scope.newItem);
      $scope.total = 0;
      for(var i = 0; i < $scope.items.length; i++){
        $scope.total += parseInt($scope.items[i]);
      }
    }
  };
});

HTML:

1
2
3
4
<h2>Add values</h2>
<input type="number" ng-model="newItem">
<button class="btn btn-default" ng-click="add()">Add!</button>
<p></p>

Then test it in the browser.

Like last time, we simply need to ensure that total is updated appropriately when the end user submits a number in the input box and then clicks the button.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
describe('TestTwoController', function () {

  var total = element(by.tagName('p'));
  var numberInputBox = element(by.css('[ng-model="newItem"]'));
  var changeTotalButton = element(by.css('.btn-default'));

  beforeEach(function() {
    browser.get('http://localhost:8888/#/two');
  });

  it('initially has a total', function () {
    expect(total.getText()).toEqual('6');
  });

  it('updates the `total` when a value is added', function () {
    numberInputBox.sendKeys(7);
    changeTotalButton.click();
    numberInputBox.clear();
    expect(total.getText()).toEqual('13');
    numberInputBox.sendKeys(7);
    changeTotalButton.click();
    expect(total.getText()).toEqual('20');
    numberInputBox.clear();
    numberInputBox.sendKeys(-700);
    changeTotalButton.click();
    expect(total.getText()).toEqual('-680');
  });

  it('does not update the `total` when an empty value is added', function () {
    numberInputBox.sendKeys('');
    changeTotalButton.click();
    expect(total.getText()).toEqual('6');
    numberInputBox.sendKeys('hi!');
    changeTotalButton.click();
    expect(total.getText()).toEqual('6');
  });

});

Run the tests and you should see:

1
6 tests, 9 assertions, 0 failures

Moving along…

TestThreeController

You know the drill:

  1. Look at the Angular and HTML code
  2. Manually test in the browser
  3. Write the e2e test to automate the manual test

Try this on your own before looking at the code below.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
describe('TestThreeController', function () {

  var modalNumber = element.all(by.tagName('span')).get(1);
  var modalButton = element(by.tagName('button'));
  var iterateButton = element(by.css('[ng-click="changeModalText()"]'));
  var hideButton = element(by.css('[ng-click="$hide()"]'));
  var justSomeText = element(by.tagName('h2'));

  beforeEach(function() {
    browser.get('http://localhost:8888/#/three');
  });

  it('initially has a modalNumber', function () {
    modalButton.click();
    expect(modalNumber.getText()).toEqual('1');
  });

  it('updates the `modalNumber` when a value is added', function () {
    modalButton.click();
    iterateButton.click();
    expect(modalNumber.getText()).toEqual('2');
    iterateButton.click().click().click();
    expect(modalNumber.getText()).toEqual('5');
    hideButton.click();
    expect(justSomeText.getText()).toEqual('Just a modal');
  });

TestFourController

Since this controller makes an external call to https://api.github.com/repositories you can either mock out (fake) this request using ngMockE2E, like we did for the unit test, or you can actually make the API call. Again, this depends on how expensive the call is and how important the functionality is to your application. In most cases, it’s better to actually make the call since e2e tests should mimic the actual end user experience as much as possible. Plus, unlike unit tests which test implementation, these tests test user behavior, across several independent units - thus, these tests should not be isolated and can rely on making actual API calls either to the back-end or externally.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
describe('TestFourController', function () {

  var loadButton = element(by.tagName('button'));
  var ul = element.all(by.tagName('ul'));
  var li = element.all(by.tagName('li'));

  beforeEach(function() {
    browser.get('http://localhost:8888/#/four');
  });

  it('updates the DOM when the button is clicked', function () {
    expect(ul.count()).toEqual(1);
    expect(li.count()).toEqual(5);
    loadButton.click();
    expect(ul.count()).toEqual(101);
    expect(li.count()).toEqual(105);
  });

});

Here, when the button is clicked, the API call is made and the scope is updated. We then assert that there are 101 UL tags and 105 LI tags, representing a Github username and repo returned from the API call, present on the DOM.

That’s it!

Conclusion

Want more?

  1. Take a look at the Page Objects design pattern and refactor the tests so that they are better organized.
  2. Break a test, and then pause the test before the break via browser.pause() and/or browser.debugger() to debug.
  3. Test your own Angular app, and then add a link to the comments to get feedback.

Be sure to check the Protractor documentation for more. Thanks again for reading, and happy testing!



Interested in learning how to test an Angular + Django app? Check out Real Python for details.

Testing AngularJS With Protractor and Karma - Part 1

This article details how to test a simple AngularJS application using unit tests and end-to-end (E2E) tests.

angular + karma

  • Part 1 - In the first part we’ll look at unit tests, which ensure that small, isolated pieces of code (e.g., a unit) behave as expected (current).
  • Part 2 - In part two we’ll address E2E tests, which verify that all the pieces of code (units) fit together by simulating the user experience through browser automation.

Contents:

  1. Project Setup
  2. Configuration Files
  3. Unit Tests
  4. Conclusion

To accomplish this we will be using Karma v0.12.31 (test runner) and Chai v2.2.0 (assertions) for the unit tests (along with Karma-Mocha) and Protractor v2.0.0 for the E2E tests. This article also uses Angular v1.3.15. Be sure to take note of all dependencies and their versions in the package.json and bower.json files in the repo.

The repo includes the following tags:

  1. v1 - project boilerplate
  2. v2 - adds testing boilerplate/configuration
  3. v3 - adds unit tests
  4. v4 - adds E2E tests

Project Setup

Start by cloning the repo, checkout out the first tag, and then install the dependencies:

1
2
3
4
$ git clone https://github.com/mjhea0/angular-testing-tutorial.git
$ cd angular-testing-tutorial
$ git checkout tags/v1
$ npm install && bower install

Run the app:

1
$ gulp

Navigate to http://localhost:8888 to view the live app.

angular app

Test it out. Once done, kill the server and checkout the second tag:

1
$ git checkout tags/v2

There should now be a “tests” folder and a few more tasks in the Gulpfile.

Run the unit tests:

1
$ gulp unit

They should pass:

1
2
3
4
5
6
7
[05:28:02] Using gulpfile ~/angular-testing-tutorial/Gulpfile.js
[05:28:02] Starting 'unit'...
INFO [karma]: Karma v0.12.31 server started at http://localhost:9876/
INFO [launcher]: Starting browser Chrome
INFO [Chrome 41.0.2272 (Mac OS X 10.10.2)]: Connected on socket JBQp0aEyu8KSqUfGoxsd with id 94772581
Chrome 41.0.2272 (Mac OS X 10.10.2): Executed 2 of 2 SUCCESS (0.061 secs / 0.002 secs)
[05:28:05] Finished 'unit' after 3.23 s

Now for the e2e tests:

  1. 1st terminal window: webdriver-manager start
  2. 2nd terminal window (within the project directory): gulp
  3. 3rd terminal window (within the project directory): gulp e2e

They should pass as well:

1
2
3
4
5
6
7
8
9
10
11
[05:29:45] Using gulpfile ~/angular-testing-tutorial/Gulpfile.js
[05:29:45] Starting 'e2e'...
Using the selenium server at http://localhost:4444/wd/hub
[launcher] Running 1 instances of WebDriver
.

Finished in 0.921 seconds
1 test, 1 assertion, 0 failures

[launcher] 0 instance(s) of WebDriver still running
[launcher] chrome #1 passed

So, what’s happening here…

Configuration Files

There are two configuration files in the “tests” folder - one for Karma and the other for Protractor.

Karma

Karma is a test runner built by the AngularJS team that executes the unit tests and reports the results.

Let’s look the config file, karma.conf.js:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
module.exports = function(config) {
  config.set({

    // base path that will be used to resolve all patterns
    basePath: '.',

    // frameworks to use
    frameworks: ['mocha', 'chai'],

    // list of files / patterns to load in the browser
    files: [
      '../app/bower_components/angular/angular.js',
      '../app/bower_components/jquery/dist/jquery.js',
      '../app/bower_components/angular-strap/dist/angular-strap.js',
      '../app/bower_components/angular-strap/dist/angular-strap.tpl.js',
      '../app/bower_components/angular-mocks/angular-mocks.js',
      '../app/bower_components/angular-route/angular-route.js',
      './unit/*.js',
      '../app/app.js'
    ],

    // test result reporter
    reporters: ['progress'],

    // web server port
    port: 9876,

    // enable / disable colors in the output (reporters and logs)
    colors: true,

    // level of logging
    logLevel: config.LOG_INFO,

    // enable / disable watching file and executing tests whenever any file changes
    autoWatch: true,

    // start these browsers
    browsers: ['Chrome'],

    // Continuous Integration mode
    singleRun: false
  });
};

You can also run karma init to be guided through the creation of a config file.

Be sure to read over the comments for an overview of each config option. For more information, review the official documentation.

Protractor

Protractor provides a nice wrapper around WebDriverJS, the JavaScript bindings for Selenium Webdriver, to run tests against an AngularJS application running live in a browser.

Turn your attention to the Protractor config file, protractor.conf.js:

1
2
3
4
exports.config = {
  seleniumAddress: 'http://localhost:4444/wd/hub',
  specs: ['tests/e2e/*.js']
};

This tells protractor where to find the test files (called specs) and specifies the address that the Selenium server is running on. Simple.

Ready to start testing?

Unit Tests

We’ll start with unit tests since they are much easier to write, debug, and maintain.

Keep in mind that unit tests, by definition, only test isolated units of code so they rely heavily on mocking fake data. This can add much complexity to your tests and can decrease the effectiveness of the actual tests. For example, if you’re mocking out an HTTP request to a back-end API, then you’re not really testing your application. Instead you’re simulating the request and then using fake JSON data to simulate the response back. The tests may run faster, but they are much less effective.

When starting out, mock out only the most expensive requests and make the actual API call in other situations. Over time you will develop a better sense of which requests should be mocked and which should not.

Finally, if you decide not to mock a request in a specific test, then the test is no longer a unit test since it’s not testing an isolated unit of code. Instead you are testing multiple units, which is an integration test. For simplicity, we will continue to refer to such tests as unit tests.

With that, let’s create some tests, broken up by controller!

TestOneController

Take a look at the code in the first controller:

1
2
3
4
5
6
7
myApp.controller('TestOneController', function($scope) {
  $scope.greeting = "Hello, World!";
  $scope.newText = undefined;
  $scope.changeGreeting = function() {
    $scope.greeting = $scope.newText;
  };
});

What’s happening here? Confirm your answer by running your app and watching what happens. Now, what can/should we test?

  1. greeting has an initial value of "Hello, World!", and
  2. The changeGreeting function updates greeting.

You probably noticed that we are already testing this in the spec:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
describe('TestOneController', function () {

  var controller = null;
  $scope = null;

  beforeEach(function () {
    module('myApp');
  });

  beforeEach(inject(function ($controller, $rootScope) {
    $scope = $rootScope.$new();
    controller = $controller('TestOneController', {
      $scope: $scope
    });
  }));

  it('initially has a greeting', function () {
    assert.equal($scope.greeting, "Hello, World!");
  });

  it('clicking the button changes the greeting', function () {
    $scope.newText = "Hi!";
    $scope.changeGreeting();
    assert.equal($scope.greeting, "Hi!");
  });

});

What’s happening?

  1. The describe block is used to group similar tests.
  2. The module, myApp, is loaded, into each test, in the first beforeEach block, which instantiates a clean testing environment.
  3. The dependencies are injected, a new scope is created, and the controller is instantiated in the second beforeEach.
  4. Each it function is a separate test, which includes a title, in human readable form, and a function with the actual test code.
  5. The first test asserts that the initial state of greeting is "Hello, World!".
  6. Meanwhile, the second test assets that the changeGreeting() function actually changes the value of greeting.

Make sense?

In most cases, unit tests simply change the scope and assert that the results are what we expected.

In general, when testing controllers, you inject then register the controller with a beforeEach block, along with the $rootScope and then test that the functions within the controller act as expected.

Run the tests again to ensure they still pass - gulp unit.

What else could we test? How about if newText doesn’t change - e.g., if the user submits the button without entering any text in the input box - then the value of greeting should stay the same. Try writing this on your own, before you look at my answer:

1
2
3
4
it('clicking the button does not change the greeting if text is not inputed', function () {
  $scope.changeGreeting();
  assert.equal($scope.greeting, "Hello, World!");
});

Try running this. It should fail.

1
2
Chrome 41.0.2272 (Mac OS X 10.10.2) TestOneController clicking the button does not change the greeting FAILED
  AssertionError: expected undefined to equal 'Hello, World!'

So, we’ve revealed a bug. We could fix this by adding validation to the input box to ensure the end user enters a value or we could update changeGreeting to only update greeting if newText is not undefined. Let’s go with the latter.

1
2
3
4
5
$scope.changeGreeting = function() {
  if ($scope.newText !== undefined) {
    $scope.greeting = $scope.newText;
  }
};

Save the code, and then run the tests again:

1
2
3
4
5
6
7
8
$ gulp unit
[08:28:18] Using gulpfile ~/angular-testing-tutorial/Gulpfile.js
[08:28:18] Starting 'unit'...
INFO [karma]: Karma v0.12.31 server started at http://localhost:9876/
INFO [launcher]: Starting browser Chrome
INFO [Chrome 41.0.2272 (Mac OS X 10.10.2)]: Connected on socket HGnVC5-cAXOZjAsrSCWj with id 83240025
Chrome 41.0.2272 (Mac OS X 10.10.2): Executed 3 of 3 SUCCESS (0.065 secs / 0.001 secs)
[08:28:21] Finished 'unit' after 3.13 s

Nice!

Since controllers are used to bind data to the template (via scope), unit tests are perfect for testing the controller logic - e.g., what happens to the scope as the controller runs - while E2E tests ensure that the template is updated accordingly.

TestTwoController

Start by analyzing the code:

1
2
3
4
5
6
7
8
9
10
11
12
myApp.controller('TestTwoController', function($scope) {
  $scope.total = 6;
  $scope.newItem = undefined;
  $scope.items = [1, 2, 3];
  $scope.add = function () {
    $scope.items.push($scope.newItem);
    $scope.total = 0;
    for(var i = 0; i < $scope.items.length; i++){
      $scope.total += parseInt($scope.items[i]);
    }
  };
});

What should we test? Take out a pen and paper and write down everything that should be tested. Once done, write the code. Check your code against mine.

Be sure to start with the following boilerplate:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
describe('TestTwoController', function () {

  var controller = null;
  $scope = null;

  beforeEach(function () {
    module('myApp');
  });

  beforeEach(inject(function ($controller, $rootScope) {
    $scope = $rootScope.$new();
    controller = $controller('TestTwoController', {
      $scope: $scope
    });
  }));

});

Test 1: The initial value of total

1
2
3
it('initially has a total', function () {
  assert.equal($scope.total, 6);
});

Test 2: The initial value of items

1
2
3
4
it('initially has items', function () {
  assert.isArray($scope.items);
  assert.deepEqual($scope.items, [1, 2, 3]);
});

Test 3: The add function updates the total and items array when a value is added

1
2
3
4
5
6
it('the `add` function updates the `total` and `items` array when a value is added', function () {
  $scope.newItem = 7;
  $scope.add();
  assert.equal($scope.total, 13);
  assert.deepEqual($scope.items, [1, 2, 3, 7]);
});

Test 4: The add function does not update the total and items array when an empty value is added

1
2
3
4
5
6
7
8
9
10
it('does not update the `total` and `items` array when an empty value is added', function () {
  $scope.newItem = undefined;
  $scope.add();
  assert.equal($scope.total, 6);
  assert.deepEqual($scope.items, [1, 2, 3]);
  $scope.newItem = 22;
  $scope.add();
  assert.equal($scope.total, 28);
  assert.deepEqual($scope.items, [1, 2, 3, 22]);
});

Run

Each test should be straightforward. Run the tests. There should be one failure:

1
2
Chrome 41.0.2272 (Mac OS X 10.10.2) TestTwoController does not update the `total` and `items` array when an empty value is added FAILED
  AssertionError: expected NaN to equal 6

Update the code, adding a conditional again:

1
2
3
4
5
6
7
8
$scope.add = function () {
  if(typeof $scope.newItem == 'number') {
    $scope.items.push($scope.newItem);
    $scope.total = 0;
    for(var i = 0; i < $scope.items.length; i++){
      $scope.total += parseInt($scope.items[i]);
    }
  }

Also update the partial:

1
<input type="number" ng-model="newItem">

Run it again:

1
2
3
4
5
6
7
8
$ gulp unit
[09:56:10] Using gulpfile ~/angular-testing-tutorial/Gulpfile.js
[09:56:10] Starting 'unit'...
INFO [karma]: Karma v0.12.31 server started at http://localhost:9876/
INFO [launcher]: Starting browser Chrome
INFO [Chrome 41.0.2272 (Mac OS X 10.10.2)]: Connected on socket Lbv1sROpYrEHgotlmJZf with id 91008249
Chrome 41.0.2272 (Mac OS X 10.10.2): Executed 7 of 7 SUCCESS (0.082 secs / 0.003 secs)
[09:56:13] Finished 'unit' after 3.05 s

Success!

Did I miss anything? Comment below.

TestThreeController

Again, check out the code in app.js:

1
2
3
myApp.controller('TestThreeController', function($scope) {
  $scope.modal = {title: 'Hi!', content: 'This is a message!'};
});

What can we test here?

1
2
3
4
it('initially has a modal', function () {
  assert.isObject($scope.modal);
  assert.deepEqual($scope.modal, {title: 'Hi!', content: 'This is a message!'});
});

Perhaps a better question is: What should we test here? Is the above test really necessary? Probably not. But we may need to test it out more in the future if we build out the functionality. Let’s go for it!

Update app.js:

1
2
3
4
5
6
7
8
9
10
myApp.controller('TestThreeController', function($scope, $modal) {
  $scope.modalNumber = 1;
  var myModal = $modal({scope: $scope, template: 'modal.tpl.html', show: false});
  $scope.showModal = function() {
    myModal.$promise.then(myModal.show);
  };
  $scope.changeModalText = function() {
    $scope.modalNumber++;
    };
});

Here we are defined a custom template, modal.tpl.html, to be used for the modal text and then we assigned $scope.modalNumber to 1 as well as function to iterate the number.

Add modal.tpl.html:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<div class="modal" tabindex="-1" role="dialog">
  <div class="modal-dialog">
    <div class="modal-content">
      <div class="modal-body">
        <span>
          <button class="btn btn-default" ng-click="changeModalText()">Iterate</button>
          &nbsp;&#8594;&nbsp;
          <span></span>
        </span>
      </div>
      <div class="modal-footer">
        <button type="button" class="btn btn-default" ng-click="$hide()">Close</button>
      </div>
    </div>
  </div>
</div>

Add this template to the “app” folder.

Update three.html:

Finally, update the partial:

1
2
3
4
<h2>Just a modal</h2>
<button type="button" class="btn btn-lg btn-default" data-template="modal.tpl.html" bs-modal="modal">
  Launch modal!
</button>

Run the app to make sure everything works, and then update the test…

Test redux

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
describe('TestThreeController', function () {

  var controller = null;
  $scope = null;

  beforeEach(function () {
    module('myApp');
  });

  beforeEach(inject(function ($controller, $rootScope) {
    $scope = $rootScope.$new();
    controller = $controller('TestThreeController', {
      $scope: $scope
    });
  }));

  it('initially has a modalNumber', function () {
    assert.equal($scope.modalNumber, 1);
  });

  it('updates the `modalNumber` when a value is added', function () {
    $scope.changeModalText();
    assert.equal($scope.modalNumber, 2);
    $scope.changeModalText();
    assert.equal($scope.modalNumber, 3);
  });

});

Notice how we’re no longer testing that a modal is present. We’ll test that via the E2E tests.

TestFourController

Finally, let’s test the AJAX request:

1
2
3
4
5
6
7
8
myApp.controller('TestFourController', function($scope, $http) {
  $scope.repos = [];
  $scope.loadRepos = function () {
    $http.get('https://api.github.com/repositories').then(function (repos) {
      $scope.repos = repos.data;
    });
  };
});

Remember the discussion earlier on mocking HTTP requests? Well, here’s probably a good place to actually use a mocking library since this request hits an external API. To do this, we can use the $httpBackend directive from the angular-mocks library.

First, let’s first add the mock.js file found in the repo into a new folder called “mock” within the “tests” folder. This module use angular.module().value to set a JSON value to use as the fake data.

Next, add the test:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
describe('TestFourController', function () {

  var controller = null;
  var $scope = null;
  var $httpBackend = null;
  var mockedDashboardJSON = null;

  beforeEach(function () {
    module('myApp', 'mockedDashboardJSON');
  });

    beforeEach(inject(function ($controller, $rootScope, _$httpBackend_, defaultJSON) {
      $httpBackend = _$httpBackend_;
      $scope = $rootScope.$new();
      $httpBackend.when('GET','https://api.github.com/repositories').respond(defaultJSON.fakeData);
      controller = $controller('TestFourController', {
          $scope: $scope
      });
    }));

    afterEach(function () {
      $httpBackend.verifyNoOutstandingExpectation();
      $httpBackend.verifyNoOutstandingRequest();
    });

  it('initially has repos', function () {
    assert.isArray($scope.repos);
    assert.deepEqual($scope.repos, []);
  });

  it('clicking the button updates the repos', function () {
      $scope.loadRepos();
      $httpBackend.flush();
      assert.equal($scope.repos.length, 100);
  });

});

What’s happening?

  1. Essentially, here we’re injecting defaultJSON so that when the app tries to make the HTTP request, it triggers $httpBackend, which, in turn, uses the defaultJSON value.
  2. Did you notice the underscores surrounding the $httpBackend directive? This is a hack that allows us to use the dependency in multiple tests. You can find more information on this from the official documentation.
  3. Finally, we’re using an afterEach block to check that we’re not missing any HTTP requests in our tests via the verifyNoOutstandingExpectation() and verifyNoOutstandingRequest() methods. Again, you can read more about these methods from the Angular docs.

Test it out!

Routes

How about the routes, templates, and partials?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
describe('routes', function(){

  beforeEach(function () {
    module('myApp');
  });

  beforeEach(inject(function (_$httpBackend_, _$route_, _$location_, $rootScope) {
    $httpBackend = _$httpBackend_;
    $route = _$route_;
    $location = _$location_;
    $scope = $rootScope.$new();
  }));

  it('should load the one.html template', function(){
    $httpBackend.whenGET('partals/one.html').respond('...');
    $scope.$apply(function() {
      $location.path('/one');
    });
    assert.equal($route.current.templateUrl, 'partials/one.html');
    assert.equal($route.current.controller, 'TestOneController');
  });

});
  1. When the route is loaded, the current property is updated. We then test to ensure that the current controller and template are TestOneController and partials/one.html, respectively.
  2. Did you notice that we wrapped the route change inside the $apply callback? Since unit tests don’t run the full Angular app, we had to simulate it by triggering the digest cycle.
  3. Curious about WhenGET? Check out the Angular documentation. Take note of ExpectGET as well. Can you re-write the above test to use ExpectGET?

Make sure to run the tests one last time:

1
2
3
4
5
6
7
8
$ gulp unit
[05:20:07] Using gulpfile ~/angular-testing-tutorial/Gulpfile.js
[05:20:07] Starting 'unit'...
INFO [karma]: Karma v0.12.31 server started at http://localhost:9876/
INFO [launcher]: Starting browser Chrome
INFO [Chrome 41.0.2272 (Mac OS X 10.10.2)]: Connected on socket R5qQUcjswAbpcvMK6JKu with id 67365006
Chrome 41.0.2272 (Mac OS X 10.10.2): Executed 12 of 12 SUCCESS (0.16 secs / 0.027 secs)
[05:20:10] Finished 'unit' after 3.44 s

Conclusion

That’s it for unit tests. In the next part, we’ll test the entire application, front to back, using end-to-end (E2E) tests via Protractor.

Checkout the third tag, v3, to view all the completed unit tests:

1
$ git checkout tags/v3

Ready for more?

Try adding some Factories/Services and Filters to your app to continue practicing. Since the syntax is relatively the same for testing all parts of an Angular app, you should be able to extend your testing knowledge to both factories and filters. Take a look at this example for help getting started. Once you feel comfortable with factories, controllers, and filters, move on to testing more difficult components, like directives, resources, and animations. Good luck!

Comment below with questions.

Node With Docker - Continuous Integration and Delivery

Welcome.

This is a quick start guide for spinning up Docker containers that run NodeJS and Redis. We’ll look at a basic development workflow to manage the local development of an app, on Mac OS X, as well as continuous integration and delivery, step by step.

logo

This tutorial is ported from Docker in Action - Fitter, Happier, More Productive.

We’ll be using the following tools, technologies, and services in this post:

  1. NodeJS v0.12.0
  2. Express v3.4.8
  3. Redis v2.8.19
  4. Docker v1.5.0
  5. boot2docker v1.5.0
  6. Docker Compose v1.1.0
  7. Docker Hub
  8. CircleCI
  9. Digital Ocean
  10. Tutum

There’s slides too! Check them out here, if interested.

Docker?

Be sure you understand the Docker basics before diving into this tutorial. Check out the official “What is Docker?” guide for an excellent intro.

In short, with Docker, you can truly mimic your production environment on your local machine. No more having to debug environment specific bugs or worrying that your app will perform differently in production.

  1. Version control for infrastructure
  2. Easily distribute/recreate your entire development environment
  3. Build once, run anywhere – aka The Holy Grail!

Docker-specific terms

  • A Dockerfile is a file that contains a set of instructions used to create an image*.
  • An image is used to build and save snapshots (the state) of an environment.
  • A container is an instantiated, live image that runs a collection of processes.

Be sure to check out the Docker documentation for more info on Dockerfiles, images, and containers.

Local Setup

Let’s get your local development environment set up!

Get Docker

Follow the download instructions from the guide Installing Docker on Mac OS X to install both Docker and the official boot2docker package. boot2docker is a lightweight Linux distribution designed specifically to run Docker for Windows and Mac OS X users. In essence, it starts a small VM that’s configured to run Docker containers.

Once installed, run the following commands in your project directory to start boot2docker:

1
2
3
$ boot2docker init
$ boot2docker up
$ $(boot2docker shellinit)

Get the Project

Grab the base code from the repo, and add it to your project directory:

1
2
3
4
5
6
7
8
├── app
│   ├── Dockerfile
│   ├── index.js
│   ├── package.json
│   └── test
│       └── test.js
└── redis
    └── Dockerfile

Compose Up!

Docker Compose (Previously known as fig) is an orchestration framework that handles the building and running of multiple services, making it easy to link multiple services together running in different containers. Follow the installation instructions here, and then test it out to make sure all is well:

1
2
$ docker-compose --version
docker-compose 1.1.0

Now we just need to define the services - web (NodeJS) and persistence (Redis) in a configuration file called docker-compose.yml:

1
2
3
4
5
6
7
8
9
10
11
12
web:
  build: ./app
  volumes:
    - "app:/src/app"
  ports:
    - "80:3000"
  links:
   - redis
redis:
    build: ./redis
    ports:
        - "6379:6379"

Here we add the services that make up our basic stack:

  1. web: First, we build the image based on the instructions in the Dockerfile - where we setup our Node environment, create a volume, install the required dependencies, and fire up the app running on port 3000. Then we forward that port in the container to port 80 on the host environment - e.g., the boot2docker VM.
  2. redis: Next, the Redis service is again built from the instructions in the Dockerfile. Port 6379 is exposed and forwarded.

Profit

Run docker-compose up to build new images for the NodeJS/Express app and Redis services and then run both processes in new containers. Open your browser and navigate to the IP address associated the boot2docker VM (boot2docker ip). You should see the text, “You have viewed this page 1 times!” in your browser. Refresh. The page counter should increment.

Once done, kill the processes (Ctrl-C). Commit your changes locally, and then push to Github.

Next Steps

So, what did we accomplish?

We set up our local environment, detailing the basic process of building an image from a Dockerfile and then creating an instance of the image called a container. We then tied everything together with Docker Compose to build and connect different containers for both the NodeJS/Express app and Redis process.

Need the updated code? Grab it from the repo.

Next, let’s talk about Continuous Integration…

Continuous Integration

We’ll start with Docker Hub.

Docker Hub

Docker Hub “manages the lifecycle of distributed apps with cloud services for building and sharing containers and automating workflows”. It’s the Github for Docker images.

  1. Signup using your Github credentials.
  2. Set up a new automated build. And add your Github repo that you created and pushed to earlier. Just accept all the default options, expect for the “Dockerfile Location” - change that to “/app”. Once complete, Docker Hub will trigger an initial build.

Each time you push to Github, Docker Hub will generate a new build from scratch.

Docker Hub acts much like a continuous integration server since it ensures you do not cause a regression that completely breaks the build process when the code base is updated. That said, Docker Hub should be the last test before deployment to either staging or production so let’s use a true continuous integration server to fully test our code before it hits Docker Hub.

CircleCI

CircleCI is a CI platform that supports Docker.

Given a Dockerfile, CircleCI builds an image, starts a new container (or containers), and then runs tests inside that container.

  1. Sign up with your Github account.
  2. Create a new project using the Github repo you created.

Next we need to add a configuration file, called circle.yml, to the root folder of the project so that CircleCI can properly create the build.

1
2
3
4
5
6
7
8
9
10
11
12
machine:
  services:
    - docker

dependencies:
  override:
    - curl -L https://github.com/docker/compose/releases/download/1.1.0/docker-compose-`uname -s`-`uname -m` > /usr/local/bin/docker-compose chmod +x /usr/local/bin/docker-compose

test:
  override:
    - docker-compose run -d --no-deps web
    - cd app; mocha

Here, we install Docker Compose, then we create a new image, and run the container along with our unit tests.

Notice how we’re using the command docker-compose run -d --no-deps web, to run the web process, instead of docker-compose up. This is because CircleCI already has Redis running and available to us for our tests. So, we just need to run the web process.

Before we test this out, we need to change some settings on Docker Hub.

Docker Hub (redux)

Right now, each push to Github will create a new build. That’s not what we want. Instead, we want CircleCI to run tests against the master branch then after they pass(and only after they pass), a new build should trigger on Docker Hub.

Open your repository on Docker Hub, and make the following updates:

  1. Under Settings click Automated Build.
  2. Uncheck the Active box: “When active we will build when new pushes occur”. Save the changes.
  3. Then once again under Settings click Build Triggers.
  4. Change the status to on.
  5. Copy the example curl command – i.e., $ curl --data "build=true" -X POST https://registry.hub.docker.com/u/mjhea0/node-docker-workflow/trigger/84957124-2b85-410d-b602-b48193853b66/.

CircleCI (redux)

Back on CircleCI, let’s add that curl command as an environment variable:

  1. Within the Project Settings, select Environment variables.
  2. Add a new variable with the name “DEPLOY” and paste the curl command as the value.

Then add the following code to the bottom of the circle.yml file:

1
2
3
4
5
deployment:
  hub:
    branch: master
    commands:
      - $DEPLOY

This simple fires the $DEPLOY variable after our tests pass on the master branch.

Now, let’s test!

Profit!

Follow these steps…

  1. Create a new branch
  2. Make changes locally
  3. Issue a pull request
  4. Manually merge once the tests pass
  5. Once the second round passes, a new build is triggered on Docker Hub

What’s left? Deployment! Grab the updated code, if necessary.

Deployment

Let’s get our app running on Digital Ocean.

After you’ve signed up, create a new Droplet, choose “Applications” and then select the Docker Application.

Once setup, SSH into the server as the ‘root’ user:

1
$ ssh root@<some_ip_address>

Now you just need to clone the repo, install Docker compose, and then you can run your app:

1
2
3
4
$ git clone https://github.com/mjhea0/node-docker-workflow.git
$ curl -L https://github.com/docker/compose/releases/download/1.1.0/docker-compose-`uname -s`-`uname -m` > /usr/local/bin/docker-compose
$ chmod +x /usr/local/bin/docker-compose
$ docker-compose up -d

Sanity check. Navigate to your Droplet’s IP address in the browser. You should see your app.

Nice!

But what about continuous delivery? Instead of having to SSH into the server and clone the new code, the process should be part of our workflow so that once a new build is generated on Docker Hub, the code is updated on Digital Ocean automatically.

Enter Tutum.

Continuous Delivery

Tutum manages the orchestration and deployment of Docker images and containers. Setup is simple. After you’ve signed up (with Github), you need to add a Node, which is just a Linux host. We’ll use Digital Ocean.

Start by linking your Digital Ocean account within the “Account Info” area.

Now you can add a new Node. The process is straightforward, but if you need help, please refer to the official documentation. Just add a name, select a region, and then you’re good to go.

With a Node setup, we can now add a Stack of services - web and Redis, in our case - that make up our tech stack. Next, create a new file called tutum.yml, and add the following code:

1
2
3
4
5
6
7
8
9
10
11
12
web:
  image: mjhea0/node-docker-workflow
  autorestart: always
  ports:
    - "80:3000"
  links:
   - "redis:redis"
redis:
    image: redis
    autorestart: always
    ports:
        - "6379:6379"

Here, we are pulling the images from Docker Hub and building them just like we did with Docker Compose. Notice the difference here, between this file and the docker-compose.yml file. Here, we are not creating images, we’re pulling them in from Docker Hub. It’s essentially the same thing since the most updated build is on Docker Hub.

Now just create a new Stack, adding a name and uploading the tutum.yml file, and click “Create and deploy” to pull in the new images on the Node and then build and run the containers.

Once done, you can view your live app!

Note: You lose the “magic” of Tutum when running things in a single host, as we’re currently doing. In a real world scenario you’d want to deploy multiple web containers, load balance across them and have them live on different hosts, sharing a single REDIS cache. We may look at this in a future post, focusing solely on delivery.

Before we call it quits, we need to sync Docker Hub with Tutum so that when a new build is created on Docker Hub, the services are rebuilt and redeployed on Tutum - automatically!

Tutum makes this simple.

Under the Services tab, click the web service, and, finally, click the Webhooks tab. To create a new hook, simply add a name and then click Add. Copy the URL, and then navigate back to Docker Hub. Once there, click the Webhook link and add a new hook, pasting in the URL.

Now after a build is created on Docker Hub, a POST request is sent to that URL, which, in turn, triggers a redeploy on Tutum. Boom!

Conclusion

As always comment below if you have questions. If you manage a different workflow for continuous integration and delivery, please post the details below. Grab the final code from the repo.

See you next time!