Matt RaibleMatt Raible is a Web Developer and Java Champion. Connect with him on LinkedIn.

The Angular Mini-Book The Angular Mini-Book is a guide to getting started with Angular. You'll learn how to develop a bare-bones application, test it, and deploy it. Then you'll move on to adding Bootstrap, Angular Material, continuous integration, and authentication.

Spring Boot is a popular framework for building REST APIs. You'll learn how to integrate Angular with Spring Boot and use security best practices like HTTPS and a content security policy.

For book updates, follow @angular_book on Twitter.

The JHipster Mini-Book The JHipster Mini-Book is a guide to getting started with hip technologies today: Angular, Bootstrap, and Spring Boot. All of these frameworks are wrapped up in an easy-to-use project called JHipster.

This book shows you how to build an app with JHipster, and guides you through the plethora of tools, techniques and options you can use. Furthermore, it explains the UI and API building blocks so you understand the underpinnings of your great application.

For book updates, follow @jhipster-book on Twitter.

10+ YEARS


Over 10 years ago, I wrote my first blog post. Since then, I've authored books, had kids, traveled the world, found Trish and blogged about it all.

Testing AngularJS Applications

This article is the second in a series about learning AngularJS. It describes how to test a simple AngularJS application. In a previous article, Getting Started with AngularJS, I showed how to develop a simple search and edit feature.

What you'll learn

You'll learn to use Jasmine for unit testing controllers and Protractor for integration testing. Angular's documentation has a good developer's guide to unit testing if you'd like more information on testing and why it's important.

The best reason for writing tests is to automate your testing. Without tests, you'll likely be testing manually. This manual testing will take longer and longer as your codebase grows.

What you'll need

  • About 15-30 minutes
  • A favorite text editor or IDE. We recommend IntelliJ IDEA.
  • Git installed.
  • Node.js and NPM installed.

Get the tutorial project

Clone the angular-tutorial repository using git and install the dependencies.

git clone https://github.com/mraible/angular-tutorial.git
cd angular-tutorial
npm install

If you haven't completed the Getting Started with AngularJS tutorial, you should peruse it so you understand how this application works. You can also simply start the app with "npm start" and view it in your browser at http://localhost:8000/app/.

Test the SearchController

  1. Create app/search/search_test.js and populate it with the basic test infrastructure. This code sets up a mock SearchService that has the first function we want to test: query(term). It uses $provide to override the default SearchService. Angular unit-test controllers - mocking service inside controller was a useful question on Stack Overflow for figuring out how to mock services in controllers.

    'use strict';
    
    describe('myApp.search module', function() {
        var mockSearchService;
    
        beforeEach(module('myApp.search', function($provide) {
            mockSearchService = {query: function(term) {}};
            $provide.value("SearchService", mockSearchService);
        }));
    });
  2. Modify karma.conf.js (in the root directory) to add the search implementation and test.

    files : [
      ...
      'app/components/**/*.js',
      'app/search/search.js',
      'app/search/search_test.js',
      'app/view*/**/*.js'
    ],
  3. Add the first test to search_test.js. This test verifies that setting a search term and executing the search() function will call the service and return results. You can see the results returned from the service are mocked with deferred.resolve(). The deferred.resolve() call is how to handle promises in unit tests.

    describe('search by term', function() {
        var scope, rootScope, controller, deferred;
    
    	// setup the controller with dependencies.
        beforeEach(inject(function($rootScope, $controller, $q) {
            rootScope = $rootScope;
            scope = $rootScope.$new();
            controller = $controller('SearchController', {$scope: scope, SearchService: mockSearchService });
            deferred = $q.defer();
        }));
    
        it('should search when a term is set and search() is called', function() {
            spyOn(mockSearchService, 'query').andReturn(deferred.promise);
            scope.term = 'M';
            scope.search();
            deferred.resolve({data: {name: "Peyton Manning"}});
            rootScope.$apply();
            expect(scope.searchResults).toEqual({name: "Peyton Manning"});
        });
    });

    Related: Introduction to Unit Test: Spies is a good introduction to using spies in unit tests.

  4. Run the following command on the command line to start the Karma test runner. You can leave this process running and new tests will be run automatically. You can change the mocked data and expectation to see your test fail.

    npm test
    Running Tests from IntelliJ IDEA
    See Running Unit Tests on Karma to learn how to run your tests from IntelliJ IDEA.
  5. Add a test to verify a search occurs automatically when the term is in the URL. Notice how the code structure had to change a bit to handle $routeParams.

    describe('search by term automatically', function() {
        var scope, rootScope, controller, location, deferred;
    
        beforeEach(inject(function($rootScope, $controller, $q) {
            rootScope = $rootScope;
            scope = $rootScope.$new();
    
            // in this case, expectations need to be setup before controller is initialized
            var routeParams = {"term": "peyton"};
            deferred = $q.defer();
            spyOn(mockSearchService, 'query').andReturn(deferred.promise);
            deferred.resolve({data: {name: "Peyton Manning"}});
    
            controller = $controller('SearchController', {$scope: scope, $routeParams: routeParams, SearchService: mockSearchService });
        }));
    
        it('should search automatically when a term is on the URL', function() {
            rootScope.$apply();
            expect(scope.searchResults).toEqual({name: "Peyton Manning"});
        });
    });
  6. Add a test to verify the EditController works as expected.

    describe('edit person', function() {
        var scope, rootScope, controller, location, deferred;
    
        beforeEach(inject(function($rootScope, $controller, $q) {
            rootScope = $rootScope;
            scope = $rootScope.$new();
    
            // expectations need to be setup before controller is initialized
            var routeParams = {"id": "1"};
            deferred = $q.defer();
            spyOn(mockSearchService, 'fetch').andReturn(deferred.promise);
            deferred.resolve({data: {name: "Peyton Manning", address: {street: "12345 Blake Street", city: "Denver"}}});
    
            controller = $controller('EditController', {$scope: scope, $routeParams: routeParams, SearchService: mockSearchService });
        }));
    
        it('should fetch a single record', function() {
            rootScope.$apply();
            expect(scope.person.name).toBe("Peyton Manning");
            expect(scope.person.address.street).toBe("12345 Blake Street");
        });
    });

    After adding this test, you'll likely see the following error in your terminal.

    Chrome 40.0.2214 (Mac OS X 10.10.2) myApp.search module edit person should fetch a single record FAILED
    	fetch() method does not exist
    	TypeError: Cannot read property 'name' of undefined
    

    This happens because the mockSearchService does not have a fetch method defined. Modify the beforeEach() on line 7 to add this function.

    mockSearchService = {query: function(term) {}, fetch: function(id) {}};

Extra Credit

Create a test for saving a person. Here's a question on Stack Overflow that might help you verify the location after a save has been performed.

Test the Search Feature

To test if the application works end-to-end, you can write scenarios with Protractor. These are also known as integration tests, as they test the integration between all layers of your application.

To verify end-to-end tests work in the project before you begin, run the following command in one terminal window:

npm start

Then in another window, run the following to execute the tests:

npm run protractor
  1. Write your first integration test to verify you can navigate to /search and enter a search term to see results. Add the following to e2e-tests/scenarios.js:

    describe('search', function() {
      var searchTerm = element(by.model('term'));
      var searchButton = element(by.id('search'));
    
      beforeEach(function() {
        browser.get('index.html#/search');
      });
    
      it('should allow searching at /search', function() {
        searchTerm.sendKeys("M");
        searchButton.click().then(function() {
          expect(element.all(by.repeater('person in searchResults')).count()).toEqual(3);
        });
      });
    });

    The "searchTerm" variable represents the input field. The by.model() syntax binds to the "ng-model" attribute you defined in the HTML.

  2. Run "npm run protractor". This should fail with the following error.

    [launcher] Running 1 instances of WebDriver
    Selenium standalone server started at http://172.16.6.39:64230/wd/hub
    ...F
    Failures:
      1) my app search should allow searching at /search
       Message:
         NoSuchElementError: No element found using locator: By.id("search")
  3. To fix, you need to add an "id" attribute to the Search button in app/search/index.html.

    <form ng-submit="search()">
        <input type="search" name="search" ng-model="term">
        <button id="search">Search</button>
    </form>
    
  4. Run the tests again using "npm run protractor". They should all pass this time.
  5. Write another test to verify editing a user displays their information.

    describe('edit person', function() {
      var name = element(by.model('person.name'));
      var street = element(by.model('person.address.street'));
      var city = element(by.model('person.address.city'));
    
      beforeEach(function() {
        browser.get('index.html#/edit/1');
      });
    
      it('should allow viewing a person', function() {
        // getText() doesn't work with input elements, see the following for more information:
        // https://github.com/angular/protractor/blob/master/docs/faq.md#the-result-of-gettext-from-an-input-element-is-always-empty
        expect(name.getAttribute('value')).toEqual("Peyton Manning");
        expect(street.getAttribute('value')).toEqual("1234 Main Street");
        expect(city.getAttribute('value')).toEqual("Greenwood Village");
      });
    });

    Verify it works with "npm run protractor".

  6. Finally, write a test to verify you can save a person and their details are updated. Figuring out how to verify the URL after it changed was assisted by this issue.

    describe('save person', function() {
      var name = element(by.model('person.name'));
      var save = element(by.id('save'));
    
      beforeEach(function() {
        browser.get('index.html#/edit/1');
      });
    
      it('should allow updating a name', function() {
        name.sendKeys(" Updated");
        save.click().then(function() {
          // verify url set back to search results
          browser.driver.wait(function() {
            return browser.driver.getCurrentUrl().then(function(url) {
              expect(url).toContain('/search/Peyton%20Manning%20Updated');
              return url;
            });
          });
          // verify one element matched this change
          expect(element.all(by.repeater('person in searchResults')).count()).toEqual(1);
        });
      });
    });
  7. When you run this test with "npm run protractor", it should fail because there's no element with id="save" in app/search/edit.html. Add it to the Save button in this file and try again. You should see something similar to the following:

    Finished in 4.478 seconds
    6 tests, 9 assertions, 0 failures

Source Code

A completed project with this code in it is available on GitHub at https://github.com/mraible/angular-tutorial on the testing branch.

There are two commits that make the changes for the two main steps in this tutorial:

Summary

I hope you've enjoyed this quick-and-easy tutorial on testing AngularJS applications. The first couple AngularJS applications I developed didn't have tests and required a lot of manual testing to verify their quality. After learning that testing AngularJS apps is pretty easy, I now do it on all my projects. Hopefully this tutorial motivates you to do do the same.

Posted in The Web at Feb 02 2015, 10:11:56 AM MST Add a Comment
Comments:

Post a Comment:
  • HTML Syntax: Allowed