Unit-Test-Klickereignis in Angular

75

Ich versuche, meiner Angular 2-App Komponententests hinzuzufügen. In einer meiner Komponenten befindet sich eine Schaltfläche mit einem (click)Handler. Wenn der Benutzer auf die Schaltfläche klickt, wird eine Funktion aufgerufen, die in der .tsKlassendatei definiert ist . Diese Funktion gibt im Fenster console.log eine Meldung aus, dass die Schaltfläche gedrückt wurde. Mein aktueller Testcode testet zum Drucken der console.logNachricht:

describe('Component: ComponentToBeTested', () => {
    var component: ComponentToBeTested;

    beforeEach(() => {
        component = new ComponentToBeTested();
        spyOn(console, 'log');
    });

    it('should call onEditButtonClick() and print console.log', () => {
        component.onEditButtonClick();
        expect(console.log).toHaveBeenCalledWith('Edit button has been clicked!);
    });
});

Dies testet jedoch nur die Controller-Klasse, nicht den HTML-Code. Ich möchte nicht nur testen, ob die Protokollierung erfolgt, wenn sie onEditButtonClickaufgerufen wird. Ich möchte auch testen, onEditButtonClickwas aufgerufen wird, wenn der Benutzer auf die in der HTML-Datei der Komponente definierte Schaltfläche zum Bearbeiten klickt. Wie kann ich das machen?

Aiguo
quelle

Antworten:

109

Mein Ziel ist es zu überprüfen, ob der 'onEditButtonClick' aufgerufen wird, wenn der Benutzer auf die Schaltfläche Bearbeiten klickt und nicht nur das zu druckende console.log überprüft.

Sie müssen den Test zuerst mit dem Winkel einrichten TestBed. Auf diese Weise können Sie die Schaltfläche tatsächlich greifen und darauf klicken. Sie konfigurieren ein Modul wie @NgModulefür die Testumgebung

import { TestBed, async, ComponentFixture } from '@angular/core/testing';

describe('', () => {
  let fixture: ComponentFixture<TestComponent>;
  let component: TestComponent;

  beforeEach(async(() => {
    TestBed.configureTestingModule({
      imports: [ ],
      declarations: [ TestComponent ],
      providers: [  ]
    }).compileComponents().then(() => {
      fixture = TestBed.createComponent(TestComponent);
      component = fixture.componentInstance;
    });
  }));
});

Dann müssen Sie die onEditButtonClickMethode ausspionieren, auf die Schaltfläche klicken und überprüfen, ob die Methode aufgerufen wurde

it('should', async(() => {
  spyOn(component, 'onEditButtonClick');

  let button = fixture.debugElement.nativeElement.querySelector('button');
  button.click();

  fixture.whenStable().then(() => {
    expect(component.onEditButtonClick).toHaveBeenCalled();
  });
}));

Hier müssen wir einen asyncTest ausführen , da der Klick auf die Schaltfläche eine asynchrone Ereignisbehandlung enthält, und warten, bis das Ereignis durch Aufrufen verarbeitet wirdfixture.whenStable()

Siehe auch:

AKTUALISIEREN

Es wird jetzt bevorzugt, eine fakeAsync/tickKombination im Gegensatz zur async/whenStableKombination zu verwenden. Letzteres sollte verwendet werden, wenn ein XHR-Aufruf erfolgt, da dieser fakeAsyncnicht unterstützt wird. Anstelle des oben überarbeiteten Codes würde es also so aussehen

it('should', fakeAsync(() => {
  spyOn(component, 'onEditButtonClick');

  let button = fixture.debugElement.nativeElement.querySelector('button');
  button.click();
  tick();
  expect(component.onEditButtonClick).toHaveBeenCalled();

}));

Vergessen Sie nicht zu importieren fakeAsyncundtick

Paul Samsotha
quelle
2
Was ist, wenn wir mehr als eine Schaltfläche in der HTML-Datei haben? So etwas tun wie: fixture.debugElement.nativeElement.querySelector ('button');
Aiguo
2
sucht nach 'button' in der HTML-Datei, aber was haben wir mehr als zwei einen Button? Wie soll ich zum Testen auf das Auftreten der zweiten Schaltfläche verweisen?
Aiguo
2
Ich habe die Lösung! button = fixture.debugElement.queryAll (By.css ('button'); button1 = button [0];
Aiguo
2
Ich habe gerade einen Tag damit verbracht herauszufinden, warum dies bei mir nicht funktioniert hat, nur um zu erkennen, dass ich auf das Element meiner Komponente geklickt habe, nicht auf das Div darin, auf das ich tatsächlich klicke.
Majinnaibu
1
Was ist der Unterschied zwischen button.triggerEventHandler ('click', null) und button.click90?
Jitenagarwal19
46

Ereignisse können mit den von bereitgestellten Funktionen async/ getestet werden , da jedes Ereignis im Browser asynchron ist und in die Ereignisschleife / -warteschlange verschoben wird.fakeAsync'@angular/core/testing'

Im Folgenden finden Sie ein sehr einfaches Beispiel zum Testen des Klickereignisses mit fakeAsync.

Die fakeAsyncFunktion ermöglicht einen linearen Codierungsstil, indem der Testkörper in einer speziellen fakeAsyncTestzone ausgeführt wird.

Hier teste ich eine Methode, die vom click-Ereignis aufgerufen wird.

it('should', fakeAsync( () => {
    fixture.detectChanges();
    spyOn(componentInstance, 'method name'); //method attached to the click.
    let btn = fixture.debugElement.query(By.css('button'));
    btn.triggerEventHandler('click', null);
    tick(); // simulates the passage of time until all pending asynchronous activities finish
    fixture.detectChanges();
    expect(componentInstance.methodName).toHaveBeenCalled();
}));

Im Folgenden finden Sie Informationen zu Angular-Dokumenten :

Der Hauptvorteil von fakeAsync gegenüber async besteht darin, dass der Test synchron zu sein scheint. Es gibt keine then(...)Möglichkeit, den sichtbaren Kontrollfluss zu stören. Die Rückgabe des Versprechens fixture.whenStableist weg, ersetzt durchtick()

Es gibt Einschränkungen. Beispielsweise können Sie innerhalb von a keinen XHR-Anruf tätigenfakeAsync

Mav55
quelle
Gibt es einen Grund, diese Lösung oder die akzeptierte Lösung zu bevorzugen? Als eckiger Noob habe ich keine Ahnung, was "besser" ist
Adam Hughes
2
@AdamHughes fakeAsync ist laut eckiger Dokumentation leichter zu lesen und zu verstehen. Aber Sie können wählen, was am besten zu Ihnen passt. Es gibt nichts Schöneres als eine akzeptierte Lösung bei der Verwendung von aync oder fakeAsync.
Mav55
1
Ich schrieb eine Antwort hier über den Vergleich zwischen der Verwendung von asyncvs. fakeAsync. Auch wenn in meiner Antwort habe ich asyncim Allgemeinen, würde meine Präferenz zu verwenden sein fakeAsync.
Paul Samsotha
Sie sollten nur tick()s haben, wenn Ihr Code aufruft setTimeout. Ich glaube nicht, dass ein Häkchen benötigt wird. Siehe stackoverflow.com/a/50574080/227299
Juan Mendes
9

Ich benutze Angular 6 . Ich folgte der Antwort von Mav55 und es funktionierte. Ich wollte jedoch sicherstellen, dass fixture.detectChanges();es wirklich notwendig war, also entfernte ich es und es funktionierte immer noch. Dann entfernte ich, um tick();zu sehen, ob es funktionierte und es tat. Schließlich entfernte ich den Test aus der fakeAsync()Verpackung und überraschte, dass es funktionierte.

Am Ende hatte ich folgendes:

it('should call onClick method', () => {
  const onClickMock = spyOn(component, 'onClick');
  fixture.debugElement.query(By.css('button')).triggerEventHandler('click', null);
  expect(onClickMock).toHaveBeenCalled();
});

Und es hat gut funktioniert.

Parziphal
quelle
Meine allgemeine Strategie bestand darin, immer aufzurufen fixture.detectChanges(), um die Lebenszyklusereignisse von Angular auszulösen. Es kann sein, dass Ihre Komponente keine hat ngOnInitoder dass der Test nicht bestanden werden musste.
SnailCoil
@SnailCoil oder er könnte die automatische Änderungserkennung ( angle.io/guide/testing#automatic-change-detection ) verwenden
Maksymilian Majer
Einige Dom-Ereignisse sind synchron. Beispielsweise wird dokumentiert, dass das Eingabeereignis synchron ist. Wenn das, was mit dem Schaltflächenklickereignis auftritt, wahr ist, bedeutet dies, dass das Schaltflächenklickereignis auch synchron ist. Aber es gibt eine Menge von Dom Ereignisse, die asynchron , in welchem Fall Sie würde verwenden müssen asyncoder fakeAsync. Um sicher zu gehen, sehe ich nichts falsches daran anzunehmen, dass alle Ereignisse asynchron sind und nur asyncoder verwenden fakeAsync(es ist Ihre Präferenz).
Paul Samsotha
Wenn Sie nicht in einem vollständigen Browser ausgeführt werden, ist es besser, By.css anstelle von querySelector zu verwenden. (src stackoverflow.com/questions/44400566/… ).
Ambroise Rabier
@PaulSamsotha Scheint eine schlechte Idee zu sein, Ihren Code mit tick()s zu verunreinigen, weil möglicherweise etwas asynchron ist? Sie sollten wissen, was asynchron ist und was nicht. Das Auslösen von Maus- / Tastatur- / Eingabeereignissen ist immer synchron.
Juan Mendes
1

Ich hatte ein ähnliches Problem (detaillierte Erklärung unten) und habe es (in jasmine-core: 2.52) gelöst, indem ich die tickFunktion mit der gleichen (oder größeren) Anzahl von Millisekunden wie im ursprünglichen setTimeoutAufruf verwendet habe.

Wenn ich zum Beispiel eine hätte setTimeout(() => {...}, 2500);(damit sie nach 2500 ms ausgelöst wird), würde ich anrufen tick(2500), und das würde das Problem lösen.

Was ich in meiner Komponente hatte, als Reaktion auf einen Klick auf die Schaltfläche Löschen :

delete() {
    this.myService.delete(this.id)
      .subscribe(
        response => {
          this.message = 'Successfully deleted! Redirecting...';
          setTimeout(() => {
            this.router.navigate(['/home']);
          }, 2500); // I wait for 2.5 seconds before redirect
        });
  }

Sie ist mein Arbeitstest :

it('should delete the entity', fakeAsync(() => {
    component.id = 1; // preparations..
    component.getEntity(); // this one loads up the entity to my component
    tick(); // make sure that everything that is async is resolved/completed
    expect(myService.getMyThing).toHaveBeenCalledWith(1);
    // more expects here..
    fixture.detectChanges();
    tick();
    fixture.detectChanges();
    const deleteButton = fixture.debugElement.query(By.css('.btn-danger')).nativeElement;
    deleteButton.click(); // I've clicked the button, and now the delete function is called...

    tick(2501); // timeout for redirect is 2500 ms :)  <-- solution

    expect(myService.delete).toHaveBeenCalledWith(1);
    // more expects here..
  }));

PS Eine großartige Erklärung zu fakeAsyncund allgemeine Asynchronität beim Testen finden Sie hier: Ein Video zu Teststrategien mit Angular 2 - Julie Ralph, ab 8:10 Uhr, dauert 4 Minuten :)

Aleksandar
quelle
0

Um das Ereignis des Schaltflächenaufrufs zuerst zu überprüfen, müssen wir die Methode ausspionieren, die nach dem Klicken auf die Schaltfläche aufgerufen wird, damit unsere erste Zeile "spyOn spy methode" lautet. Nehmen Sie zwei Argumente. () 'Nur Name erforderlich, dann müssen wir das Objekt der Schaltfläche zum Klicken machen. Jetzt müssen wir den Ereignishandler auslösen, zu dem wir das Klickereignis hinzufügen. Dann erwarten wir, dass unser Code die Submit-Methode einmal aufruft

it('should call onSubmit method',() => {
    spyOn(component, 'onSubmit');
    let submitButton: DebugElement = 
    fixture.debugElement.query(By.css('button[type=submit]'));
    fixture.detectChanges();
    submitButton.triggerEventHandler('click',null);
    fixture.detectChanges();
    expect(component.onSubmit).toHaveBeenCalledTimes(1);
});
Afzal Azad
quelle
Scheint bei mir nicht zu funktionieren. Ich benutze Angular 7. Ich denke, der richtige Ansatz ist:it('should call onSubmit method',() => { let mock = spyOn(component, 'onSubmit'); let submitButton: DebugElement = fixture.debugElement.query(By.css('button[type=submit]')); fixture.detectChanges(); submitButton.triggerEventHandler('click',null); fixture.detectChanges(); expect(mock).toHaveBeenCalledTimes(1); });
Todor Todorov