Angular Komponenten-Tests oder: wie ich lernte Spione zu lieben
Weiter geht es mit meiner Serie zum Thema Unit-Tests mit Angular. Dieses Mal geht es darum, einen Unit-Test für eine GUI-Komponente zu schreiben. Wie immer findet ihr den vollständigen Quellcode auf GitHub.
Der Umfang des Projektes ist schnell erklärt. Die Anwendung zeigt eine Komponente „TodoComponent" an. Diese lädt über den „TodoService" ein JSON-Objekt. Dieses Objekt beinhaltet die Einträge einer Aufgabenliste die dann angezeigt werden. Die Herausforderung ist nun herauszufinden, wie diese Komponente getestet werden kann. Denn während unseres Unit-Tests möchten wir keine REST-Schnittstelle ansprechen, die uns benötigte Daten zur Verfügung stellt. Wäre diese Schnittstelle - aus welchen Gründen auch immer - nicht erreichbar, würde unser Test fehlschlagen. Das wollen wir auf keinen Fall! Von daher müssen wir irgendwie hinbekommen, dass der "TodoService" keinen wirklichen http-Request absendet.
Um den nachfolgenden Service geht es. Dieser stellt die beschriebene Funktionalität zur Verfügung, um Einträge einer Aufgabenliste per REST-Request zu laden.
import { Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { Observable } from 'rxjs'; import { Todo } from './todo'; @Injectable({ providedIn: 'root' }) export class TodoService { constructor(private http: HttpClient) {} public getTodos(): Observable<Todo[]> { return this.http.get<Todo[]>('https://jsonplaceholder.typicode.com/todos/'); } }
import { Component, OnInit } from '@angular/core'; import { TodoService } from '../todo.service'; import { Todo } from '../todo'; import { Observable } from 'rxjs'; @Component({ selector: 'app-todo', templateUrl: './todo.component.html', styleUrls: ['./todo.component.scss'] }) export class TodoComponent implements OnInit { public todos$: Observable<Todo[]>; constructor(private todoService: TodoService) {} ngOnInit() { this.todos$ = this.todoService.getTodos(); } clickOnTodo(todo: Todo): void { console.log(JSON.stringify(todo)); } }
<h1>Todos</h1> <ng-container *ngIf="todos$ | async as todos"> <div class="todos" *ngFor="let todo of todos"> <a [id]="'todo-' + todo.id" (click)="clickOnTodo(todo)">{{ todo | json }}</a> </div> </ng-container>
import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { TodoComponent } from './todo.component'; import { HttpClientModule } from '@angular/common/http'; import { TodoService } from '../todo.service'; import { Todo } from '../todo'; import { of } from 'rxjs'; import { By } from '@angular/platform-browser'; describe('TodoComponent', () => { let component: TodoComponent; let fixture: ComponentFixture<TodoComponent>; beforeEach(async(() => { TestBed.configureTestingModule({ declarations: [TodoComponent], imports: [HttpClientModule], providers: [TodoService] }).compileComponents(); })); beforeEach(() => { fixture = TestBed.createComponent(TodoComponent); component = fixture.componentInstance; fixture.detectChanges(); }); });
Der zweite "beforeEach" Block bereitet unsere Komponente für die nachfolgenden Tests vor. Der Fokus dieses Blog-Posts liegt nicht auf diesen Details. Dies kann man in der offiziellen Angular Dokumentation nachlesen.
Nun können wir uns den Unit-Test selbst anschauen. Würde der Test loslaufen, würde aktuell der "TodoService" den http-Request ausführen. Genau das wollen wir nicht! Als erstes holen wir uns die Instanz des Services und definieren Daten, die statt der Antwort des echten http-Requests benutzt werden sollen. Mit der "spyOn" Angabe erreichen wir, dass die originale Implementierung der "getTodos" überschrieben wird und etwas anderes zurückgibt. Und zwar ein Observable der selbstdefinierten Aufgaben ("mockTodos").const service: TodoService = TestBed.get(TodoService); 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 } ]; spyOn(service, 'getTodos').and.returnValue(of(mockTodos));
component.ngOnInit(); fixture.detectChanges();
const allAHrefElements = fixture.debugElement.queryAll(By.css('a')); expect(allAHrefElements.length).toBe(mockTodos.length);
Der Benutzer kann auf jedes einzelne Aufgabe klicken. Dabei wird die "clickOnTodo" Funktion ausgeführt und die ID der Aufgabe auf die Konsole geschrieben. Wir überprüfen nun, ob die Funktion mit den richtigen Übergabeparameter aufgerufen wird. Dazu erzeugen wir ein "spy"-Objekt, welches die "clickOnTodo" Funktion überwacht. Danach wird durch den Test auf das zweite Element der Aufgabenliste geklickt. Mit der letzten "expect"-Angabe überprüfen wir das "spy"-Objekt, ob die Funktion wirklich mit dem richtigen "Todo"-Objekt aufgerufen wurde.
const spyOnClickOnTodo = spyOn(component, 'clickOnTodo'); const todo2Test = 1; allAHrefElements[todo2Test].nativeElement.click(); expect(spyOnClickOnTodo).toHaveBeenCalledWith(mockTodos[todo2Test]);
Zwei Sachen werden also überprüft.
- Wird die gesamte Aufgabenliste anzeigt.
- Funktioniert ein Klick auf eine einzelne Aufgabe richtig.
Fazit!
Das war's! Wir haben einen Unit-Test für eine Komponente geschrieben. Die Abhängigkeiten zu einen Service macht uns keine Probleme. Wir wissen nun, wie man eine Funktion eines Services überschreiben kann, ohne die originale Funktionalität aufrufen zu müssen. Diese Vorgehensweise bietet uns die Möglichkeit beliebige Abhängigkeiten aufzulösen bzw. zu entfernen. Dadurch wird unser Komponenten-Test übersichtlich und hat keine Abhängigkeiten nach Extern.
Nachfolgend noch der gesamte Inhalt der "todo.component.spec.ts" Datei. Außerdem ist das gesamte Beispiel auf GitHub zu finden.
import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { TodoComponent } from './todo.component'; import { HttpClientModule } from '@angular/common/http'; import { TodoService } from '../todo.service'; import { Todo } from '../todo'; import { of } from 'rxjs'; import { By } from '@angular/platform-browser'; describe('TodoComponent', () => { let component: TodoComponent; let fixture: ComponentFixture<TodoComponent>; beforeEach(async(() => { TestBed.configureTestingModule({ declarations: [TodoComponent], imports: [HttpClientModule], providers: [TodoService] }).compileComponents(); })); beforeEach(() => { fixture = TestBed.createComponent(TodoComponent); component = fixture.componentInstance; fixture.detectChanges(); }); it('should create', () => { expect(component).toBeTruthy(); }); it('should display a list of todos', () => { const service: TodoService = TestBed.get(TodoService); 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 } ]; spyOn(service, 'getTodos').and.returnValue(of(mockTodos)); component.ngOnInit(); fixture.detectChanges(); const allAHrefElements = fixture.debugElement.queryAll(By.css('a')); expect(allAHrefElements.length).toBe(mockTodos.length); const spyOnClickOnTodo = spyOn(component, 'clickOnTodo'); const todo2Test = 1; allAHrefElements[todo2Test].nativeElement.click(); expect(spyOnClickOnTodo).toHaveBeenCalledWith(mockTodos[todo2Test]); }); });
Senior Chief Consultant bei ORDIX
Bei Updates im Blog, informieren wir per E-Mail.
Kommentare