1. Code
  2. JavaScript
  3. Angular

Testen von Komponenten in Angular mit Jasmine: Teil 2, Dienste

Scroll to top
This post is part of a series called Testing Components in Angular Using Jasmine.
Testing Components in Angular Using Jasmine: Part 1

German (Deutsch) translation by Katharina Grigorovich-Nevolina (you can also view the original English article)

Final product imageFinal product imageFinal product image
What You'll Be Creating

Dies ist der zweite Teil der Serie zum Testen in Angular mit Jasmine. Im ersten Teil des Tutorials haben wir grundlegende Komponententests für die Pastebin-Klasse und die Pastebin-Komponente geschrieben. Die Tests, die anfänglich fehlschlugen, wurden später grün gemacht.

Überblick

Hier ist eine Übersicht darüber, woran wir im zweiten Teil des Tutorials arbeiten werden.

High level overview of things weve discussed in the previous tutorial and things we will be discussing in this tutorialHigh level overview of things weve discussed in the previous tutorial and things we will be discussing in this tutorialHigh level overview of things weve discussed in the previous tutorial and things we will be discussing in this tutorial

In diesem Tutorial werden wir:

  • neue Komponenten erstellen und weitere Unit-Tests schreiben 
  • Tests für die Benutzeroberfläche der Komponente schreiben
  • Unit-Tests für den Pastebin-Dienst schreiben 
  • eine Komponente mit Ein- und Ausgängen testen 
  • eine Komponente mit Routen testen 

Lassen Sie uns anfangen!

Hinzufügen einer Paste (Fortsetzung)

Wir waren in der Mitte des Prozesses des Schreibens von Komponententests für die AddPaste-Komponente. Hier haben wir im ersten Teil der Serie aufgehört.

1
  it('should display the `create Paste` button', () => {
2
     //There should a create button in view

3
      expect(element.innerText).toContain("create Paste");
4
  });
5
6
  it('should not display the modal unless the button is clicked', () => {
7
      //source-model is an id for the modal. It shouldn't show up unless create button is clicked

8
      expect(element.innerHTML).not.toContain("source-modal");
9
  })
10
11
  it('should display the modal when `create Paste` is clicked', () => {
12
13
      let createPasteButton = fixture.debugElement.query(By.css("button"));
14
      //triggerEventHandler simulates a click event on the button object

15
      createPasteButton.triggerEventHandler('click',null);
16
      fixture.detectChanges();
17
      expect(element.innerHTML).toContain("source-modal");
18
     
19
  })
20
21
})

Wie bereits erwähnt, werden wir keine strengen UI-Tests schreiben. Stattdessen werden wir einige grundlegende Tests für die Benutzeroberfläche schreiben und nach Möglichkeiten suchen, die Logik der Komponente zu testen.

Die Klickaktion wird mit der DebugElement.triggerEventHandler()-Methode ausgelöst, die Teil der Angular-Testdienstprogramme ist.

Bei der AddPaste-Komponente geht es im Wesentlichen darum, neue Pasten zu erstellen. Daher sollte die Vorlage der Komponente eine Schaltfläche zum Erstellen einer neuen Einfügung enthalten. Durch Klicken auf die Schaltfläche sollte ein "modales Fenster" mit der ID "source-modal" angezeigt werden, das ansonsten ausgeblendet bleiben sollte. Das modale Fenster wird mit Bootstrap erstellt. Daher finden Sie möglicherweise viele CSS-Klassen in der Vorlage.

Die Vorlage für die Add-Paste-Komponente sollte ungefähr so aussehen:

1
<!--- add-paste.component.html -->
2
3
<div class="add-paste">
4
    <button> create Paste </button>
5
  <div  id="source-modal" class="modal fade in">
6
    <div class="modal-dialog" >
7
      <div class="modal-content">
8
        <div class="modal-header"></div>
9
        <div class="modal-body"></div>
10
         <div class="modal-footer"></div>
11
     </div>
12
    </div>
13
  </div>
14
</div>

Der zweite und dritte Test geben keine Auskunft über die Implementierungsdetails der Komponente. Hier ist die überarbeitete Version von add-paste.component.spec.ts.

1
 it('should not display the modal unless the button is clicked', () => {
2
   
3
   //source-model is an id for the modal. It shouldn't show up unless create button is clicked

4
    expect(element.innerHTML).not.toContain("source-modal");
5
6
   //Component's showModal property should be false at the moment

7
    expect(component.showModal).toBeFalsy("Show modal should be initially false");
8
 })
9
10
 it('should display the modal when `create Paste` is clicked',() => {
11
   
12
    let createPasteButton = fixture.debugElement.query(By.css("button"));
13
    //create a spy on the createPaste  method

14
    spyOn(component,"createPaste").and.callThrough();
15
    
16
    //triggerEventHandler simulates a click event on the button object

17
    createPasteButton.triggerEventHandler('click',null);
18
    
19
    //spy checks whether the method was called

20
    expect(component.createPaste).toHaveBeenCalled();
21
    fixture.detectChanges();
22
    expect(component.showModal).toBeTruthy("showModal should now be true");
23
    expect(element.innerHTML).toContain("source-modal");
24
 })

Die überarbeiteten Tests sind insofern expliziter, als sie die Logik der Komponente perfekt beschreiben. Hier ist die AddPaste-Komponente und ihre Vorlage.

1
<!--- add-paste.component.html -->
2
3
<div class="add-paste">
4
  <button (click)="createPaste()"> create Paste </button>
5
  <div *ngIf="showModal" id="source-modal" class="modal fade in">
6
    <div class="modal-dialog" >
7
      <div class="modal-content">
8
        <div class="modal-header"></div>
9
        <div class="modal-body"></div>
10
         <div class="modal-footer"></div>
11
     </div>
12
    </div>
13
  </div>
14
</div>
1
/* add-paste.component.ts */
2
3
export class AddPasteComponent implements OnInit {
4
5
  showModal: boolean = false;
6
  // Languages imported from Pastebin class

7
  languages: string[] = Languages;
8
  
9
  constructor() { }
10
  ngOnInit() { }
11
  
12
  //createPaste() gets invoked from the template. 

13
  public createPaste():void {
14
  	this.showModal = true;
15
  }
16
}

Die Tests sollten weiterhin fehlschlagen, da der Spion von addPaste eine solche Methode im PastebinService nicht findet. Kehren wir zum PastebinService zurück und legen etwas Fleisch darauf.

Schreiben der Tests für Dienste

Bevor wir mit dem Schreiben weiterer Tests fortfahren, fügen wir dem Pastebin-Dienst Code hinzu.

1
public addPaste(pastebin: Pastebin): Promise<any> {
2
    return this.http.post(this.pastebinUrl, JSON.stringify(pastebin), {headers: this.headers})
3
	   .toPromise()
4
 	   .then(response =>response.json().data)
5
 	   .catch(this.handleError);
6
}

addPaste() ist die Methode des Dienstes zum Erstellen neuer Pasten. http.post gibt ein Observable zurück, das mit der toPromise()-Methode in ein Versprechen umgewandelt wird. Die Antwort wird in das JSON-Format umgewandelt, und alle Laufzeitausnahmen werden von handleError() abgefangen und gemeldet.

Sollten wir nicht Tests für Dienstleistungen schreiben, könnten Sie fragen? Und meine Antwort ist ein klares Ja. Services, die über Dependency Injection(DI) in Angular-Komponenten injiziert werden, sind ebenfalls fehleranfällig. Darüber hinaus sind Tests für Angular-Dienste relativ einfach. Die Methoden in PastebinService sollten den vier CRUD-Operationen ähneln, mit einer zusätzlichen Methode zur Behandlung von Fehlern. Die Methoden sind wie folgt:

  • handleError()
  • getPastebin()
  • addPaste()
  • updatePaste()
  • deletePaste()

Wir haben die ersten drei Methoden in der Liste implementiert. Versuchen wir, Tests für sie zu schreiben. Hier ist der Beschreibungsblock.

1
import { TestBed, inject } from '@angular/core/testing';
2
import { Pastebin, Languages } from './pastebin';
3
import { PastebinService } from './pastebin.service';
4
import { AppModule } from './app.module';
5
import { HttpModule } from '@angular/http';
6
7
let testService: PastebinService;
8
let mockPaste: Pastebin;
9
let responsePropertyNames, expectedPropertyNames;
10
11
describe('PastebinService', () => {
12
  beforeEach(() => {
13
   TestBed.configureTestingModule({
14
      providers: [PastebinService],
15
      imports: [HttpModule]
16
    });
17
    
18
    //Get the injected service into our tests

19
    testService= TestBed.get(PastebinService);
20
    mockPaste = { id:999, title: "Hello world", language: Languages[2], paste: "console.log('Hello world');"};
21
22
  });
23
});

Wir haben TestBed.get(PastebinService) verwendet, um den realen Service in unsere Tests einzufügen.

1
  it('#getPastebin should return an array with Pastebin objects',async() => {
2
     
3
    testService.getPastebin().then(value => {
4
      //Checking the property names of the returned object and the mockPaste object

5
      responsePropertyNames = Object.getOwnPropertyNames(value[0]);
6
      expectedPropertyNames = Object.getOwnPropertyNames(mockPaste);
7
     
8
      expect(responsePropertyNames).toEqual(expectedPropertyNames);
9
      
10
    });
11
  });

getPastebin gibt ein Array von Pastebin-Objekten zurück. Die Typprüfung zur Kompilierungszeit von TypeScript kann nicht verwendet werden, um zu überprüfen, ob der zurückgegebene Wert tatsächlich ein Array von Pastebin-Objekten ist. Daher haben wir Object.getOwnPropertNames() verwendet, um sicherzustellen, dass beide Objekte dieselben Eigenschaftsnamen haben.

Der zweite Test folgt:

1
  it('#addPaste should return async paste', async() => {
2
    testService.addPaste(mockPaste).then(value => {
3
      expect(value).toEqual(mockPaste);
4
    })
5
  })

Beide Tests sollten bestehen. Hier sind die restlichen Tests.

1
  it('#updatePaste should update', async() => {
2
    //Updating the title of Paste with id 1
3
    mockPaste.id = 1;
4
    mockPaste.title = "New title"
5
    testService.updatePaste(mockPaste).then(value => {
6
      expect(value).toEqual(mockPaste);
7
    })
8
  })
9
10
  it('#deletePaste should return null', async() => {
11
    testService.deletePaste(mockPaste).then(value => {
12
      expect(value).toEqual(null);
13
    })
14
  })

Überarbeiten Sie pastebin.service.ts mit dem Code für die Methoden updatePaste() und deletePaste().

1
//update a paste

2
public updatePaste(pastebin: Pastebin):Promise<any> {
3
	const url = `${this.pastebinUrl}/${pastebin.id}`;
4
	return this.http.put(url, JSON.stringify(pastebin), {headers: this.headers})
5
		.toPromise()
6
		.then(() => pastebin)
7
		.catch(this.handleError);
8
}
9
//delete a paste

10
public deletePaste(pastebin: Pastebin): Promise<void> {
11
	const url = `${this.pastebinUrl}/${pastebin.id}`;
12
	return this.http.delete(url, {headers: this.headers})
13
		.toPromise()
14
		.then(() => null )
15
		.catch(this.handleError);
16
}

Zurück zu den Komponenten

Die verbleibenden Anforderungen für die AddPaste-Komponente lauten wie folgt:

  • Durch Drücken der Schaltfläche Speichern sollte die addPaste()-Methode des Pastebin-Dienstes aufgerufen werden.
  • Wenn der Vorgang addPaste erfolgreich ist, sollte die Komponente ein Ereignis ausgeben, um die übergeordnete Komponente zu benachrichtigen.
  • Durch Klicken auf die Schaltfläche Schließen sollte die ID 'source-modal' aus dem DOM entfernt und die showModal-Eigenschaft auf false aktualisiert werden.

Da sich die obigen Testfälle mit dem Modalfenster befassen, ist es möglicherweise eine gute Idee, verschachtelte Beschreibungsblöcke zu verwenden.

1
describe('AddPasteComponent', () => {
2
  .
3
  .
4
  .
5
  describe("AddPaste Modal", () => {
6
  
7
    let inputTitle: HTMLInputElement;
8
    let selectLanguage: HTMLSelectElement;
9
    let textAreaPaste: HTMLTextAreaElement;
10
    let mockPaste: Pastebin;
11
    let spyOnAdd: jasmine.Spy;
12
    let pastebinService: PastebinService;
13
    
14
    beforeEach(() => {
15
      
16
      component.showModal = true;
17
      fixture.detectChanges();
18
19
      mockPaste = { id:1, title: "Hello world", language: Languages[2], paste: "console.log('Hello world');"};
20
      //Create a jasmine spy to spy on the addPaste method

21
      spyOnAdd = spyOn(pastebinService,"addPaste").and.returnValue(Promise.resolve(mockPaste));
22
      
23
    });
24
  
25
  });
26
});

Das Deklarieren aller Variablen an der Wurzel des Beschreibungsblocks ist aus zwei Gründen eine gute Vorgehensweise. Auf die Variablen kann innerhalb des Beschreibungsblocks zugegriffen werden, in dem sie deklariert wurden, und der Test wird lesbarer.

1
  it("should accept input values", () => {
2
      //Query the input selectors

3
      inputTitle = element.querySelector("input");
4
      selectLanguage = element.querySelector("select");
5
      textAreaPaste = element.querySelector("textarea");
6
      
7
      //Set their value

8
      inputTitle.value = mockPaste.title;
9
      selectLanguage.value = mockPaste.language;
10
      textAreaPaste.value = mockPaste.paste;
11
      
12
      //Dispatch an event

13
      inputTitle.dispatchEvent(new Event("input"));
14
      selectLanguage.dispatchEvent(new Event("change"));
15
      textAreaPaste.dispatchEvent(new Event("input"));
16
17
      expect(mockPaste.title).toEqual(component.newPaste.title);
18
      expect(mockPaste.language).toEqual(component.newPaste.language);
19
      expect(mockPaste.paste).toEqual(component.newPaste.paste);
20
    });

Der obige Test verwendet die querySelector()-Methode, um inputTitle, SelectLanguage und textAreaPaste ihre jeweiligen HTML-Elemente (<input>, <select> und <textArea>) zuzuweisen. Als Nächstes werden die Werte dieser Elemente durch die Eigenschaftswerte von mockPaste ersetzt. Dies entspricht einem Benutzer, der das Formular über einen Browser ausfüllt.

element.dispatchEvent(new Event("input")) löst ein neues Eingabeereignis aus, um die Vorlage darüber zu informieren, dass sich die Werte des Eingabefelds geändert haben. Der Test erwartet, dass die Eingabewerte in die newPaste-Eigenschaft der Komponente übertragen werden.

Deklarieren Sie die newPaste-Eigenschaft wie folgt:

1
    newPaste: Pastebin = new Pastebin();

Und aktualisieren Sie die Vorlage mit dem folgenden Code:

1
<!--- add-paste.component.html -->
2
<div class="add-paste">
3
  <button type="button" (click)="createPaste()"> create Paste </button>
4
  <div *ngIf="showModal"  id="source-modal" class="modal fade in">
5
    <div class="modal-dialog" >
6
      <div class="modal-content">
7
        <div class="modal-header">
8
           <h4 class="modal-title"> 
9
        	 <input  placeholder="Enter the Title" name="title" [(ngModel)] = "newPaste.title" />
10
          </h4>
11
        </div>
12
        <div class="modal-body">
13
      	 <h5> 
14
      		<select name="category"  [(ngModel)]="newPaste.language" >
15
      			<option  *ngFor ="let language of languages" value={{language}}> {{language}} </option>
16
        	</select>
17
         </h5>     	
18
      	 <textarea name="paste" placeholder="Enter the code here" [(ngModel)] = "newPaste.paste"> </textarea>
19
      	</div>
20
      <div class="modal-footer">
21
        <button type="button" (click)="onClose()">Close</button>
22
        <button type="button" (click) = "onSave()">Save</button>
23
      </div>
24
     </div>
25
    </div>
26
  </div>
27
</div>

Die zusätzlichen Divs und Klassen gelten für das modale Fenster des Bootstraps. [(ngModel)] ist eine Angular-Direktive, die die bidirektionale Datenbindung implementiert. (click) = "onClose()" und (click) = "onSave()" sind Beispiele für Ereignisbindungstechniken, mit denen das Klickereignis an eine Methode in der Komponente gebunden wird. Weitere Informationen zu verschiedenen Datenbindungstechniken finden Sie in der offiziellen Vorlage Syntax-Anleitung von Angular.

Wenn ein Template Parse-Fehler auftritt, liegt dies daran, dass Sie das FormsModule nicht in die AppComponent importiert haben.

Fügen wir unserem Test weitere Spezifikationen hinzu.

1
 it("should submit the values", async() => {   
2
   component.newPaste = mockPaste;
3
   component.onSave();
4
    fixture.detectChanges();
5
    fixture.whenStable().then( () => {
6
        fixture.detectChanges();
7
        expect(spyOnAdd.calls.any()).toBeTruthy();
8
    });
9
10
 });
11
 
12
 it("should have a onClose method", () => {
13
    component.onClose();
14
    fixture.detectChanges();
15
    expect(component.showModal).toBeFalsy();
16
  })

component.onSave() entspricht dem Aufruf von triggerEventHandler() für das Element Save button. Da wir die Benutzeroberfläche für die Schaltfläche bereits hinzugefügt haben, klingt der Aufruf von component.save() sinnvoller. Die Expect-Anweisung prüft, ob der Spion angerufen wurde. Hier ist die endgültige Version der AddPaste-Komponente.

1
import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core';
2
import { Pastebin, Languages } from '../pastebin';
3
import { PastebinService } from '../pastebin.service';
4
5
@Component({
6
  selector: 'app-add-paste',
7
  templateUrl: './add-paste.component.html',
8
  styleUrls: ['./add-paste.component.css']
9
})
10
export class AddPasteComponent implements OnInit {
11
12
  @Output() addPasteSuccess: EventEmitter<Pastebin> = new EventEmitter<Pastebin>();
13
  showModal: boolean = false;
14
  newPaste: Pastebin = new Pastebin();
15
  languages: string[] = Languages;
16
17
  constructor(private pasteServ: PastebinService) { }
18
19
  ngOnInit() {  }
20
  //createPaste() gets invoked from the template. This shows the Modal

21
  public createPaste():void {
22
    this.showModal = true;
23
    
24
  }
25
  //onSave() pushes the newPaste property into the server

26
  public onSave():void {
27
    this.pasteServ.addPaste(this.newPaste).then( () => {
28
      console.log(this.newPaste);
29
        this.addPasteSuccess.emit(this.newPaste);
30
        this.onClose();
31
    });
32
  }
33
  //Used to close the Modal

34
  public onClose():void {
35
    this.showModal=false;
36
  }
37
}

Wenn der onSave-Vorgang erfolgreich ist, sollte die Komponente ein Ereignis ausgeben, das der übergeordneten Komponente (Pastebin-Komponente) signalisiert, ihre Ansicht zu aktualisieren. Zu diesem Zweck dient addPasteSuccess, eine Ereigniseigenschaft, die mit einem @Output-Dekorator dekoriert ist.

Das Testen einer Komponente, die ein Ausgabeereignis ausgibt, ist einfach.

1
 describe("AddPaste Modal", () => {
2
   
3
    beforeEach(() => {
4
    .
5
    .
6
   //Subscribe to the event emitter first

7
   //If the emitter emits something, responsePaste will be set

8
   component.addPasteSuccess.subscribe((response: Pastebin) => {responsePaste = response},)
9
      
10
    });
11
    
12
    it("should accept input values", async(() => {
13
    .
14
    .
15
      component.onSave();
16
      fixture.detectChanges();
17
      fixture.whenStable().then( () => {
18
        fixture.detectChanges();
19
        expect(spyOnAdd.calls.any()).toBeTruthy();
20
        expect(responsePaste.title).toEqual(mockPaste.title);
21
      });
22
    }));
23
  
24
  });
25

Der Test abonniert die Eigenschaft addPasteSuccess genau wie die übergeordnete Komponente. Die Erwartung gegen Ende bestätigt dies. Unsere Arbeit an der AddPaste-Komponente ist abgeschlossen.

Kommentieren Sie diese Zeile in pastebin.component.html aus:

1
<app-add-paste (addPasteSuccess)= 'onAddPaste($event)'> </app-add-paste> 

Und aktualisieren Sie pastebin.component.ts mit dem folgenden Code.

1
 //This will be invoked when the child emits addPasteSuccess event

2
 public onAddPaste(newPaste: Pastebin) {
3
    this.pastebin.push(newPaste);
4
  }

Wenn Sie auf einen Fehler stoßen, liegt dies daran, dass Sie die AddPaste-Komponente in der Spezifikationsdatei der Pastebin-Komponente nicht deklariert haben. Wäre es nicht großartig, wenn wir alles, was unsere Tests erfordern, an einem einzigen Ort deklarieren und in unsere Tests importieren könnten? Um dies zu erreichen, können wir entweder das AppModule in unsere Tests importieren oder stattdessen ein neues Modul für unsere Tests erstellen. Erstellen Sie eine neue Datei und nennen Sie sie app-testing-module.ts:

1
import { BrowserModule } from '@angular/platform-browser';
2
import { NgModule } from '@angular/core';
3
4
//Components

5
import { AppComponent } from './app.component';
6
import { PastebinComponent } from './pastebin/pastebin.component';
7
import { AddPasteComponent } from './add-paste/add-paste.component';
8
//Service for Pastebin

9
10
import { PastebinService } from "./pastebin.service";
11
12
//Modules used in this tutorial

13
import { HttpModule }    from '@angular/http';
14
import { FormsModule } from '@angular/forms';
15
16
//In memory Web api to simulate an http server

17
import { InMemoryWebApiModule } from 'angular-in-memory-web-api';
18
import { InMemoryDataService }  from './in-memory-data.service';
19
20
@NgModule({
21
  declarations: [
22
    AppComponent,
23
    PastebinComponent,
24
    AddPasteComponent,
25
  ],
26
  
27
  imports: [
28
    BrowserModule, 
29
    HttpModule,
30
    FormsModule,
31
    InMemoryWebApiModule.forRoot(InMemoryDataService),
32
  ],
33
  providers: [PastebinService],
34
  bootstrap: [AppComponent]
35
})
36
export class AppTestingModule { }

Jetzt können Sie ersetzen:

1
 beforeEach(async(() => {
2
    TestBed.configureTestingModule({
3
      declarations: [ AddPasteComponent ],
4
      imports: [ HttpModule, FormsModule ],
5
      providers: [ PastebinService ],
6
    })
7
    .compileComponents();
8
}));

mit:

1
beforeEach(async(() => {
2
    TestBed.configureTestingModule({
3
      imports: [AppTestingModule]
4
    })
5
    .compileComponents();
6
  }));

Die Metadaten, die providers und declarations definieren, sind verschwunden. Stattdessen wird das AppTestingModule importiert. Das ist ordentlich! TestBed.configureTestingModule() sieht schlanker aus als zuvor.

Anzeigen, Bearbeiten und Löschen Einfügen

Die ViewPaste-Komponente übernimmt die Logik zum Anzeigen, Bearbeiten und Löschen einer Einfügung. Das Design dieser Komponente ähnelt dem, was wir mit der AddPaste-Komponente gemacht haben.

Mock design of the ViewPasteComponent in edit modeMock design of the ViewPasteComponent in edit modeMock design of the ViewPasteComponent in edit mode
Bearbeitungsmodus
Mock design of the ViewPasteComponent in view modeMock design of the ViewPasteComponent in view modeMock design of the ViewPasteComponent in view mode
Ansichtsmodus

Die Ziele der ViewPaste-Komponente sind nachfolgend aufgeführt:

  • Die Vorlage der Komponente sollte eine Schaltfläche namens Ansicht Einfügen enthalten.
  • Wenn Sie auf die Schaltfläche Ansicht Einfügen klicken, sollte ein modales Fenster mit der ID "source-modal" angezeigt werden.
  • Die Einfügedaten sollten sich von der übergeordneten Komponente zur untergeordneten Komponente ausbreiten und im modalen Fenster angezeigt werden.
  • Durch Drücken der Bearbeitungstaste sollte component.editEnabled auf true gesetzt werden (editEnabled wird verwendet, um zwischen Bearbeitungsmodus und Ansichtsmodus umzuschalten).
  • Durch Klicken auf die Schaltfläche Speichern sollte die updatePaste()-Methode des Pastebin-Dienstes aufgerufen werden.
  • Ein Klick auf die Schaltfläche Löschen sollte die Methode deletePaste() des Pastebin-Dienstes aufrufen.
  • Erfolgreiche Aktualisierungs- und Löschvorgänge sollten ein Ereignis auslösen, um die übergeordnete Komponente über Änderungen an der untergeordneten Komponente zu informieren.

Lassen Sie uns anfangen! Die ersten beiden Spezifikationen sind identisch mit den Tests, die wir zuvor für die AddPaste-Komponente geschrieben haben.

1
 it('should show a button with text View Paste', ()=> {
2
    expect(element.textContent).toContain("View Paste");
3
  });
4
5
  it('should not display the modal until the button is clicked', () => {
6
      expect(element.textContent).not.toContain("source-modal");
7
  });

Ähnlich wie zuvor werden wir einen neuen Beschreibungsblock erstellen und den Rest der Spezifikationen darin platzieren. Durch das Verschachteln von Beschreibungsblöcken auf diese Weise wird die Spezifikationsdatei besser lesbar und das Vorhandensein einer Beschreibungsfunktion aussagekräftiger.

Der verschachtelte Beschreibungsblock verfügt über eine beforeEach()-Funktion, mit der zwei Spione initialisiert werden, einer für die updatePaste()-Methode und der andere für die deletePaste()-Methode. Vergessen Sie nicht, ein mockPaste-Objekt zu erstellen, da unsere Tests darauf basieren.

1
beforeEach(()=> {
2
      //Set showPasteModal to true to ensure that the modal is visible in further tests
3
      component.showPasteModal = true;
4
      mockPaste = {id:1, title:"New paste", language:Languages[2], paste: "console.log()"};
5
      
6
      //Inject PastebinService
7
      pastebinService = fixture.debugElement.injector.get(PastebinService);
8
      
9
      //Create spies for deletePaste and updatePaste methods
10
      spyOnDelete = spyOn(pastebinService,'deletePaste').and.returnValue(Promise.resolve(true));
11
      spyOnUpdate = spyOn(pastebinService, 'updatePaste').and.returnValue(Promise.resolve(mockPaste));
12
     
13
      //component.paste is an input property 
14
      component.paste = mockPaste;
15
      fixture.detectChanges();
16
     
17
    })

Hier sind die Tests.

1
 it('should display the modal when the view Paste button is clicked',() => {
2
    
3
    fixture.detectChanges();
4
    expect(component.showPasteModal).toBeTruthy("Show should be true");
5
    expect(element.innerHTML).toContain("source-modal");
6
})
7
8
it('should display title, language and paste', () => {
9
    expect(element.textContent).toContain(mockPaste.title, "it should contain title");
10
    expect(element.textContent).toContain(mockPaste.language, "it should contain the language");
11
    expect(element.textContent).toContain(mockPaste.paste, "it should contain the paste");
12
});

Der Test setzt voraus, dass die Komponente über eine paste-Eigenschaft verfügt, die Eingaben von der übergeordneten Komponente akzeptiert. Zuvor haben wir ein Beispiel dafür gesehen, wie von der untergeordneten Komponente ausgegebene Ereignisse getestet werden können, ohne dass die Logik der Hostkomponente in unsere Tests einbezogen werden muss. In ähnlicher Weise ist es zum Testen der Eingabeeigenschaften einfacher, die Eigenschaft auf ein Scheinobjekt festzulegen und zu erwarten, dass die Werte des Scheinobjekts im HTML-Code angezeigt werden.

Das modale Fenster enthält viele Schaltflächen, und es wäre keine schlechte Idee, eine Spezifikation zu schreiben, um sicherzustellen, dass die Schaltflächen in der Vorlage verfügbar sind.

1
it('should have all the buttons',() => {
2
      expect(element.innerHTML).toContain('Edit Paste');
3
      expect(element.innerHTML).toContain('Delete');
4
      expect(element.innerHTML).toContain('Close');
5
});

Beheben wir die fehlgeschlagenen Tests, bevor wir komplexere Tests durchführen.

1
<!--- view-paste.component.html -->
2
<div class="view-paste">
3
    <button class="text-primary button-text"  (click)="showPaste()"> View Paste </button>
4
  <div *ngIf="showPasteModal" id="source-modal" class="modal fade in">
5
    <div class="modal-dialog">
6
      <div class="modal-content">
7
        <div class="modal-header">
8
          <button type="button" class="close" (click)='onClose()' aria-hidden="true">&times;</button>
9
          <h4 class="modal-title">{{paste.title}} </h4>
10
        </div>
11
        <div class="modal-body">
12
      	  <h5> {{paste.language}} </h5>     	
13
      	  <pre><code>{{paste.paste}}</code></pre>
14
        </div>
15
        <div class="modal-footer">
16
          <button type="button" class="btn btn-default" (click)="onClose()" data-dismiss="modal">Close</button>
17
          <button type="button" *ngIf="!editEnabled" (click) = "onEdit()" class="btn btn-primary">Edit Paste</button>
18
           <button type = "button"  (click) = "onDelete()" class="btn btn-danger"> Delete Paste </button>
19
        </div>
20
      </div>
21
    </div>
22
  </div>
23
</div>
24
       
1
/* view-paste.component.ts */
2
3
export class ViewPasteComponent implements OnInit {
4
5
  @Input() paste: Pastebin;
6
  @Output() updatePasteSuccess: EventEmitter<Pastebin> = new EventEmitter<Pastebin>();
7
  @Output() deletePasteSuccess: EventEmitter<Pastebin> = new EventEmitter<Pastebin>();
8
9
  showPasteModal:boolean ;
10
  readonly languages = Languages;
11
  
12
  constructor(private pasteServ: PastebinService) { }
13
14
  ngOnInit() {
15
      this.showPasteModal = false;
16
  }
17
  //To make the modal window visible

18
  public showPaste() {
19
  	this.showPasteModal = true;
20
  }
21
  //Invoked when edit button is clicked

22
  public onEdit() { }
23
  
24
  //invoked when save button is clicked

25
  public onSave() { }
26
  
27
  //invoked when close button is clicked

28
  public onClose() {
29
  	this.showPasteModal = false;
30
  }
31
  
32
  //invoked when Delete button is clicked

33
  public onDelete() { }
34
  
35
}

Es reicht nicht aus, die Paste anzeigen zu können. Die Komponente ist auch für das Bearbeiten, Aktualisieren und Löschen einer Paste verantwortlich. Die Komponente sollte über eine editEnabled-Eigenschaft verfügen, die auf true gesetzt wird, wenn der Benutzer auf die Schaltfläche Einfügen bearbeiten klickt.

1
it('and clicking it should make the paste editable', () => {
2
3
    component.onEdit();
4
    fixture.detectChanges();
5
    expect(component.editEnabled).toBeTruthy();
6
    //Now it should have a save button

7
    expect(element.innerHTML).toContain('Save');
8
      
9
});

Fügen Sie editEnabled=true; in die Methode onEdit() ein, um die erste expect-Anweisung zu löschen.

In der folgenden Vorlage wird mit der Anweisung ngIf zwischen dem Ansichtsmodus und dem Bearbeitungsmodus umgeschaltet. <ng-container> ist ein logischer Container, mit dem mehrere Elemente oder Knoten gruppiert werden.

1
  <div *ngIf="showPasteModal" id="source-modal" class="modal fade in" >
2
3
    <div class="modal-dialog">
4
      <div class="modal-content">
5
        <!---View mode -->
6
        <ng-container *ngIf="!editEnabled">
7
        
8
          <div class="modal-header">
9
            <button type="button" class="close" (click)='onClose()' data-dismiss="modal" aria-hidden="true">&times;</button>
10
            <h4 class="modal-title"> {{paste.title}} </h4>
11
          </div>
12
          <div class="modal-body">
13
              <h5> {{paste.language}} </h5>
14
      		  <pre><code>{{paste.paste}}</code>
15
            </pre>
16
      	
17
      	  </div>
18
          <div class="modal-footer">
19
            <button type="button" class="btn btn-default" (click)="onClose()" data-dismiss="modal">Close</button>
20
            <button type="button" (click) = "onEdit()" class="btn btn-primary">Edit Paste</button>
21
            <button type = "button"  (click) = "onDelete()" class="btn btn-danger"> Delete Paste </button>
22
23
          </div>
24
        </ng-container>
25
        <!---Edit enabled mode -->
26
        <ng-container *ngIf="editEnabled">
27
          <div class="modal-header">
28
             <button type="button" class="close" (click)='onClose()' data-dismiss="modal" aria-hidden="true">&times;</button>
29
             <h4 class="modal-title"> <input *ngIf="editEnabled" name="title" [(ngModel)] = "paste.title"> </h4>
30
          </div>
31
          <div class="modal-body">
32
            <h5>
33
                <select name="category"  [(ngModel)]="paste.language">
34
                  <option   *ngFor ="let language of languages" value={{language}}> {{language}} </option>
35
                </select>
36
            </h5>
37
38
           <textarea name="paste" [(ngModel)] = "paste.paste">{{paste.paste}} </textarea>
39
          </div>
40
          <div class="modal-footer">
41
             <button type="button" class="btn btn-default" (click)="onClose()" data-dismiss="modal">Close</button>
42
             <button type = "button" *ngIf="editEnabled" (click) = "onSave()" class="btn btn-primary"> Save Paste </button>
43
             <button type = "button"  (click) = "onDelete()" class="btn btn-danger"> Delete Paste </button>      
44
          </div>
45
        </ng-container>
46
      </div>
47
    </div>
48
  </div>

Die Komponente sollte über zwei Output()-Ereignisemitter verfügen, einen für die updatePasteSuccess-Eigenschaft und einen für deletePasteSuccess. Der folgende Test bestätigt Folgendes:

  1. Die Vorlage der Komponente akzeptiert Eingaben.
  2. Die Vorlageneingaben sind an die paste-Eigenschaft der Komponente gebunden.
  3. Wenn der Aktualisierungsvorgang erfolgreich ist, gibt updatePasteSuccess ein Ereignis mit dem aktualisierten Einfügen aus.
1
it('should take input values', fakeAsync(() => {
2
      component.editEnabled= true;
3
      component.updatePasteSuccess.subscribe((res:any) => {response = res},)
4
      fixture.detectChanges();
5
6
      inputTitle= element.querySelector("input");
7
      inputTitle.value = mockPaste.title;
8
      inputTitle.dispatchEvent(new Event("input"));
9
      
10
      expect(mockPaste.title).toEqual(component.paste.title);
11
    
12
      component.onSave();
13
       //first round of detectChanges()

14
      fixture.detectChanges();
15
16
      //the tick() operation. Don't forget to import tick

17
      tick();
18
19
      //Second round of detectChanges()

20
      fixture.detectChanges();
21
      expect(response.title).toEqual(mockPaste.title);
22
      expect(spyOnUpdate.calls.any()).toBe(true, 'updatePaste() method should be called');
23
      
24
}))

Der offensichtliche Unterschied zwischen diesem Test und den vorherigen ist die Verwendung der fakeAsync-Funktion. fakeAsync ist mit async vergleichbar, da beide Funktionen zum Ausführen von Tests in einer asynchronen Testzone verwendet werden. Mit fakeAsync sieht Ihr Look-Test jedoch synchroner aus.

Die tick()-Methode ersetzt fixture.whenStable(). Then(), und der Code ist aus Entwicklersicht besser lesbar. Vergessen Sie nicht, fakeAsync zu importieren und von @angular/core/testing anzukreuzen.

Schließlich ist hier die Spezifikation zum Löschen einer Paste.

1
it('should delete the paste', fakeAsync(()=> {
2
      
3
      component.deletePasteSuccess.subscribe((res:any) => {response = res},)
4
      component.onDelete();
5
      fixture.detectChanges();
6
      tick();
7
      fixture.detectChanges();
8
      expect(spyOnDelete.calls.any()).toBe(true, "Pastebin deletePaste() method should be called");
9
      expect(response).toBeTruthy();
10
}))
11
    

Wir sind fast fertig mit den Komponenten. Hier ist der endgültige Entwurf der ViewPaste-Komponente.

1
/*view-paste.component.ts*/
2
export class ViewPasteComponent implements OnInit {
3
4
  @Input() paste: Pastebin;
5
  @Output() updatePasteSuccess: EventEmitter<Pastebin> = new EventEmitter<Pastebin>();
6
  @Output() deletePasteSuccess: EventEmitter<Pastebin> = new EventEmitter<Pastebin>();
7
8
  showPasteModal:boolean ;
9
  editEnabled: boolean;
10
  readonly languages = Languages;
11
  
12
  constructor(private pasteServ: PastebinService) { }
13
14
  ngOnInit() {
15
      this.showPasteModal = false;
16
  	  this.editEnabled = false;
17
  }
18
  //To make the modal window visible

19
  public showPaste() {
20
  	this.showPasteModal = true;
21
  }
22
  //Invoked when the edit button is clicked

23
  public onEdit() {
24
  	this.editEnabled=true;
25
  }
26
  //Invoked when the save button is clicked

27
  public onSave() {
28
 	this.pasteServ.updatePaste(this.paste).then( () => {
29
  		this.editEnabled= false;
30
        this.updatePasteSuccess.emit(this.paste);
31
  	})
32
  }
33
 //Invoked when the close button is clicked

34
  public onClose() {
35
  	this.showPasteModal = false;
36
  }
37
 
38
 //Invoked when the delete button is clicked

39
  public onDelete() {
40
	  this.pasteServ.deletePaste(this.paste).then( () => {
41
        this.deletePasteSuccess.emit(this.paste);
42
 	    this.onClose();
43
 	  })
44
  }
45
  
46
}

Die übergeordnete Komponente (pastebin.component.ts) muss mit Methoden aktualisiert werden, um die von der untergeordneten Komponente ausgegebenen Ereignisse zu verarbeiten.

1
/*pastebin.component.ts */
2
  public onUpdatePaste(newPaste: Pastebin) {
3
    this.pastebin.map((paste)=> { 
4
       if(paste.id==newPaste.id) {
5
         paste = newPaste;
6
       } 
7
    })
8
  }
9
10
  public onDeletePaste(p: Pastebin) {
11
   this.pastebin= this.pastebin.filter(paste => paste !== p);
12
   
13
  }

Hier ist die aktualisierte pastebin.component.html:

1
<tbody>
2
    	<tr *ngFor="let paste of pastebin">
3
			<td> {{paste.id}} </td>
4
			<td> {{paste.title}} </td>
5
			<td> {{paste.language}} </td>
6
			
7
			<td> <app-view-paste [paste] = paste (updatePasteSuccess)= 'onUpdatePaste($event)' (deletePasteSuccess)= 'onDeletePaste($event)'> </app-view-paste></td> 
8
		</tr>
9
	</tbody>
10
	<app-add-paste (addPasteSuccess)= 'onAddPaste($event)'> </app-add-paste> 

Einrichten von Routen

Um eine geroutete Anwendung zu erstellen, benötigen wir einige weitere Lagerkomponenten, damit wir einfache Routen erstellen können, die zu diesen Komponenten führen. Ich habe eine Info-Komponente und eine Kontaktkomponente erstellt, damit wir sie in eine Navigationsleiste einfügen können. AppComponent enthält die Logik für die Routen. Wir werden die Tests für Routen schreiben, nachdem wir damit fertig sind.

Importieren Sie zunächst RouterModule und Routes in AppModule (und AppTestingModule).

1
import { RouterModule, Routes } from '@angular/router';

Definieren Sie als Nächstes Ihre Routen und übergeben Sie die Routendefinition an die RouterModule.forRoot-Methode.

1
const appRoutes :Routes = [
2
  { path: '', component: PastebinComponent },
3
  { path: 'about', component: AboutComponent },
4
  { path: 'contact', component: ContactComponent},
5
  ];
6
 
7
 imports: [
8
    BrowserModule, 
9
    FormsModule,
10
    HttpModule,
11
    InMemoryWebApiModule.forRoot(InMemoryDataService),
12
    RouterModule.forRoot(appRoutes),
13
   
14
  ],

Alle am AppModule vorgenommenen Änderungen sollten auch am AppTestingModule vorgenommen werden. Wenn Sie jedoch beim Ausführen der Tests auf einen Fehler "No base href set" stoßen, fügen Sie dem providers-Array Ihres AppTestingModule die folgende Zeile hinzu.

1
{provide: APP_BASE_HREF, useValue: '/'}

Fügen Sie nun den folgenden Code zu app.component.html hinzu.

1
<nav class="navbar navbar-inverse">
2
   <div class="container-fluid">
3
       <div class="navbar-header">
4
      	   <div class="navbar-brand" >{{title}}</div>
5
      </div>
6
   	  <ul class="nav navbar-nav bigger-text">
7
    	  <li>
8
	    	 <a routerLink="" routerLinkActive="active">Pastebin Home</a>
9
	      </li>
10
	      <li>
11
	     	 <a routerLink="/about" routerLinkActive="active">About Pastebin</a>
12
	      </li>
13
	      <li>
14
	     	 <a routerLink="/contact" routerLinkActive="active"> Contact </a>
15
	       </li>
16
	  </ul>
17
   </div>
18
</nav>
19
  <router-outlet></router-outlet>
20
21

routerLink ist eine Direktive, mit der ein HTML-Element an eine Route gebunden wird. Wir haben es hier mit dem HTML-Ankertag verwendet. RouterOutlet ist eine weitere Anweisung, die die Stelle in der Vorlage markiert, an der die Ansicht des Routers angezeigt werden soll.

Das Testen von Routen ist etwas schwierig, da es mehr Interaktion mit der Benutzeroberfläche erfordert. Hier ist der Test, der prüft, ob die Ankerverbindungen funktionieren.

1
describe('AppComponent', () => {
2
  beforeEach(async(() => {
3
    TestBed.configureTestingModule({
4
      imports: [AppTestingModule],
5
      
6
    }).compileComponents();
7
  }));
8
9
10
  it(`should have as title 'Pastebin Application'`, async(() => {
11
    const fixture = TestBed.createComponent(AppComponent);
12
    const app = fixture.debugElement.componentInstance;
13
    expect(app.title).toEqual('Pastebin Application');
14
  }));
15
16
17
  it('should go to url',
18
    fakeAsync((inject([Router, Location], (router: Router, location: Location) => {
19
      let anchorLinks,a1,a2,a3;
20
    let fixture = TestBed.createComponent(AppComponent);
21
    fixture.detectChanges();
22
     //Create an array of anchor links

23
     anchorLinks= fixture.debugElement.queryAll(By.css('a'));
24
     a1 = anchorLinks[0];
25
     a2 = anchorLinks[1];
26
     a3 = anchorLinks[2];
27
     
28
     //Simulate click events on the anchor links

29
     a1.nativeElement.click();
30
     tick();
31
     
32
     expect(location.path()).toEqual("");
33
34
     a2.nativeElement.click();
35
     tick()
36
     expect(location.path()).toEqual("/about");
37
38
      a3.nativeElement.click();
39
      tick()
40
      expect(location.path()).toEqual("/contact");
41
    
42
  }))));
43
});

Wenn alles gut geht, sollten Sie so etwas sehen.

Screenshot of Karma test runner on Chrome displaying the final test resultsScreenshot of Karma test runner on Chrome displaying the final test resultsScreenshot of Karma test runner on Chrome displaying the final test results

Letzter Schliff

Fügen Sie Ihrem Projekt ein ansprechendes Bootstrap-Design hinzu und bedienen Sie Ihr Projekt, falls Sie dies noch nicht getan haben.

1
ng serve

Zusammenfassung

Wir haben eine vollständige Anwendung von Grund auf in einer testgetriebenen Umgebung geschrieben. Ist das nicht toll? In diesem Tutorial haben wir gelernt:

  • entwerfen einer Komponente mithilfe des ersten Testansatzes
  • schreiben von Komponententests und grundlegenden UI-Tests für Komponenten
  • über Angulars Testdienstprogramme und wie man sie in unsere Tests einbezieht
  • Informationen zur Verwendung von async() und fakeAsync() zum Ausführen asynchroner Tests
  • die Grundlagen des Routings in Angular und des Schreibens von Routentests

Ich hoffe, Ihnen hat der TDD-Workflow gefallen. Bitte kontaktieren Sie uns über die Kommentare und teilen Sie uns Ihre Meinung mit!