bpineda / ng-starter-kit-w-tdd

Angular Starter Kit with Test Driven Development

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Angular and Angular CLI Demo

This tutorial shows you how to build a simple search and edit application using Angular and Angular CLI.

đź’ˇ
It appears you’re reading this document on GitHub. If you want a prettier view, install Asciidoctor.js Live Preview for Chrome, then view the raw document. Another option is to use the DocGist view.
Source Code

If you’d like to get right to it, the source is on GitHub. To run the app, use ng serve. To test it, run ng test. To run its integration tests, run ng e2e.

What you’ll build

You’ll build a simple web application with Angular CLI, a tool for Angular development. You’ll create an application with search and edit features.

What you’ll need

đź’ˇ
Angular Augury is a Google Chrome Dev Tools extension for debugging Angular applications. I haven’t needed it much myself, but I can see how it might come in handy.

Create your project

Create a new project using the ng new command:

ng new ng-demo

This will create a ng-demo project and run npm install in it. It takes about a minute to complete, but will vary based on your internet connection speed.

[mraible:~/dev] $ ng new ng-demo
installing ng
  create .editorconfig
  create README.md
  create src/app/app.component.css
  create src/app/app.component.html
  create src/app/app.component.spec.ts
  create src/app/app.component.ts
  create src/app/app.module.ts
  create src/assets/.gitkeep
  create src/environments/environment.prod.ts
  create src/environments/environment.ts
  create src/favicon.ico
  create src/index.html
  create src/main.ts
  create src/polyfills.ts
  create src/styles.css
  create src/test.ts
  create src/tsconfig.app.json
  create src/tsconfig.spec.json
  create src/typings.d.ts
  create .angular-cli.json
  create e2e/app.e2e-spec.ts
  create e2e/app.po.ts
  create e2e/tsconfig.e2e.json
  create .gitignore
  create karma.conf.js
  create package.json
  create protractor.conf.js
  create tsconfig.json
  create tslint.json
Successfully initialized git.
Installing packages for tooling via npm.
Installed packages for tooling via npm.
You can `ng set --global packageManager=yarn`.
Project 'ng-demo' successfully created.
[mraible:~] 46s $

You can see the what version of Angular CLI you’re using with ng --version.

$ ng --version
    _                      _                 ____ _     ___
   / \   _ __   __ _ _   _| | __ _ _ __     / ___| |   |_ _|
  / â–ł \ | '_ \ / _` | | | | |/ _` | '__|   | |   | |    | |
 / ___ \| | | | (_| | |_| | | (_| | |      | |___| |___ | |
/_/   \_\_| |_|\__, |\__,_|_|\__,_|_|       \____|_____|___|
               |___/
@angular/cli: 1.0.0
node: 6.9.5
os: darwin x64

Run the application

The project is configured with a simple web server for development. To start it, run:

ng serve

You should see a screen like the one below at http://localhost:4200.

Default Homepage
Figure 1. Default homepage

You can make sure your new project’s tests pass, run ng test:

$ ng test
...
Chrome 56.0.2924 (Mac OS X 10.12.2): Executed 3 of 3 SUCCESS (0.377 secs / 0.341 secs)

Add a search feature

To add a search feature, open the project in an IDE or your favorite text editor. For IntelliJ IDEA, use File > New Project > Static Web and point to the ng-demo directory.

The Basics

In a terminal window, cd into your project’s directory and run the following command. This will create a search component.

$ ng g component search
installing component
  create src/app/search/search.component.css
  create src/app/search/search.component.html
  create src/app/search/search.component.spec.ts
  create src/app/search/search.component.ts
  update src/app/app.module.ts

Open src/app/search/search.component.html and replace its default HTML with the following:

src/app/search/search.component.html
<h2>Search</h2>
<form>
  <input type="search" name="query" [(ngModel)]="query" (keyup.enter)="search()">
  <button type="button" (click)="search()">Search</button>
</form>
<pre>{{searchResults | json}}</pre>
Adding a Search Route
đź“Ž

In previous versions of CLI, you could generate a route and a component. However, since beta 8, route generation has been disabled. This will hopefully be re-enabled in a future release.

The Router documentation for Angular provides the information you need to setup a route to the SearchComponent you just generated. Here’s a quick summary:

In src/app/app.module.ts, add an appRoutes constant and import it in @NgModule:

src/app/app.module.ts
import { Routes, RouterModule } from '@angular/router';

const appRoutes: Routes = [
  { path: 'search', component: SearchComponent },
  { path: '', redirectTo: '/search', pathMatch: 'full' }
];

@NgModule({
  ...
  imports: [
    ...
    RouterModule.forRoot(appRoutes)
  ]
  ...
})
export class AppModule { }

In src/app/app.component.html, add a RouterOutlet to display routes.

src/app/app.component.html
<!-- Routed views go here -->
<router-outlet></router-outlet>

Now that you have routing setup, you can continue writing the search feature.

If you still have ng serve running, your browser should refresh automatically. If not, navigate to http://localhost:4200, and you should see the search form.

Search component
Figure 2. Search component

If you want to add CSS for this components, open src/app/search/search.component.css and add some CSS. For example:

src/app/search/search.component.css
:host {
  display: block;
  padding: 0 20px;
}

This section has shown you how to generate a new component to a basic Angular application with Angular CLI. The next section shows you how to create a use a JSON file and localStorage to create a fake API.

The Backend

To get search results, create a SearchService that makes HTTP requests to a JSON file. Start by generating a new service.

$ ng g service search
installing service
  create src/app/search.service.spec.ts
  create src/app/search.service.ts
  WARNING Service is generated but not provided, it must be provided to be used

Move the generated search.service.ts and its test to app/shared/search. You will need to create this directory.

Create src/assets/data/people.json to hold your data.

src/assets/data/people.json
[
  {
    "id": 1,
    "name": "Peyton Manning",
    "phone": "(303) 567-8910",
    "address": {
      "street": "1234 Main Street",
      "city": "Greenwood Village",
      "state": "CO",
      "zip": "80111"
    }
  },
  {
    "id": 2,
    "name": "Demaryius Thomas",
    "phone": "(720) 213-9876",
    "address": {
      "street": "5555 Marion Street",
      "city": "Denver",
      "state": "CO",
      "zip": "80202"
    }
  },
  {
    "id": 3,
    "name": "Von Miller",
    "phone": "(917) 323-2333",
    "address": {
      "street": "14 Mountain Way",
      "city": "Vail",
      "state": "CO",
      "zip": "81657"
    }
  }
]

Modify src/app/shared/search/search.service.ts and provide Http as a dependency in its constructor. In this same file, create a getAll() method to gather all the people. Also, define the Address and Person classes that JSON will be marshalled to.

src/app/shared/search/search.service.ts
import { Injectable } from '@angular/core';
import { Http, Response } from '@angular/http';
import 'rxjs/add/operator/map';

@Injectable()
export class SearchService {
  constructor(private http: Http) {}

  getAll() {
    return this.http.get('assets/data/people.json').map((res: Response) => res.json());
  }
}

export class Address {
  street: string;
  city: string;
  state: string;
  zip: string;

  constructor(obj?: any) {
    this.street = obj && obj.street || null;
    this.city = obj && obj.city || null;
    this.state = obj && obj.state || null;
    this.zip = obj && obj.zip || null;
  }
}

export class Person {
  id: number;
  name: string;
  phone: string;
  address: Address;

  constructor(obj?: any) {
    this.id = obj && Number(obj.id) || null;
    this.name = obj && obj.name || null;
    this.phone = obj && obj.phone || null;
    this.address = obj && obj.address || null;
  }
}

To make these classes available for consumption by your components, edit src/app/shared/index.ts and add the following:

export * from './search/search.service';
đź“Ž
If you’re wondering why you should use index.ts, see this Stack Overflow question.

In search.component.ts, add imports for these classes.

src/app/search/search.component.ts
import { Person, SearchService } from '../shared/index';

You can now add query and searchResults variables. While you’re there, modify the constructor to inject the SearchService.

src/app/search/search.component.ts
export class SearchComponent implements OnInit {
  query: string;
  searchResults: Array<Person>;

  constructor(private searchService: SearchService) {}

Then implement a search() method to call the service’s getAll() method.

src/app/search/search.component.ts
search(): void {
  this.searchService.getAll().subscribe(
    data => { this.searchResults = data; },
    error => console.log(error)
  );
}

At this point, you’ll likely see the following message in your browser’s console.

ORIGINAL EXCEPTION: No provider for SearchService!

To fix the "No provider" error from above, update app.component.ts to import the SearchService and add the service to the list of providers.

src/app/app.component.ts
import { SearchService } from './shared/index';

@Component({
  ...
  styleUrls: ['./app.component.css'],
  viewProviders: [SearchService]
})

Now clicking the search button should work. To make the results look better, remove the <pre> tag and replace it with a <table>.

src/app/search/search.component.html
<table *ngIf="searchResults">
  <thead>
  <tr>
    <th>Name</th>
    <th>Phone</th>
    <th>Address</th>
  </tr>
  </thead>
  <tbody>
  <tr *ngFor="let person of searchResults; let i=index">
    <td>{{person.name}}</td>
    <td>{{person.phone}}</td>
    <td>{{person.address.street}}<br/>
      {{person.address.city}}, {{person.address.state}} {{person.address.zip}}
    </td>
  </tr>
  </tbody>
</table>

Then add some additional CSS to improve its table layout.

src/app/search/search.component.css
table {
  margin-top: 10px;
  border-collapse: collapse;
}

th {
  text-align: left;
  border-bottom: 2px solid #ddd;
  padding: 8px;
}

td {
  border-top: 1px solid #ddd;
  padding: 8px;
}

Now the search results look better.

Search Results
Figure 3. Search results

But wait, you still don’t have search functionality! To add a search feature, add a search() method to SearchService.

src/app/shared/search/search.service.ts
import { Observable } from 'rxjs';

search(q: string): Observable<any> {
  if (!q || q === '*') {
    q = '';
  } else {
    q = q.toLowerCase();
  }
  return this.getAll().map(data => data.filter(item => JSON.stringify(item).toLowerCase().includes(q)));
}

Then refactor SearchComponent to call this method with its query variable.

src/app/search/search.component.ts
search(): void {
  this.searchService.search(this.query).subscribe(
    data => { this.searchResults = data; },
    error => console.log(error)
  );
}

Now search results will be filtered by the query value you type in.

This section showed you how to fetch and display search results. The next section builds on this and shows how to edit and save a record.

Add an edit feature

Modify search.component.html to add a click handler for editing a person.

src/app/search/search.component.html
<td><a href="" (click)="onSelect(person); false">{{person.name}}</a></td>
đź“Ž

The false in the (click) handler it necessary to keep the browser from following the blank href. You can also remove the href attribute, but then the cursor doesn’t indicate it’s a link properly when hovering.

Then add onSelect(person) to search.component.ts. You’ll need to import Router and set it as a local variable to make this work.

import { Router } from '@angular/router';
...
export class SearchComponent implements OnInit {
  ...

  constructor(private searchService: SearchService, private router: Router) { }

  ...

  onSelect(person: Person) {
    this.router.navigate(['/edit', person.id]);
  }
}

Run the following command to generate an EditComponent.

$ ng g component edit
installing component
  create src/app/edit/edit.component.css
  create src/app/edit/edit.component.html
  create src/app/edit/edit.component.spec.ts
  create src/app/edit/edit.component.ts
  update src/app/app.module.ts

Add a route for this component in app.module.ts:

src/app/app.module.ts
const appRoutes: Routes = [
  { path: 'search', component: SearchComponent },
  { path: 'edit/:id', component: EditComponent },
  { path: '', redirectTo: '/search', pathMatch: 'full' }
];

Update src/app/edit/edit.component.html to display an editable form. You might notice I’ve added id attributes to most elements. This is to make things easier when writing integration tests with Protractor.

src/app/edit/edit.component.html
<div *ngIf="person">
  <h3>{{editName}}</h3>
  <div>
    <label>Id:</label>
    {{person.id}}
  </div>
  <div>
    <label>Name:</label>
    <input [(ngModel)]="editName" name="name" id="name" placeholder="name"/>
  </div>
  <div>
    <label>Phone:</label>
    <input [(ngModel)]="editPhone" name="phone" id="phone" placeholder="Phone"/>
  </div>
  <fieldset>
    <legend>Address:</legend>
    <address>
      <input [(ngModel)]="editAddress.street" id="street"><br/>
      <input [(ngModel)]="editAddress.city" id="city">,
      <input [(ngModel)]="editAddress.state" id="state" size="2">
      <input [(ngModel)]="editAddress.zip" id="zip" size="5">
    </address>
  </fieldset>
  <button (click)="save()" id="save">Save</button>
  <button (click)="cancel()" id="cancel">Cancel</button>
</div>

Modify EditComponent to import model and service classes and to use the SearchService to get data.

src/app/edit/edit.component.ts
import { Component, OnInit, OnDestroy } from '@angular/core';
import { Address, Person, SearchService } from '../shared/index';
import { Subscription } from 'rxjs';
import { ActivatedRoute, Router } from '@angular/router';

@Component({
  selector: 'app-edit',
  templateUrl: './edit.component.html',
  styleUrls: ['./edit.component.css']
})
export class EditComponent implements OnInit, OnDestroy {
  person: Person;
  editName: string;
  editPhone: string;
  editAddress: Address;

  sub: Subscription;

  constructor(private route: ActivatedRoute,
              private router: Router,
              private service: SearchService) {
  }

  ngOnInit() {
    this.sub = this.route.params.subscribe(params => {
      let id = + params['id']; // (+) converts string 'id' to a number
      this.service.get(id).subscribe(person => {
        if (person) {
          this.editName = person.name;
          this.editPhone = person.phone;
          this.editAddress = person.address;
          this.person = person;
        } else {
          this.gotoList();
        }
      });
    });
  }

  ngOnDestroy() {
    this.sub.unsubscribe();
  }

  cancel() {
    this.router.navigate(['/search']);
  }

  save() {
    this.person.name = this.editName;
    this.person.phone = this.editPhone;
    this.person.address = this.editAddress;
    this.service.save(this.person);
    this.gotoList();
  }

  gotoList() {
    if (this.person) {
      this.router.navigate(['/search', {term: this.person.name} ]);
    } else {
      this.router.navigate(['/search']);
    }
  }
}

Modify SearchService to contain functions for finding a person by their id, and saving them. While you’re in there, modify the search() method to be aware of updated objects in localStorage.

src/app/shared/search/search.service.ts
search(q: string): Observable<any> {
  if (!q || q === '*') {
    q = '';
  } else {
    q = q.toLowerCase();
  }
  return this.getAll().map(data => {
    let results: any = [];
    data.map(item => {
      // check for item in localStorage
      if (localStorage['person' + item.id]) {
        item = JSON.parse(localStorage['person' + item.id]);
      }
      if (JSON.stringify(item).toLowerCase().includes(q)) {
        results.push(item);
      }
    });
    return results;
  });
}

get(id: number) {
  return this.getAll().map(all => {
    if (localStorage['person' + id]) {
      return JSON.parse(localStorage['person' + id]);
    }
    return all.find(e => e.id === id);
  });
}

save(person: Person) {
  localStorage['person' + person.id] = JSON.stringify(person);
}

You can add CSS to src/app/edit/edit.component.css if you want to make the form look a bit better.

src/app/edit/edit.component.css
:host {
  display: block;
  padding: 0 20px;
}

button {
  margin-top: 10px;
}

At this point, you should be able to search for a person and update their information.

Edit form
Figure 4. Edit component

The <form> in src/app/edit/edit.component.html calls a save() function to update a person’s data. You already implemented this above. The function calls a gotoList() function that appends the person’s name to the URL when sending the user back to the search screen.

src/app/edit/edit.component.ts
gotoList() {
  if (this.person) {
    this.router.navigate(['/search', {term: this.person.name} ]);
  } else {
    this.router.navigate(['/search']);
  }
}

Since the SearchComponent doesn’t execute a search automatically when you execute this URL, add the following logic to do so in its constructor.

src/app/search/search.component.ts
import { Router, ActivatedRoute } from '@angular/router';
import { Subscription } from 'rxjs';
...
  sub: Subscription;

  constructor(private searchService: SearchService, private router: Router, private route: ActivatedRoute) {
    this.sub = this.route.params.subscribe(params => {
      if (params['term']) {
        this.query = decodeURIComponent(params['term']);
        this.search();
      }
    });
  }

You’ll want to implement OnDestroy and define the ngOnDestroy method to clean up this subscription.

src/app/search/search.component.ts
import { Component, OnInit, OnDestroy } from '@angular/core';

export class SearchComponent implements OnInit, OnDestroy {
...
  ngOnDestroy() {
    this.sub.unsubscribe();
  }
}

After making all these changes, you should be able to search/edit/update a person’s information. If it works - nice job!

Form Validation

One thing you might notice is you can clear any input element in the form and save it. At the very least, the name field should be required. Otherwise, there’s nothing to click on in the search results.

To make name required, modify edit.component.html to add a required attribute to the name <input>.

src/edit/edit.component.html
<input [(ngModel)]="editName" name="name" id="name" placeholder="name" required/>

You’ll also need to wrap everything in a <form> element. Add <form> after the <h3> tag and close it before the last </div>. You’ll also need to add an (ngSubmit) handler to the form and change the save button to be a regular submit button.

src/edit/edit.component.html
<h3>{{editName}}</h3>
<form (ngSubmit)="save()" ngNativeValidate>
  ...
  <button type="submit" id="save">Save</button>
  <button (click)="cancel()" id="cancel">Cancel</button>
</form>

After making these changes, any field with a required attribute will be required.

Edit form with validation
Figure 5. Edit form with validation

In this screenshot, you might notice the address fields are blank. This is explained by the error in your console.

If ngModel is used within a form tag, either the name attribute must be set or the form control must be defined as 'standalone' in ngModelOptions.

Example 1: <input [(ngModel)]="person.firstName" name="first">
Example 2: <input [(ngModel)]="person.firstName" [ngModelOptions]="{standalone: true}">

To fix, add a name attribute to all the address fields. For example:

src/edit/edit.component.html
<address>
  <input [(ngModel)]="editAddress.street" name="street" id="street"><br/>
  <input [(ngModel)]="editAddress.city" name="city" id="city">,
  <input [(ngModel)]="editAddress.state" name="state" id="state" size="2">
  <input [(ngModel)]="editAddress.zip" name="zip" id="zip" size="5">
</address>

Now values should display in all fields and name should be required.

Edit form with names and validation
Figure 6. Edit form with names and validation

With Angular 2, this is all you’ll need to do. However, with Angular 4+, you need to a little more work to stop the form from submitting.

  • To display HTML5 validation messages, add the ngNativeValidate directive to the <form> tag.

  • If you want to provide your own validation messages:

    • Add #editForm="ngForm" to the <form> element.

    • Add #name="ngModel" to the <input id="name"> element.

    • Add [disabled]="!editForm.form.valid" to the Save button.

    • Add the following under the name field to display a validation error.

<div [hidden]="name.valid || name.pristine" style="color: red">
  Name is required
</div>

To learn more about forms and validation, see Angular forms documentation.

Testing

Now that you’ve built an application, it’s important to test it to ensure it works. 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 application grows.

đź’ˇ

If you didn’t complete the previous section, you can clone the ng-demo repository and checkout the test-start branch.

git clone https://github.com/mraible/ng-demo.git
cd ng-demo && git checkout test-start

In this section, you’ll learn to use Jasmine for unit testing controllers and Protractor for integration testing. Angular’s testing documentation lists good reasons to test, but doesn’t currently have many examples.

Fix the AppComponent test

If you run ng test, you’ll likely received an error:

Chrome 55.0.2883 (Mac OS X 10.12.2) AppComponent should create the app FAILED
	'router-outlet' is not a known element:

This happens because the test is unaware of Angular’s router. To fix this, import RouterTestingModule in app.component.spec.ts:

src/app/app.component.spec.ts
import { RouterTestingModule } from '@angular/router/testing';

describe('AppComponent', () => {
  beforeEach(async(() => {
    TestBed.configureTestingModule({
      declarations: [
        AppComponent
      ],
      imports: [RouterTestingModule]
    }).compileComponents();
  }));

You’ll also get failures for the components and service you created. These failures will be solved as you complete the section below.

đź’ˇ
You can use x and f prefixes Jasmine’s describe and it functions to exclude only run only a particular test.

Unit test the SearchService

Modify src/app/shared/search/search.service.spec.ts and setup the test’s infrastructure using MockBackend and BaseRequestOptions.

src/app/shared/search/search.service.spec.ts
import { TestBed, inject, tick, fakeAsync } from '@angular/core/testing';
import { SearchService } from './search.service';
import { BaseRequestOptions, Http, ConnectionBackend, Response, ResponseOptions } from '@angular/http';
import { MockBackend } from '@angular/http/testing';

describe('SearchService', () => {
  beforeEach(() => {

    TestBed.configureTestingModule({
      providers: [SearchService,
        {
          provide: Http, useFactory: (backend: ConnectionBackend, defaultOptions: BaseRequestOptions) => {
          return new Http(backend, defaultOptions);
        }, deps: [MockBackend, BaseRequestOptions]
        },
        {provide: MockBackend, useClass: MockBackend},
        {provide: BaseRequestOptions, useClass: BaseRequestOptions}
      ]
    });
  });
  ...

If you run ng test, you will likely see some errors about the test stubs that Angular CLI created for you. You can ignore these for now.

Chrome 55.0.2883 (Mac OS X 10.12.2) EditComponent should create FAILED
	Can't bind to 'ngModel' since it isn't a known property of 'input'. ("

Chrome 55.0.2883 (Mac OS X 10.12.2) SearchComponent should create FAILED
	Can't bind to 'ngModel' since it isn't a known property of 'input'. ("<h2>Search</h2>

Add the first test of getAll() to search.service.spec.ts. 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 beforeEach.

src/app/shared/search/search.service.spec.ts
it('should retrieve all search results',
  inject([SearchService, MockBackend], fakeAsync((searchService: SearchService, mockBackend: MockBackend) => {
    let res: Response;
    mockBackend.connections.subscribe(c => {
      expect(c.request.url).toBe('assets/data/people.json');
      const 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');
  }))
);

Notice that tests continually run as you add them when using ng test. You can run tests once by using ng test --watch=false or ng test -sr. Add a couple more tests for filtering by search term and fetching by id.

src/app/shared/search/search.service.spec.ts
it('should filter by search term',
  inject([SearchService, MockBackend], fakeAsync((searchService: SearchService, mockBackend: MockBackend) => {
    let res;
    mockBackend.connections.subscribe(c => {
      expect(c.request.url).toBe('assets/data/people.json');
      const 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) => {
    let res;
    mockBackend.connections.subscribe(c => {
      expect(c.request.url).toBe('assets/data/people.json');
      const 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');
  }))
);

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/app/shared/search/mocks/search.service.ts and populate it with spies for each method, as well as methods to set the response and subscribe to results.

src/app/shared/search/mocks/search.service.ts
import { SpyObject } from './helper';
import { SearchService } from '../search.service';
import Spy = jasmine.Spy;

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

  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: any) {
    callback(this.fakeResponse);
  }

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

In this same directory, create a helper.ts class to implement the SpyObject that MockSearchService extends.

src/app/shared/search/mocks/helper.ts
/// <reference path="../../../../../node_modules/@types/jasmine/index.d.ts"‌​/>

export interface GuinessCompatibleSpy extends jasmine.Spy {
  /** By chaining the spy with and.returnValue, all calls to the function will return a specific
   * value. */
  andReturn(val: any): void;
  /** By chaining the spy with and.callFake, all calls to the spy will delegate to the supplied
   * function. */
  andCallFake(fn: Function): GuinessCompatibleSpy;
  /** removes all recorded calls */
  reset();
}

export class SpyObject {
  static stub(object = null, config = null, overrides = null) {
    if (!(object instanceof SpyObject)) {
      overrides = config;
      config = object;
      object = new SpyObject();
    }

    let m = {};
    Object.keys(config).forEach((key) => m[key] = config[key]);
    Object.keys(overrides).forEach((key) => m[key] = overrides[key]);
    for (let key in m) {
      object.spy(key).andReturn(m[key]);
    }
    return object;
  }

  constructor(type = null) {
    if (type) {
      for (let prop in type.prototype) {
        let m = null;
        try {
          m = type.prototype[prop];
        } catch (e) {
          // As we are creating spys for abstract classes,
          // these classes might have getters that throw when they are accessed.
          // As we are only auto creating spys for methods, this
          // should not matter.
        }
        if (typeof m === 'function') {
          this.spy(prop);
        }
      }
    }
  }

  spy(name) {
    if (!this[name]) {
      this[name] = this._createGuinnessCompatibleSpy(name);
    }
    return this[name];
  }

  prop(name, value) { this[name] = value; }

  /** @internal */
  _createGuinnessCompatibleSpy(name): GuinessCompatibleSpy {
    const newSpy: GuinessCompatibleSpy = <any>jasmine.createSpy(name);
    newSpy.andCallFake = <any>newSpy.and.callFake;
    newSpy.andReturn = <any>newSpy.and.returnValue;
    newSpy.reset = <any>newSpy.calls.reset;
    // revisit return null here (previously needed for rtts_assert).
    newSpy.and.returnValue(null);
    return newSpy;
  }
}

Alongside, create routes.ts to mock Angular’s Router and ActivatedRoute.

src/app/shared/search/mocks/routes.ts
import { ActivatedRoute, Params } from '@angular/router';
import { Observable } from 'rxjs';

export class MockActivatedRoute extends ActivatedRoute {
  params: Observable<Params>;

  constructor(parameters?: { [key: string]: any; }) {
    super();
    this.params = Observable.of(parameters);
  }
}

export class MockRouter {
  navigate = jasmine.createSpy('navigate');
}

With mocks in place, you can TestBed.configureTestingModule() to setup SearchComponent to use these as providers.

src/app/search/search.component.spec.ts
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { SearchComponent } from './search.component';
import { MockSearchService } from '../shared/search/mocks/search.service';
import { MockActivatedRoute, MockRouter } from '../shared/search/mocks/routes';
import { SearchService } from '../shared/search/search.service';
import { ActivatedRoute, Router } from '@angular/router';
import { FormsModule } from '@angular/forms';

describe('SearchComponent', () => {
  let component: SearchComponent;
  let fixture: ComponentFixture<SearchComponent>;
  let mockSearchService: MockSearchService;
  let mockActivatedRoute: MockActivatedRoute;
  let mockRouter: MockRouter;

  beforeEach(() => {
    mockSearchService = new MockSearchService();
    mockActivatedRoute = new MockActivatedRoute({'term': 'peyton'});
    mockRouter = new MockRouter();

    TestBed.configureTestingModule({
      declarations: [SearchComponent],
      providers: [
        {provide: SearchService, useValue: mockSearchService},
        {provide: ActivatedRoute, useValue: mockActivatedRoute},
        {provide: Router, useValue: mockRouter}
      ],
      imports: [FormsModule]
    }).compileComponents();
  });

  beforeEach(() => {
    fixture = TestBed.createComponent(SearchComponent);
    component = fixture.componentInstance;
    fixture.detectChanges();
  });
});

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.

src/app/search/search.component.spec.ts
it('should search when a term is set and search() is called', () => {
  component = fixture.debugElement.componentInstance;
  component.query = 'M';
  component.search();
  expect(mockSearchService.searchSpy).toHaveBeenCalledWith('M');
});

it('should search automatically when a term is on the URL', () => {
  fixture.detectChanges();
  expect(mockSearchService.searchSpy).toHaveBeenCalledWith('peyton');
});

Update the test for EditComponent, 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.

src/app/edit/edit.component.spec.ts
import { MockSearchService } from '../shared/search/mocks/search.service';
import { EditComponent } from './edit.component';
import { TestBed } from '@angular/core/testing';
import { SearchService } from '../shared/search/search.service';
import { MockRouter, MockActivatedRoute } from '../shared/search/mocks/routes';
import { ActivatedRoute, Router } from '@angular/router';
import { FormsModule } from '@angular/forms';

describe('EditComponent', () => {
  let mockSearchService: MockSearchService;
  let mockActivatedRoute: MockActivatedRoute;
  let mockRouter: MockRouter;

  beforeEach(() => {
    mockSearchService = new MockSearchService();
    mockActivatedRoute = new MockActivatedRoute({'id': 1});
    mockRouter = new MockRouter();

    TestBed.configureTestingModule({
      declarations: [EditComponent],
      providers: [
        {provide: SearchService, useValue: mockSearchService},
        {provide: ActivatedRoute, useValue: mockActivatedRoute},
        {provide: Router, useValue: mockRouter}
      ],
      imports: [FormsModule]
    }).compileComponents();
  });

  it('should fetch a single record', () => {
    const fixture = TestBed.createComponent(EditComponent);

    const 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
    const editComponent = fixture.debugElement.componentInstance;
    expect(editComponent.editAddress.city).toBe('Denver');

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

You should see "Executed 9 of 9 SUCCESS (0.684 secs / 0.598 secs)" in the shell window that’s running ng test. 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 command in a terminal window.

ng e2e

All tests should pass.

$ ng e2e
** NG Live Development Server is running on http://localhost:49152 **
Hash: 24fc422df67845d66a49
Time: 15299ms
chunk    {0} polyfills.bundle.js, polyfills.bundle.js.map (polyfills) 158 kB {4} [initial] [rendered]
chunk    {1} main.bundle.js, main.bundle.js.map (main) 3.62 kB {3} [initial] [rendered]
chunk    {2} styles.bundle.js, styles.bundle.js.map (styles) 9.77 kB {4} [initial] [rendered]
chunk    {3} vendor.bundle.js, vendor.bundle.js.map (vendor) 2.37 MB [initial] [rendered]
chunk    {4} inline.bundle.js, inline.bundle.js.map (inline) 0 bytes [entry] [rendered]
webpack: Compiled successfully.
[11:34:42] I/file_manager - creating folder /Users/mraible/ng-demo/node_modules/webdriver-manager/selenium
[11:34:43] I/downloader - curl -o /Users/mraible/ng-demo/node_modules/webdriver-manager/selenium/chromedriver_2.28.zip https://chromedriver.storage.googleapis.com/2.28/chromedriver_mac64.zip
[11:34:44] I/update - chromedriver: unzipping chromedriver_2.28.zip
[11:34:45] I/update - chromedriver: setting permissions to 0755 for /Users/mraible/ng-demo/node_modules/webdriver-manager/selenium/chromedriver_2.28
[11:34:45] I/launcher - Running 1 instances of WebDriver
[11:34:45] I/direct - Using ChromeDriver directly...
Spec started

  ng-demo App
    âś“ should display message saying app works

Executed 1 of 1 spec SUCCESS in 1 sec.
[11:34:49] I/launcher - 0 instance(s) of WebDriver still running
[11:34:49] I/launcher - chrome #01 passed

Testing the search feature

Create end-to-end tests in e2e/search.e2e-spec.ts to verify the search feature works. Populate it with the following code:

e2e/search.e2e-spec.ts
import { browser, element, by } from 'protractor';

describe('Search', () => {

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

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

  it('should allow searching', () => {
    const searchButton = element(by.css('button'));
    const searchBox = element(by.css('input'));
    searchBox.sendKeys('M');
    searchButton.click().then(() => {
      let list = element.all(by.css('app-search table tbody tr'));
      expect(list.count()).toBe(3);
    });
  });
});

Testing the edit feature

Create a e2e/edit.e2e-spec.ts test to verify the EditComponent renders a person’s information and that their information can be updated.

e2e/edit.e2e-spec.ts
import { browser, element, by } from 'protractor';

describe('Edit', () => {

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

  const name = element(by.id('name'));
  const street = element(by.id('street'));
  const 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 () {
    const save = element(by.id('save'));
    name.sendKeys(' Won!');
    save.click();
    // verify one element matched this change
    const list = element.all(by.css('app-search table tbody tr'));
    expect(list.count()).toBe(1);
  });
});

Run ng 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
Figure 7. Protractor success

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

You can see the test coverage of your project by running ng test -cc -sr and then opening coverage/index.html in your browser.

You might notice that the new components and service could use some additional coverage. If you feel the need to improve this coverage, please create a pull request!

Test coverage
Figure 8. Test coverage

Continuous Integration

At the time of this writing, Angular CLI did not have any continuous integration support. This section shows you how to setup continuous integration with Travis CI and Jenkins.

Travis CI

If you’ve checked in your project to GitHub, you can use Travis CI.

  1. Login to Travis CI and enable builds for the GitHub repo you published the project to.

  2. Add the following .travis.yml in your root directory and git commit/push it. This will trigger the first build.

os:
  - linux
services:
  - docker
language: node_js
node_js:
  - "6.10.0"
addons:
  apt:
    sources:
    - google-chrome
    packages:
    - google-chrome-stable
cache:
  yarn: true
  directories:
    - $HOME/.yarn-cache
    - node_modules
branches:
  only:
  - master
before_install:
  - export CHROME_BIN=/usr/bin/google-chrome
  - export DISPLAY=:99.0
  - sh -e /etc/init.d/xvfb start
  # Repo for Yarn
  - curl -o- -L https://yarnpkg.com/install.sh | bash
  - export PATH=$HOME/.yarn/bin:$PATH
  - yarn global add @angular/cli
install:
  - yarn install
script:
  - ng test --watch false
  - ng e2e
notifications:
  webhooks:
    on_success: change
    on_failure: always
    on_start: false

Here is a build showing all unit and integration tests passing.

Jenkins

If you’ve checked your project into source control, you can use Jenkins to automate testing.

  1. Create a Jenkinsfile in the root directory and commit to master.

node {
    def nodeHome = tool name: 'node-6.9.5', type: 'jenkins.plugins.nodejs.tools.NodeJSInstallation'
    env.PATH = "${nodeHome}/bin:${env.PATH}"

    stage('check tools') {
        sh "node -v"
        sh "npm -v"
    }

    stage('checkout') {
        checkout scm
    }

    stage('npm install') {
        sh "npm install"
    }

    stage('unit tests') {
        sh "ng test --watch false"
    }

    stage('protractor tests') {
        sh "npm run e2e"
    }
}
  1. Download Jenkins 2 and install it on your local hard drive. Start it using java -jar jenkins.war.

  2. Login to Jenkins and create a new project with an SCM Pipeline. Point it at your project’s repository. Run a build.

Deployment

This section shows you how to deploy an Angular app to Cloud Foundry and Heroku.

Cloud Foundry

Create a Pivotal account and install the cf CLI. Then run the following commands to build and deploy your application.

ng build -prod --aot
cd dist && touch Staticfile
cf push ng-demo
đź“Ž
You might need to use an app name other than ng-demo.

Heroku

Create a Heroku account and install the heroku CLI. Then run the following commands to build and deploy your application.

  1. Run `heroku create`s

  2. Change package.json to have a different start script.

    "start": "http-server-spa dist index.html $PORT",
  3. Add preinstall and postinstall scripts to package.json:

    "preinstall": "npm install -g http-server-spa",
    "postinstall": "ng build -prod --aot"
  4. Run git push heroku master

  5. View the application in your browser with heroku open

Source code

A completed project with this code in it is available on GitHub at https://github.com/mraible/ng-demo.

Summary

I hope you’ve enjoyed this in-depth tutorial on how to get started with Angular and Angular CLI. Angular CLI takes much of the pain out of setting up an Angular project and using Typescript. I expect great things from Angular CLI, mostly because the Angular setup process can be tedious and CLI greatly simplifies things.

Bonus: Angular Material, Bootstrap 4, and Okta

If you’d like to see how to integrate Angular Material, Bootstrap 4, or authentication with Okta, this section is for you!

I’ve created branches to show how to integrate each of these libraries. Click on the links below to see each branch’s documentation.

About

Angular Starter Kit with Test Driven Development


Languages

Language:TypeScript 75.5%Language:HTML 14.6%Language:JavaScript 9.5%Language:CSS 0.4%