Angular 5 TDD – Upgrade complete

That’s right, I upgraded the game character TDD application from previous posts to Angular 5 and will share my experiences.

Prerequisites

This blogpost assumes the following has been installed on your machine.

Result

The following will be in place at the end of this post:

  • An app component (rendered when the application is served)
  • An app component spec file
  • A game character component
  • A game character component spec file
  • A game character service
  • A game character service spec file

The application can be found on GitHub here.

Introduction

Initially, I wanted to do a quick upgrade to Angular 5 and then post about doing more cool stuff with Docker, but there are some key aspects in upgrading the application that took more time than I initially hoped. I believe it would better to highlight these aspects in a separate post. Because we already have an Angular TDD application, the focus of this post will be on the upgrade process rather than doing things in a TDD way (creating tests before functionality).

Upgrading from Angular 1.X to 5 is more of a complete rewrite than an upgrade and when you have a more complex project (with deadlines and other fun stuff) you’ll likely not have room for this.

Creating an Angular 5 app

This is the easiest part. You just need NPM and do the following (source: https://cli.angular.io/):

A sample Angular 5 app with all the dependencies is built (approximately 180 MB) and run.
This is great, because we have a starting point which uses best practices and we can continue to build from this.

The app component

So I created a new app with “ng new game-app“ and this is how the folder structure looks like after I copied my files:

The file names have been renamed to use dash notation and TypeScript. The app component is the component that we get for free and we add the game-character component to the html:

The app component spec file

This comes for free too, but the tests will not succeed, because we added the game character component to its HTML, which is unknown when the spec file is run.
The component can be mocked as follows:

 

//Define the game character component mock
@Component({
  selector: 'game-character',
  template: ''
})
class MockGameCharacterComponent {
}

//Load the game character component mock into the test run
describe('AppComponent', () => {
  beforeEach(async(() => {
    TestBed.configureTestingModule({
      declarations: [
        AppComponent,
        MockGameCharacterComponent
      ],
    }).compileComponents();
  }));

The game character component

The controller has been axed. The component in Angular 5 contains both the rendering and the call to the service.

<h1>Game Characters</h1>
<ul class="game-characters">
    <li *ngFor="let gameCharacter of gameCharacters">
        <span class="badge">{{gameCharacter.id}}</span> {{gameCharacter.name}}
    </li>
</ul>
import { Component, OnInit } from '@angular/core';
import { GameCharacterService } from './game-character.service'
import { GameCharacter } from './game-character'

@Component({
  selector: 'game-character',
  templateUrl: './game-character.component.html',
  styleUrls: ['./game-character.component.css']
})
export class GameCharacterComponent implements OnInit {
  gameCharacters: GameCharacter[];
  title = 'Game Characters App';
  error = null;

  constructor(private gameCharacterService: GameCharacterService) { }

  ngOnInit() {
    this.getGameCharacterData();
  }

  getGameCharacterData(): void {
    this.gameCharacterService.getGameCharacters()
      .subscribe(
      gameCharacters => this.gameCharacters = gameCharacters,
      err => this.error = err
      );
  }
}

Although the code looks different than in Angular 1.X, apart from the “title” property the structure is the same. Angular 5 uses RxJS (Reactive extensions for JavaScript), which is a library to handle asynchronous data streams. This replaces the $q syntax (promises) that we had in Angular 1.

The game character component spec file

One of the nice things about TypeScript is that we can define objects to be of a certain type as can be seen in the makeGameCharacters function.

//An array of GameCharacter objects
const makeGameCharacters = () => [
  {
    "id": 9460,
    "name": "Janus",
    "created_at": 1472328002062,
    "updated_at": 1472328002125,
    "slug": "janus",
    "url": "https://www.igdb.com/characters/janus",
    "people": [
      80788
    ],
    "games": [
      9498
    ]
  }
] as GameCharacter[];

Next, lets define the test initializer:

beforeEach(async(() => {
    gameCharacterServiceStub = {
      getGameCharacters(): Observable<GameCharacter[]> {
        return of(makeGameCharacters());
      }
    };
    TestBed.configureTestingModule({
      declarations: [
        GameCharacterComponent
      ],
      providers: [{ provide: GameCharacterService, useValue: gameCharacterServiceStub }]
    }).compileComponents();
  }));

Similar to the test in Angular 1, we need to define a service stub (in Angular 1, we did this with bard.mockService) and mock the getGameCharacters method. It will return an Observable (RxJS) GameCharacter array with the array returned by the makeGameCharacters method.

The Angular TestBed is used to configure and initialize the test suite’s environment. This is similar to a module definition in Angular 5. In the providers section, a reference is being made to the real game character service, but the service is replaced by the service stub that mocks the getGameCharacters data.

In the previous version we had a unit test that checked if the data property is set in the controller. This translated to the following:

it('should contain character data', async(() => {
    const fixture = TestBed.createComponent(GameCharacterComponent);
    const gameCharacterComponent = fixture.debugElement.componentInstance;
    fixture.detectChanges();

    expect(gameCharacterComponent.gameCharacters).toBeDefined();
  }));

The TestBed can also be used to create components and services. An instance of the component (which contains the loaded properties) can be retrieved using the componentInstance member.
We could do a little better by expecting the rendered value of the data to match a value:

it('should render game character data', function () {
    const fixture = TestBed.createComponent(GameCharacterComponent);
    fixture.detectChanges();
    const compiled = fixture.debugElement.nativeElement;
    expect(compiled.querySelector('li').textContent).toContain('Janus');
  });

For this, the nativeElement member will provide the rendered DOM output.

The other test (catch an error, if the service request is erroneous) is trickier and took me a while to figure out.

it('should return an error, when the service fails to return data', function () {
    // UserService from the root injector
    let gameCharacterServiceStub = TestBed.get(GameCharacterService);

    //Return an error
    gameCharacterServiceStub.getGameCharacters = function (): Observable<GameCharacter> {
      return Observable.throw(new Error('Whoops, cannot retrieve character data'));
    }

    const fixture = TestBed.createComponent(GameCharacterComponent);
    const gameCharacterComponent = fixture.debugElement.componentInstance;
    fixture.detectChanges();
    expect(gameCharacterComponent.error).toBeTruthy();
  });

The Angular documentation advises to use the RxJS Observable class to fetch async data with an http request. Since we are doing TypeScript, we can strongly type the return value to be Observable.
In the case we want to return an error, to see if the component is correctly handling this error, we need to use Observable.throw.
Since the Angular CLI (which I used to create the new project) does not know the method .throw, we need to explicitly import this by the following line (thank you acdcjunior, http://bit.ly/2AVbUmE):

import 'rxjs/add/observable/throw';

This unit test eventually will not be used, since the game character service logs an error to the console in case a request returns an error. Still, I believe it is good practice to fill in the second argument (the error case) of an asynchronous return call.

The game character service

The Angular docs cover a large area and asynchronous services are part of them. I used this Plunkr example referred to by the Angular HTTP tutorial.

The result is the following:

const httpOptions = {
    headers: new HttpHeaders({ 
      'Accept': 'application/json',
      'X-Mashape-Key': '[It`s a secret]'
    })
  };

@Injectable()
export class GameCharacterService {
    private gameCharacterUrl = 'https://igdbcom-internet-game-database-v1.p.mashape.com/characters/?fields=*&limit=1';
    constructor(private httpClient: HttpClient) { }

    getGameCharacters(): Observable<GameCharacter[]> {
        return this.httpClient.get<GameCharacter[]>(this.gameCharacterUrl, httpOptions)
        .pipe(
          catchError(this.handleError('getGameCharacters', [])),
          tap(gameCharacterData => console.log(gameCharacterData))
        );
    }

In the getGameCharacters function, two functions are piped. The first is a private catchError function (which logs an error to the console) and a tap function. The tap function is part of the RxJS operators and is used to implement a side effect. A side effect is an operation that is done prior to handing the game character data out to the code that is subscribed to this observable sequence (http://bit.ly/2zelELq).

The game character service spec file

Unfortunately, the Angular docs don’t have a spec file for the http service based on the HttpClient that we used in the previous section.

There is a Plunkr defined in the Angular docs testing section that contains an HTTP service and an HTTP service spec file, but using these took me off into the wrong direction (a lot of errors and troubleshooting). After a while I saw that HTTP, HTTPModule, XHRBackend are all deprecated. According to the Angular mocking philosophy we need to implement the test suite using the new HTTPClient, HTTPClientModule, HttpClientTestingModule and HttpTestingController.

Use the classes described in the Angular mocking philosophy. I have tried using the Plunkr example and it took me several hours to figure out why it didn’t work.

The test initializer looks as follows:

beforeEach(async(() => {
        TestBed.configureTestingModule({
            imports: [HttpClientTestingModule],
            providers: [
                GameCharacterService
            ]
        })
            .compileComponents();
    }));

The testing module is used to intercept HTTP request and configure a testing backend as the HTTP backend used by HTTPClient.

it('should return correct data from IGDB.com after an authenticated request has been made', async(inject([HttpClient, HttpTestingController], (http: HttpClient, httpMock: HttpTestingController) => {
            service
                .getGameCharacters()
                .subscribe(gameCharacters => expect(gameCharacters.length).toBe(fakeGameCharacters.length, 'should have expected no. of gameCharacters'));

            const req = httpMock.expectOne('https://igdbcom-internet-game-database-v1.p.mashape.com/characters/?fields=*&limit=1');
            expect(req.request.method).toEqual('GET');

            //return character data
            req.flush(fakeGameCharacters);

            // Finally, assert that there are no outstanding requests.
            httpMock.verify();
        })));

Notice how the expect statement is put inside the subscribe method. The fakeGameCharacters property is the return value of the makeGameCharacters function. When the subscribe method is executed, the testing controller expects one request to be made to the URL. This request method is captured inside a constant and it is verified that this request is a GET request.
The flush function in the TestRequest class resolves the request by returning a body plus additional HTTP information. This means that after the flush, the expect statement in the subscribe function will be triggered and evaluated to true.

It is good practice to add the following code:

afterEach(inject([HttpTestingController], (httpMock: HttpTestingController) => {
        httpMock.verify();
    }));

This ensures that after each test there should not be any outstanding HTTP requests.
In the case an error returns from the IGDB server, this can be mocked as follows:

it('should return an error after an erroneous request has been made', async(inject([HttpClient, HttpTestingController], (http: HttpClient, httpMock: HttpTestingController) => {
            service
                .getGameCharacters()
                .subscribe(gameCharacters => expect(gameCharacters.length).toBe(0, 'An error occurred, the error handler returns an empty data array and logs the error to the console'));

            const req = httpMock.expectOne('https://igdbcom-internet-game-database-v1.p.mashape.com/characters/?fields=*&limit=1');
            expect(req.request.method).toEqual('GET');

            //return empty character data
            req.error(new ErrorEvent('Erroneous request'));

            // Finally, assert that there are no outstanding requests.
            httpMock.verify();
        })));

The only difference here being req.error instead of req.flush and the expectation that an empty character array is being returned.

The result (after running “ng test”) looks as follows:

I have left some of the default unit tests that I got from the ng new command and the Plunkr intact as they check for existence and instantiation mostly. All the tests from the Angular 1 app are there.

Summary

We have successfully upgraded (rewritten) an Angular 1 application to Angular 5. The unit tests have been upgraded to work with RxJS and the HTTPClient class.

Leave a Reply