Ferdinand Malcher und Johannes Hoppe schreiben z.B. in ihrem Buch Angular: Grundlagen, fortgeschrittene Themen und Best Practices: „…beim Testing wollen wir beweisen, dass unsere Software die an sie gestellten Anforderungen fehlerfrei erfüllt. Weiterhin stellen Tests eine Dokumentation der fachlichen oder technischen Anforderungen dar. Zudem erhöhen Tests insgesamt die Qualität unserer Software. Getesteter Code ist tendenziell modularer und lose gekoppelt – sonst wäre er nicht testbar. Eine gut gepflegte Sammlung an Tests bietet uns daher einen großen Mehrwert.“
Isolierte ES2015-Klassen oder Angular Test Bed?
Beim Unit-Testen einer Angular-Anwendung gibt es zwei Arten von Tests:
- Durch die Verwendung der ES2015-Module-API und durch die Kapselung von Funktionalität in isolierten Klassen bestehen Angular-Anwendungen oft aus von Angular unabhängigem Code. In diesem Fall liegt die eigentliche Business-Logik in Form einer regulären ES2015-Klasse vor. Beim Testen bietet diese Tatsache große Vorteile, da es hier leicht möglich ist, eine simple Instanz der Klasse zu erzeugen.
- Mithilfe des Angular-Testing-Frameworks kann man echte Angular-Komponenten (inklusive der gerenderten Templates) unabhängig von einer laufenden Anwendung testen. Wir können beispielsweise überprüfen, ob eine Komponente erstellt wurde, wie sie mit dem Template, mit anderen Komponenten und mit Abhängigkeiten interagiert.
Isolierte ES2015-Klassen
In einem isolierten Ansatz testen wir eine Direktive, die dafür zuständig ist, zu überprüfen, ob der Wert eines Controls eine gültige E-Mail-Adresse enthält.
Zuerst instanziieren wir die Klasse und testen dann, wie sie in verschiedenen Situationen funktioniert.
Listing zeigt die entsprechende Implementierung:
import{Directive}from'@angular/core';
import{NG_VALIDATORS}from'@angular/forms';
@Directive({
selector:'[EmailValidator]',
exportAs:'email-validator',
providers: [
{
provide:NG_VALIDATORS,
useClass:EmailValidatorDirective,
multi:true,
},
],
})
exportclassEmailValidatorDirective{
validate(element:HTMLElement){
constinput= <HTMLInputElement>element;
constregex=
/^([\w-]+(?:\.[\w-]+)*)@((?:[\w-]+\.)*\w[\w-]{0,66})\.([a-z]{2,6}(?:\.[a-z]{2})?)$/i;
if (!input?.value||input.value===''||regex.test(input.value)) {
returntrue;
}else{
returnfalse;
}
}
}
Vergessen Sie beim Schreiben von Tests nicht die Reihenfolge der Codeausführung. Zum Beispiel legen wir die Aktionen, die vor jedem Test ausgeführt werden müssen, in beforeEach.
Auch wenn es sich um eine vollständige Angular-Direktive handelt, liegt die eigentliche Business-Logik in Form einer regulären ES2015-Klasse vor. An der Stelle benötigen wir den Validator nicht durch das Angular-Framework erzeugen zu lassen, sondern einfach eine simple Instanz der Klasse zu erzeugen.
Listing zeigt somit einen vollständigen Testfall:
import{EmailValidatorDirective}from'./email-validator.directive';
describe('EmailValidatorDirective',()=>{
letvalidatorDirective:EmailValidatorDirective|null=null;
beforeEach(()=>{
validatorDirective=newEmailValidatorDirective();
});
it('schould accept valid email addresses',()=>{
// Arrange
constexpectedColor='#00FF00';
constcontrol= <HTMLInputElement>{
value:'foo@bar.com',
style:{borderColor:'#000000'},
};
// Act
constresult=validatorDirective?.validate(control);
// Assert
expect(result?.style.borderColor).toEqual(expectedColor);
});
it('schould not accept invalid email addresses',()=>{
// Arrange
constexpectedColor='#FF0000';
constcontrol= <HTMLInputElement>{
value:'foo@barcom',
style:{borderColor:'#000000'},
};
// Act
constresult=validatorDirective?.validate(control);
// Assert
expect(result?.style.borderColor).toEqual(expectedColor);
});
});
Angular Test Bed Tests
Einfache Komponente
Während Sie im vorigen Abschnitt einfach auf ES2015-Standardfunktionalität zurückgreifen konnten, müssen Sie im Fall von echten Komponententests dafür sorgen, dass Angular die für die Testausführung benötigten Komponenten kennt und deren Instanziierung vornimmt.
Lass uns nun die volle Leistungsfähigkeit des Dienstprogramms TestBed anschauen. Beginnen wir mit der einfachsten Komponente als Beispiel:
import{Component}from'@angular/core';
@Component({
selector:'app-root',
templateUrl:'./app.component.html',
styleUrls: ['./app.component.scss'],
})
exportclassAppComponent{
title='AngularTestDemo';
constructor(){}
}
Template-Datei:
<div>
<div>Input E-Mail</div>
<input
EmailValidator
type="text"
class="form-control"
name="email"
/>
</div>
Analysieren wir die Testdatei Stück für Stück. Zuerst legen wir die TestBed-Konfiguration fest:
beforeEach(async()=>{
awaitTestBed.configureTestingModule({
declarations: [AppComponent]
}).compileComponents();
});
Im beforeEach-Hook wird zunächst das Testing-Modul konfiguriert. Sie können sich das hier erzeugte Modul als Ersatz für das echte (in der Datei app.module.ts) definierte Hauptmodul der Applikation vorstellen. Hier wird das AppComponent zu den declarations hinzugefügt. Über die Methode configureTestingModule können Sie ebenfalls andere Module mit der impots-Eigenschaft oder Services mit der providers-Eigenschaft zum Modul hinzufügen.
compileComponents() ist eine Methode, die Stile und UI-Vorlagen in separate Dateien inline gerendert macht.
Dieser Prozess ist asynchron, da der Angular-Compiler Daten aus dem Dateisystem abrufen muss.
Tests erfordern, dass die Komponenten kompiliert werden, bevor sie über die Methode createComponent() instanziiert werden.
Daher haben wir den ersten BeforeEach in der async-Methode platziert, damit sein Inhalt in einer speziellen asynchronen Umgebung ausgeführt wird. Und bis die Methode compileComponents() ausgeführt wird, wird das folgende beforeEach nicht ausgeführt:
beforeEach(()=>{
fixture=TestBed.createComponent(AppComponent);
component=fixture.componentInstance;
});
Dank der Platzierung aller gängigen Daten in beforeEach ist der weitere Code viel sauberer. Lassen Sie uns die Erstellung der Komponenteninstanz und ihrer Eigenschaft überprüfen:
it('should create the component',()=>{
expect(component).toBeTruthy();
});
it(`should have as title 'app'`,()=>{
expect(component.title).toEqual('AngularTestDemo');
});
Komponente mit Abhängigkeiten
Lassen Sie uns unsere Komponente erweitern, indem wir einen PopupService einfügen:
import{Component}from'@angular/core';
import{PopupService}from'./services/popup.service';
@Component({
selector:'app-root',
templateUrl:'./app.component.html',
styleUrls: ['./app.component.scss'],
})
exportclassAppComponent{
title='AngularTestDemo';
constructor(privatepopupService:PopupService){}
openDialog(){
this.popupService.openDialog();
}
}
Template-Datei:
<div>
<div>Input E-Mail</div>
<input
EmailValidator
type="text"
class="form-control"
name="email"
/>
<div>Open dialog</div>
<buttonclass="openDialog"(click)="openDialog()">Open dialog</button>
</div>
Es scheint, dass sie es noch nicht kompliziert haben, aber die Tests werden nicht bestehen. Auch wenn Sie nicht vergessen haben, den PopupService zu den AppModule-Anbietern hinzuzufügen.
Denn in TestBed müssen sich auch diese Veränderungen widerspiegeln:
beforeEach(async()=>{
awaitTestBed.configureTestingModule({
declarations: [AppComponent,EmailValidatorDirective],
providers: [
{provide:PopupService,useValue:popupServiceMock},
],
}).compileComponents();
});
Wir können den PopupService selbst angeben, aber normalerweise ist es am besten, ihn durch eine Klasse oder ein Objekt zu ersetzen, das genau beschreibt, was wir für unsere Tests benötigen.
Sie stellen sich einen PopupService mit einer Reihe von Abhängigkeiten vor und müssen während des Tests alles registrieren. Ganz zu schweigen davon, dass wir die Komponente in diesem Fall testen. Im Allgemeinen geht es beim Testen einer Sache nur um Unit-Tests.
Also schreiben wir den Mock wie folgt vor:
constpopupServiceMock={
openDialog:()=>{},
};
Wir legen nur die Methoden fest, die wir testen. Fügen Sie der TestBed-Konfiguration Anbieter hinzu:
providers: [
{provide:PopupService,useValue:popupServiceMock},
],
Verwechseln Sie popupServiceMock und PopupService nicht. Dies sind verschiedene Objekte: Das erste ist ein Klon des zweiten.
Großartig, aber wir haben den PopupService nicht einfach so umgesetzt, sondern für den Einsatz:
openDialog(){
this.popupService.openDialog();
}
Jetzt lohnt es sich sicherzustellen, dass die Methode tatsächlich aufgerufen wird. Dazu erhalten wir zunächst eine Instanz des Dienstes.
Da in diesem Fall der Dienst in den Anbietern des Root-Moduls angegeben ist, können wir dies tun:
letdebugElement:DebugElement;
letfixture:ComponentFixture<AppComponent>;
letpopupService:PopupService;
…
fixture=TestBed.createComponent(AppComponent);
debugElement=fixture.debugElement;
popupService=debugElement.injector.get(PopupService);
Zum Schluss die Prüfung selbst:
it('should called openDialog',()=>{
constopenSpy=spyOn(popupService,'openDialog').and.callThrough();
debugElement.query(By.css('button.openDialog')).triggerEventHandler('click', null);
expect(openSpy).toHaveBeenCalled();
});
Setzen Sie den Spy auf der openDialog-Methode des popupService-Objekts.
Beachten Sie, dass wir den Aufruf der Servicemethode überprüfen, nicht die Rückgabe oder andere Dinge über den Service selbst. Sie sollten im Dienst getestet werden.
Nur bestimmte Tests ausführen (xdescribe, xit, fdescribe, fit)
Um bestimmte Test-Suites oder Tests zeitweise abzuschalten, stehen Ihnen die beiden Funktionen xdescribe und xit zur Verfügung. Ersetzen Sie hierfür einfach die Schlüsselwörter describe bzw. it durch das jeweilige Pendant. Folgende Listing zeigt die Deaktivierung des Tests auf Aufruf von OpenDialog:
xit('should called openDialog',()=>{
constopenSpy=spyOn(popupService,'openDialog').and.callThrough();
debugElement.query(By.css('button.openDialog')).triggerEventHandler('click',null);
expect(openSpy).toHaveBeenCalled();
});
Das Gegenstück hierzu bilden die beiden Funktionen fdescribe und fit. Ihre Verwendung führt dazu, dass nur noch die ausgewählte Suite oder der ausgewählte Testfall ausgeführt wird:
fit('should called openDialog',()=>{
constopenSpy=spyOn(popupService,'openDialog').and.callThrough();
debugElement.query(By.css('button.openDialog')).triggerEventHandler('click',null);
expect(openSpy).toHaveBeenCalled();
});
Fazit
In diesem Blog haben wir Jasmine für Angular 12-Entwickler vorgestellt und erfahren, wie Sie mit Jasmine beginnen, um Ihren JavaScript-Code zu testen. Es ist schwierig, alle Momente in einem Artikel abzudecken, aber es scheint mir, dass die Hauptsache darin besteht, zu verstehen, welche Werkzeuge Sie haben und was Sie damit tun können, und sich dann in der Praxis furchtlos sowohl mit einfachen als auch mit komplexeren Fällen zu stellen.
Quellcode
https://github.com/ArkadiRosenberg/AngularTestDemo.git
Literatur
- Angular: Das große Handbuch zum JavaScript-Framework. Einführung und fortgeschrittene TypeScript-Techniken, Inkl. Angular Material Gebundene Ausgabe – 28. Januar 2019
von Christoph Höller
- Angular: Grundlagen, fortgeschrittene Themen und Best Practices – inkl. RxJS, NgRx und PWA (iX Edition)von Ferdinand Malcher , Johannes Hoppe , et al. | 15. Oktober 2020
- https://angular.io/
- https://www.habr.com
Dieser Tipp kommt von Arkadi Rosenberg, QUIBIQ Stuttgart.