Matt RaibleMatt Raible is a Java Champion and Developer Advocate at Okta. developer.okta.com

The JHipster Mini-Book The JHipster Mini-Book is a guide to getting started with hip technologies today: AngularJS, 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 Angular 2 Applications

Click here to see an updated version of this tutorial that's been upgraded for Angular 2.0 RC1.

This article is the second in a series about learning Angular 2. It describes how to test a simple Angular 2 application. In a previous article, Getting Started with Angular 2, I showed how to develop a simple search and edit feature. In this tutorial, I did my best to keep the tests similar to last year's Testing AngularJS Applications so you can compare the code between AngularJS and Angular 2.

What you'll build

You'll learn to use Jasmine for unit testing controllers and Protractor for integration testing. Angular's documentation has a good 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. I recommend IntelliJ IDEA.
  • Git installed.
  • Node.js and npm installed. I recommend using nvm.

Get the tutorial project

Clone the angular2-tutorial repository using git and install its dependencies.

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

If you haven't completed the Getting Started with Angular 2 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:5555/.

Unit test the SearchService

Create src/shared/services/search.service.spec.ts and setup the test's infrastructure using MockBackend and BaseRequestOptions.

import {it, describe, expect, inject, fakeAsync, beforeEachProviders, tick} from 'angular2/testing';
import {MockBackend} from 'angular2/http/testing';
import {provide} from 'angular2/core';
import 'rxjs/add/operator/map';
import {Http, ConnectionBackend, BaseRequestOptions, Response, ResponseOptions} from 'angular2/http';

import {SearchService} from './search.service';

export function main() {
  describe('Search Service', () => {
    beforeEachProviders(() => {
      return [BaseRequestOptions, MockBackend, SearchService,
        provide(Http, {
          useFactory: (backend:ConnectionBackend, defaultOptions:BaseRequestOptions) => {
            return new Http(backend, defaultOptions);
          }, deps: [MockBackend, BaseRequestOptions]
        }),
      ];
    });
  });
}

If you run npm test, you'll get a failed build from a number of unused imports. You can fix those by adding the first test of getAll(). This test shows how MockBackend can be used to mock results and set the response.

When you are testing code that returns either a Promise or an RxJS Observable, you can use the fakeAsync helper to test that code as if it were synchronous. Promises are be fulfilled and Observables are notified immediately after you call tick().

The test below should be on the same level as beforeEachProviders.

it('should retrieve all search results',
  inject([SearchService, MockBackend], fakeAsync((searchService:SearchService, mockBackend:MockBackend) => {
    var res:Response;
    mockBackend.connections.subscribe(c => {
      expect(c.request.url).toBe('shared/data/people.json');
      let response = new ResponseOptions({body: '[{"name": "John Elway"}, {"name": "Gary Kubiak"}]'});
      c.mockRespond(new Response(response));
    });
    searchService.getAll().subscribe((response) => {
      res = response;
    });
    tick();
    expect(res[0].name).toBe('John Elway');
  }))
);

Running npm test should result in "10 tests completed". Add a couple more tests for filtering by search term and fetching by id.

it('should filter by search term',
  inject([SearchService, MockBackend], fakeAsync((searchService:SearchService, mockBackend:MockBackend) => {
    var res:Response;
    mockBackend.connections.subscribe(c => {
      expect(c.request.url).toBe('shared/data/people.json');
      let response = new ResponseOptions({body: '[{"name": "John Elway"}, {"name": "Gary Kubiak"}]'});
      c.mockRespond(new Response(response));
    });
    searchService.search('john').subscribe((response) => {
      res = response;
    });
    tick();
    expect(res[0].name).toBe('John Elway');
  }))
);

it('should fetch by id',
  inject([SearchService, MockBackend], fakeAsync((searchService:SearchService, mockBackend:MockBackend) => {
    var res:Response;
    mockBackend.connections.subscribe(c => {
      expect(c.request.url).toBe('shared/data/people.json');
      let response = new ResponseOptions({body: '[{"id": 1, "name": "John Elway"}, {"id": 2, "name": "Gary Kubiak"}]'});
      c.mockRespond(new Response(response));
    });
    searchService.search('2').subscribe((response) => {
      res = response;
    });
    tick();
    expect(res[0].name).toBe('Gary Kubiak');
  }))
);

If you want to have tests continually run as you add them, you can run the following commands in separate shell windows.

npm run build.test.watch
npm run karma.start

NOTE: See Running Unit Tests on Karma to learn how to run your tests from IntelliJ IDEA.

Unit test the SearchComponent

To unit test the SearchComponent, create a MockSearchProvider that has spies. These allow you to spy on functions to check if they were called.

Create src/shared/services/mocks/search.service.ts and populate it with spies for each method, as well as methods to set the response and subscribe to results.

import {provide} from 'angular2/core';
import {SpyObject} from 'angular2/testing_internal';

import {SearchService} from '../search.service';
import Spy = jasmine.Spy;

export class MockSearchService extends SpyObject {
  getAllSpy:Spy;
  getByIdSpy:Spy;
  searchSpy:Spy;
  saveSpy:Spy;
  fakeResponse;

  constructor() {
    super(SearchService);

    this.fakeResponse = null;
    this.getAllSpy = this.spy('getAll').andReturn(this);
    this.getByIdSpy = this.spy('get').andReturn(this);
    this.searchSpy = this.spy('search').andReturn(this);
    this.saveSpy = this.spy('save').andReturn(this);
  }

  subscribe(callback) {
    callback(this.fakeResponse);
  }

  setResponse(json: any): void {
    this.fakeResponse = json;
  }

  getProviders(): Array<any> {
    return [provide(SearchService, {useValue: this})];
  }
}

In this same directory, create routes.ts to mock Angular's Router, RouteParams and RouterProvider.

import {provide} from 'angular2/core';
import {
  ComponentInstruction,
  Router,
  RouteParams
} from 'angular2/router';
import {ResolvedInstruction} from 'angular2/src/router/instruction';
import {SpyObject} from 'angular2/testing_internal';

export class MockRouteParams extends SpyObject {
  private ROUTE_PARAMS = {};

  constructor() { super(RouteParams); }

  set(key: string, value: string) {
    this.ROUTE_PARAMS[key] = value;
  }

  get(key: string) {
    return this.ROUTE_PARAMS[key];
  }
}

export class MockRouter extends SpyObject {
  constructor() { super(Router); }
  isRouteActive(s) { return true; }
  generate(s) {
    return new ResolvedInstruction(new ComponentInstruction('detail', [], null, null, true, '0'), null, {});
  }
}

export class MockRouterProvider {
  mockRouter: MockRouter = new MockRouter();
  mockRouteParams: MockRouteParams = new MockRouteParams();

  setRouteParam(key: string, value: any) {
    this.mockRouteParams.set(key, value);
  }

  getProviders(): Array<any> {
    return [
      provide(Router, {useValue: this.mockRouter}),
      provide(RouteParams, {useValue: this.mockRouteParams}),
    ];
  }
}

With mocks in place, you can create a spec for SearchComponent that uses these as providers. Create a file at src/search/components/search.component.spec.ts and populate it with the following code.

import {
  it,
  describe,
  expect,
  injectAsync,
  beforeEachProviders,
  TestComponentBuilder,
} from 'angular2/testing';

import {MockRouterProvider} from '../../shared/services/mocks/routes';
import {MockSearchService} from '../../shared/services/mocks/search.service';

import {SearchComponent} from './search.component';

export function main() {
  describe('Search component', () => {
    var mockSearchService:MockSearchService;
    var mockRouterProvider:MockRouterProvider;

    beforeEachProviders(() => {
      mockSearchService = new MockSearchService();
      mockRouterProvider = new MockRouterProvider();

      return [
        mockSearchService.getProviders(), mockRouterProvider.getProviders()
      ];
    });
  });
}

Add two tests, one to verify a search term is used when it's set on the component and a second to verify search is called when a term is passed in as a route parameter.

it('should search when a term is set and search() is called', injectAsync([TestComponentBuilder], (tcb:TestComponentBuilder) => {
  return tcb.createAsync(SearchComponent).then((fixture) => {
    let searchComponent = fixture.debugElement.componentInstance;
    searchComponent.query = 'M';
    searchComponent.search();
    expect(mockSearchService.searchSpy).toHaveBeenCalledWith('M');
  });
}));

it('should search automatically when a term is on the URL', injectAsync([TestComponentBuilder], (tcb:TestComponentBuilder) => {
  mockRouterProvider.setRouteParam('term', 'peyton');
  return tcb.createAsync(SearchComponent).then((fixture) => {
    fixture.detectChanges();
    expect(mockSearchService.searchSpy).toHaveBeenCalledWith('peyton');
  });
}));

Add a spec for the EditComponent as well, verifying fetching a single record works. Notice how you can access the component directly with fixture.debugElement.componentInstance, or its rendered version with fixture.debugElement.nativeElement. Create a file at src/search/components/edit.component.spec.ts and populate it with the code below.

import {
  it,
  describe,
  expect,
  injectAsync,
  beforeEachProviders,
  TestComponentBuilder,
} from 'angular2/testing';

import {MockRouterProvider} from '../../shared/services/mocks/routes';
import {MockSearchService} from '../../shared/services/mocks/search.service';

import {EditComponent} from './edit.component';

export function main() {
  describe('Edit component', () => {
    var mockSearchService:MockSearchService;
    var mockRouterProvider:MockRouterProvider;

    beforeEachProviders(() => {
      mockSearchService = new MockSearchService();
      mockRouterProvider = new MockRouterProvider();

      return [
        mockSearchService.getProviders(), mockRouterProvider.getProviders()
      ];
    });

    it('should fetch a single record', injectAsync([TestComponentBuilder], (tcb:TestComponentBuilder) => {
      mockRouterProvider.setRouteParam('id', '1');
      return tcb.createAsync(EditComponent).then((fixture) => {
        let person = {name: 'Emmanuel Sanders', address: {city: 'Denver'}};
        mockSearchService.setResponse(person);

        fixture.detectChanges();
        // verify service was called
        expect(mockSearchService.getByIdSpy).toHaveBeenCalledWith(1);

        // verify data was set on component when initialized
        let editComponent = fixture.debugElement.componentInstance;
        expect(editComponent.editAddress.city).toBe('Denver');

        // verify HTML renders as expected
        var compiled = fixture.debugElement.nativeElement;
        expect(compiled.querySelector('h3')).toHaveText('Emmanuel Sanders');
      });
    }));
  });
}

You should see "? 20 tests completed" in the shell window that's running npm run karma.start. If you don't, try cancelling the command and restarting.

Integration test the search UI

To test if the application works end-to-end, you can write tests with Protractor. These are also known as integration tests, since 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 commands in three different console windows.

# npm run webdriver-update <- You will need to run this the first time
npm run webdriver-start
npm run serve.e2e
npm run e2e

You should receive an error stating that the "nav text for About" is incorrect.

Protractor nav test error

This happens because we added a Search link to the navbar and didn't update the test (in app.component.e2e.ts) that looks for the last child.

it('should have correct nav text for About', () => {
    expect(element(by.css('sd-app sd-navbar nav a:last-child')).getText()).toEqual('ABOUT');
});

Replace this test with the one below, and add a new one to verify the Search link is last.

it('should have correct nav text for About', () => {
  expect(element(by.css('sd-app sd-navbar nav a:nth-child(2)')).getText()).toEqual('ABOUT');
});

it('should have correct nav text for Search', () => {
  expect(element(by.css('sd-app sd-navbar nav a:last-child')).getText()).toEqual('SEARCH');
});

Now when you run npm run e2e, all specs should pass.

Testing the search feature

Create a new search.component.e2e.ts spec in the same directory as your SearchComponent. Add tests to verify elements are rendered correctly and search works. At the time of this writing, Protractor's by.model and by.repeater don't work with Angular 2. For this reason, I used by.css to verify the HTML renders as expected.

describe('Search', () => {

  beforeEach(() => {
    browser.get('/search');
  });

  it('should have an input and search button', () => {
    expect(element(by.css('sd-app sd-search form input')).isPresent()).toEqual(true);
    expect(element(by.css('sd-app sd-search form button')).isPresent()).toEqual(true);
  });

  it('should allow searching', () => {
    let searchButton = element(by.css('button'));
    let searchBox = element(by.css('input'));
    searchBox.sendKeys('M');
    searchButton.click().then(() => {
      // doesn't work as expected - results in 0
      //expect(element.all(by.repeater('person of searchResults')).count()).toEqual(3);
      var list = element.all(by.css('sd-search table tbody tr'));
      expect(list.count()).toBe(3);
    });
  });
});

Testing the edit feature

Create a edit.component.e2e.ts spec to verify the EditComponent renders a person's information and that you can update their information.

describe('Edit', () => {
 
  beforeEach(() => {
    browser.get('/edit/1');
  });
 
  let name = element(by.id('name'));
  let street = element(by.id('street'));
  let city = element(by.id('city'));
 
  it('should allow viewing a person', () => {
    expect(element(by.css('h3')).getText()).toEqual('Peyton Manning');
    expect(name.getAttribute('value')).toEqual('Peyton Manning');
    expect(street.getAttribute('value')).toEqual('1234 Main Street');
    expect(city.getAttribute('value')).toEqual('Greenwood Village');
  });
 
  it('should allow updating a name', function () {
    let save = element(by.id('save'));
    // send individual characters since sendKeys passes partial values sometimes
    // https://github.com/angular/protractor/issues/698
    ' Won!'.split('').forEach((c) => name.sendKeys(c));
    save.click();
    // verify one element matched this change
    var list = element.all(by.css('sd-search table tbody tr'));
    expect(list.count()).toBe(1);
  });
});

Run npm run e2e to verify all your end-to-end tests pass. You should see a success message similar to the one below in your terminal window.

Protractor success

If you made it this far and have all 13 specs passing - congratulations! You're well on your way to writing quality code with Angular 2 and verifying it works.

Source code

A completed project with this code in it is available on GitHub at https://github.com/mraible/angular2-tutorial. If you have ideas for improvements, please leave a comment or send a pull request.

I originally wrote this tutorial in Asciidoctor because it has a slick feature where you can include the source code from files rather than copying and pasting. Unfortunately, GitHub doesn't support includes. You can use DocGist to view this tutorial, but includes don't work there either.

If you'd like to see the Asciidoctor-generated version of this tutorial, you can install the gem, checkout the project from GitHub, and then run asciidoctor TESTING.adoc to produce a TESTING.html file.

Summary

I hope you've enjoyed this quick-and-easy tutorial on testing Angular 2 applications. You can see the test coverage of your project by running npm run serve.coverage. You'll notice that the new components and service could use some additional coverage. I'll leave that as a task for the reader.

Test Coverage

I learned a lot about testing from ng-book 2 and its Testing chapter. If you have any Angular 2 testing tips and tricks you'd like to share, I'd love to hear about them.

Posted in The Web at Mar 29 2016, 08:08:58 AM MDT 2 Comments
Comments:

Hi Matt! This is very cool, but can you explain the differences in karma.conf.js compared to a NG1 configuration and what's going on in the test-main.js file? TIA.

Posted by Craig Doremus on April 25, 2016 at 08:47 AM MDT #

Hey Craig! This is probably a better question for the angular2-seed developers, but I'll take a stab at it. The test-main.js holds the testing configuration information and uses SystemJS to setup default imports, setup base test providers and run the main module in each tests. See test-main.js#L30 for more information. I believe they do this to reduce the boilerplate code needed in each test.

Posted by Matt Raible on April 25, 2016 at 10:25 AM MDT #

Post a Comment:
  • HTML Syntax: Allowed