Angular Unit Tests: eine Service Abhängigkeit auflösen mit Hilfe eines Service-Stub

angular-unit-test

Eines meiner liebsten Aufgaben ist das Schreiben von Unit-Tests. Deshalb ein neuer Posts zu diesem Thema! Gerne könnt ihr euch meine anderen Artikel anschauen. Zum Beispiel habe ich erklärt, wie man einen HTTP-Request mockt oder eine Angular Komponente testet.

Allerdings ist das Thema Unit-Testing recht umfangreich. Von daher möchte ich mich in diesen Blog-Post mit einem weiteren spannenden Szenario beschäftigen.
Wir haben Angular Services, die miteinander in Verbindung stehen. Ein Service hat eine Abhängigkeit zu einem weiteren Service. Diese Abhängigkeit wird per Dependency Injection zur Laufzeit aufgelöst. Im weiteren Verlauf möchte ich euch erklären, wie ihr diese Abhängigkeit in Unit-Tests zwischen den beiden Services elegant und einfach auflösen könnt. Ein lauffähiges Beispiel findet ihr wie immer auf GitHub. Let's start!

Die Services 

Der Aufbau der Anwendung ist schnell erklärt. Der TodoService bietet eine Funktion an, mit der man eine Aufgabenliste per http-Request laden kann.

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

  public getTodos(): Observable<Todo[]> {
    return this.http.get<Todo[]>('https://jsonplaceholder.typicode.com/todos/');
  }

}
 
Konsumiert wird der ToDoService vom ManagementService. Dieser Service bietet eine Funktion an, um die Anzahl der Todo's zu liefern.
@Injectable({
  providedIn: 'root'
})
export class ManagementService {
  constructor(private todoService: TodoService) {}

  public countTodos(): Observable<number> {
    return this.todoService
      .getTodos()
      .pipe(map((todos: Todo[]) => todos.length));
  }
}
 

Der ManagementService ist also abhängig von dem ToDoService. Diese Abhängigkeit gilt es jetzt im Unit Tests der ManagementService Klasse aufzulösen. Einen Weg habe ich bereits in dem erwähnten Blog-Post aufgezeigt. Dort wurde mit Hilfe einer spy() Angabe die eigentliche Funktionalität einer Funktion überschrieben und stattdessen etwas anderes (ein Mock-Objekt) zurückgegeben. Das funktioniert wunderbar, hat aber auch Nachteile. Wir müssen uns darum kümmern, dass die Abhängigkeiten des ToDoService aufgelöst werden. In dem konkreten Fall musste die Abhängigkeit zum HttpClient mit Hilfe des HttpClientModule aufgelöst werden. Kein Problem, aber je mehr Abhängigkeiten ein Service hat, desto aufwendiger wird es diese Abhängigkeiten aufzulösen. Von daher zeige ich im Folgenden einen Weg, bei dem wir Abhängigkeiten komplett außen vor lassen können. Wir werden uns nicht darum kümmern müssen.

Der nun folgende Code-Ausschnitt zeigt den kompletten Test des ManagementService. Es soll geprüft werden, ob wirklich die richtige Anzahl an Todo's zurückgegeben werden, wenn die countTodos() Funktion aufgerufen wird.

import { TestBed } from '@angular/core/testing';
import { ManagementService } from './management.service';
import { TodoService } from './todo.service';
import { Todo } from './todo';
import { of } from 'rxjs';

const mockTodos: Todo[] = [
  { id: 1, userId: 1, title: 'MockTitle 1', completed: true },
  { id: 2, userId: 2, title: 'MockTitle 2', completed: false },
  { id: 3, userId: 3, title: 'MockTitle 3', completed: false }
];

describe('ManagementService', () => {
  beforeEach(() =>
    TestBed.configureTestingModule({
      providers: [
        {
          provide: TodoService,
          useFactory: () => {
            
            return {};
          }
        }
      ]
    })
  );

  it('should return the correct number of todos', (done: DoneFn) => {
    const service: ManagementService = TestBed.get(ManagementService);
    const stubbedTodoService: TodoService = TestBed.get(TodoService);

    // Initialisierung der Funktion, die durch den Aufruf von countTodos
    // aufgerufen wird.
    // Wird die getTodos Funktion aufgerufen, dann werden die gemockten
    // Daten als Observable zurueckgegeben.
    stubbedTodoService.getTodos = () => of(mockTodos);

    service.countTodos().subscribe((numberOfTodos: number) => {
      expect(numberOfTodos).toBe(mockTodos.length);
      done();
    });
  });
});
 

In Zeile 7-11 befindet sich unser Mock-Objekt. Auch in diesen Tests wollen wir natürlich keine echte REST-Schnittstelle ansprechen. Das sind also unsere "Ersatzdaten".

Als nächstes schauen wir uns die Konfiguration des Test-Moduls an. In Zeile 16 ist die providers Angabe zu finden. Dort werden alle Services aufgelistet, die während der Tests per Dependency Injection auflösbar sein sollen. Dort könnte man den TodoService aufnehmen. Problem wie bereits gesagt ist, dass dieser weitere Abhängigkeiten besitzt, die es aufzulösen gilt. Für unseren Fall ist das aber gar nicht notwendig. Eigentlich soll nur unser Mock-Objekt zurückgegeben werden, wenn die getTodos() Funktion des TodoService aufgerufen wird. Der HttpClient wird zu keinen Zeitpunkt während der Tests benötigt. Dazu benutzten wir als erstes die useFactory Angabe aus Zeile 19. Dadurch erreichen wir, dass wir selbst uns um die Initialisierung des ToDoService kümmern können. Wir erzeugen einfach ein komplett leeres Objekt, welches keine Abhängigkeit zum HttpClient besitzt. Und schon müssen wir uns keinerlei Gedanken mehr um irgendwelche Abhängigkeiten machen. Ein Service der nur mit den allernotwendigsten ausgestattete ist, wird auch als Stub (in Deutsch "Stummel") bezeichnet.

Jetzt ist noch offen, wie wir erreichen, dass das Mock-Objekt zum richtigen Zeitpunkt zurückgegeben wird. Im Test in Zeile 30 holen wir uns als erstes die Referenz des ToDoService, der das Mock-Objekt liefern soll. Das funktioniert problemlos, da dieser im TestingModul definiert ist . Allerdings zeigt die Referenz zum ToDoService auf ein komplett leeres Objekt. Durch Zeile 36 initialisieren wir die getTodos() Funktion und geben bei Aufruf ein Observable des Mock-Objektes zurück. Wird nun im Nachfolgenden die countTodos() Funktion aufgerufen, wird dadurch die eben initialisierte Funktion getTodos() aufgerufen. Per Depenency Injection kann ohne Probleme der ManagementService die Abhängigkeit zum TodoService auflösen. Die Dependency Injection liefert das leere Objekt mit der zuvor initialisierten Funktion (die das Mock-Objekt liefert) zurück. Und schon kann das Mock-Objekt im ManagementService verarbeitet werden. Der eigentliche Test kann formuliert werden.

Ziel erreicht 

Das ursprüngliche Ziel ist erreicht. Die Abhängigkeiten des TodoService stellen kein Problem mehr dar. Es geht sogar so weit, dass wir diese ignorieren können. Wir definieren, was die einzige für den Unit-Test notwendige Funktion eines Services zurückgeben soll. Der Rest des Service - Abhängigkeiten und andere Funktionen - kann ausgeblendet werden.

Das öffentliche Repository auf GitHub bietet alles, um loszulegen.

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