Angular Unit-Tests: Einen HTTP-Request mocken

angular-unit-test-titel

Dieser Blog-Post ist der Beginn einer Serie an Blog-Posts. In meinem aktuellen Projekt entwickle ich eine mittlerweile recht umfangreiche Angular Anwendung. Professionelle Softwareentwicklung ist ohne entsprechende (automatisierte) Tests gar nicht möglich. Von daher veröffentliche ich einige Posts rund um das Thema automatisierter Tests in nächster Zeit.

Starten möchte ich mit einfachen Unit-Tests. Getestet werden sollen Services die Requests per http an Rest-Schnittstellen schicken. Denn hier stellt sich die Frage, wie man den Test so schreibt, „als ob" eine echte Rest-Schnittstelle kontaktiert wurde und entsprechend antwortet. Denn genau das soll nicht stattfinden. Man muss sich also darum kümmern, dass beim Absenden des Requests eine Fake-Antwort generiert wird. Diese Fake-Daten werden auch als Mocks bezeichnet.

Das fertigen Quellcode kann man sich auf GitHub anschauen. Einfach auschecken und die Tests starten. So hat man gleich ein lauffähiges Beispiel!

Der Umfang des Projektes ist schnell erklärt. Die Anwendung zeigt eine Komponente „TodoComponent" an. Diese lädt über den „TodoService" ein JSON-Objekt. Dieser beinhaltet einen einzelnen Eintrag aus einer Aufgabenliste. Dieser Eintrag wird dann angezeigt.

Den „TodoService" schauen wir uns nun genauer an.

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { Todo } from './todo';

@Injectable({
  providedIn: 'root'
})
export class TodoService {
  constructor(private http: HttpClient) {}

  public getTodo(id: number): Observable<Todo> {
    if (!id) {
      throw new TypeError('Unable to load Todo');
    }

    return this.http
      .get('https://jsonplaceholder.typicode.com/todos/' + id)
      .pipe(
        map(response => {
          const transformedTodo: Todo = {
            todoId: response['id'],
            userId: response['userId'],
            title: response['title'],
            workDone: response['completed'],
            loadTime: Date.now()
          };
          return transformedTodo;
        })
      );
    }
}
 
Wie Eingangs erwähnt möchte ich nun verdeutlichen, wie wir für diesen Service passende Unit-Tests schreiben. Die angular-cli hat bei der Erzeugung des Services eine passende Datei für die Tests angelegt. Es folgt die vollständige „todo.service.spec.ts" Datei.
import { TestBed } from '@angular/core/testing';
import {
  HttpClientTestingModule,
  HttpTestingController
} from '@angular/common/http/testing';
import { TodoService } from './todo.service';
import { Todo } from './todo';

describe('TodoService', () => {
  beforeEach(() =>
    TestBed.configureTestingModule({
      imports: [HttpClientTestingModule],
      providers: []
    })
  );

  it('should be created', () => {
    const service: TodoService = TestBed.get(TodoService);
    expect(service).toBeTruthy();
  });

  it('should perform a mocked http request', (done: DoneFn) => {
    const service: TodoService = TestBed.get(TodoService);
    const httpMock: HttpTestingController = TestBed.get(HttpTestingController);

    const mockResponse = {
      userId: 1,
      id: 2,
      title: 'Title',
      completed: false
    };

    service.getTodo(1).subscribe((todo: Todo) => {
      expect(todo).toBeTruthy();
      expect(todo.userId).toBe(mockResponse.userId);
      expect(todo.todoId).toBe(mockResponse.id);
      expect(todo.title).toBe(mockResponse.title);
      expect(todo.workDone).toBe(mockResponse.completed);
      expect(todo.loadTime).toBeTruthy();
      done();
    });

    const mockRequest = httpMock.expectOne(
      'https://jsonplaceholder.typicode.com/todos/1'
    );
    mockRequest.flush(mockResponse);
  });
});
 

Das Mocken eines http-Requests wollen wir uns im Folgenden näher anschauen.

Zuerst definieren wir uns ein Objekt, dass unsere nichtexistierende Rest-Schnitstelle zurückliefern soll. Der Attributname ist „mockResponse".

 const mockResponse = {
  userId: 1,
  id: 2,
  title: 'Title',
  completed: false
}; 

Natürlich muss die zu testende Methode „getTodo" aufgerufen werden. Da diese Methode ein Observable zurückliefert, ist ein „subscribe" erforderlich. Dadurch haben wir die Möglichkeit die gemockte Antwort der Schnittstelle auf Korrektheit zu überprüfen. Da der Service nicht einfach das gemockte JSON zurückliefert, können wir hier zum Beispiel schauen, ob die Transformierung fehlerfrei erfolgt ist. Durch das „subscribe" ist der Request abgeschickt. Allerdings wird kein echter http-Request abgeschickt, sondern der „HttpTestingController" kümmert sich darum, dass eine gemockte Antwort geliefert wird.

service.getTodo(1).subscribe((todo: Todo) => {
  expect(todo).toBeTruthy();
  expect(todo.userId).toBe(mockResponse.userId);
  expect(todo.todoId).toBe(mockResponse.id);
  expect(todo.title).toBe(mockResponse.title);
  expect(todo.workDone).toBe(mockResponse.completed);
  expect(todo.loadTime).toBeTruthy();
  done();
}); 

Wir müssen also veranlassen, dass auf den Request geantwortet wird. Wir sagen dem Objekt der Klasse „HttpTestingController", dass ein Request mit der angegeben URL abgeschickt worden sein muss. Dies ist ja durch das „subscribe" an dem Rückgabewert der „getTodo" Methode erfolgt. Es findet also eine Überprüfung statt, ob der Request an die erwartete URL geschickt wurde.

const mockRequest = httpMock.expectOne(
  'https://jsonplaceholder.typicode.com/todos/1'
); 

Zu guter Letzt wird der Request mit der „flush" Methode beantwortet. Der oben beschriebene „subscribe"-Block wird ausgeführt und die „expect"-Angaben werden überprüft.

mockRequest.flush(mockResponse); 

Eine wichtige Sache fehlt noch. Im Kopf der Test-Definition befindet sich die Angabe „done: DoneFn". Ist diese Angabe vorhanden, muss die „done()"-Methode innerhalb unseres Tests aufgerufen werden. Ansonsten schlägt der Test fehl. Den Methodenaufruf packen wir in den „subscribe"-Block. Nun wird garantiert, dass das Observable auch wirklich beantwortet wird und somit die „expect"-Angaben im „subscribe"-Block wirklich ausgeführt wurden.

That's it! Es gibt also Unit-Tests, die folgende Dinge überprüfen:

  • Kann der Service mit invaliden/falschen Übergabeparametern umgehen?
  • Geht der Request an die richtige URL?
  • Liefert der Service bei einer fest definierten Antwort das erwartete Ergebnis?
  • Wird das Observable auf jeden Fall beantwortet?

Durch die beschriebe Vorgehensweise haben wir noch viele weitere Möglichkeiten unsere http-Requests zu überprüfen! Zum Beispiel wurde bisher der http-Status-Code noch nicht überprüft. Aber auch die Angabe von http-Headern oder die korrekte Übertragung von Authentifizierungsinformationen könnte überprüft werden.

Das und viele andere Dinge möchte ich gerne in nachfolgenden Blog-Posts näher erläutern. Nun könnt ihr euch das GitHub Repository auschecken und selber ausprobieren. Viel Spaß und Erfolg!

By accepting you will be accessing a service provided by a third-party external to https://blog.ordix.de/