Jakiś sposób na przetestowanie EventEmittera w Angular2?

87

Mam składnik, który używa EventEmitter i EventEmitter jest używany, gdy ktoś na stronie zostanie kliknięty. Czy istnieje sposób, w jaki mogę obserwować EventEmitter podczas testu jednostkowego i użyć TestComponentBuilder, aby kliknąć element, który wyzwala metodę EventEmitter.next () i zobaczyć, co zostało wysłane?

tallkid24
źródło
Czy możesz dostarczyć plunker, który pokazuje, czego próbowałeś, a potem mogę rzucić okiem, aby dodać brakujące elementy.
Günter Zöchbauer

Odpowiedzi:

205

Twój test mógłby wyglądać następująco:

it('should emit on click', () => {
   const fixture = TestBed.createComponent(MyComponent);
   // spy on event emitter
   const component = fixture.componentInstance; 
   spyOn(component.myEventEmitter, 'emit');

   // trigger the click
   const nativeElement = fixture.nativeElement;
   const button = nativeElement.querySelector('button');
   button.dispatchEvent(new Event('click'));

   fixture.detectChanges();

   expect(component.myEventEmitter.emit).toHaveBeenCalledWith('hello');
});

kiedy twój komponent jest:

@Component({ ... })
class MyComponent {
  @Output myEventEmitter = new EventEmitter<string>();

  buttonClick() {
    this.myEventEmitter.emit('hello');
  }
}
cexbrayat
źródło
1
Jeśli jest to kotwica, którą klikam zamiast przycisku, czy selektor zapytania byłby po prostu przyciskiem zamiast przycisku? Używam czegoś dokładnie podobnego do tego komponentu, ale 'oczekiwać (wartość) .toBe (' cześć ');' nigdy nie ucieka. Zastanawiam się, czy to dlatego, że zamiast tego jest kotwicą.
tallkid24,
Zaktualizowałem swoją odpowiedź, wprowadzając czystszy sposób testowania, używając szpiega zamiast prawdziwego emitera i myślę, że to powinno działać (tak właśnie robię dla próbek w moim ebooku).
cexbrayat,
To działa świetnie, dzięki! Jestem nowy w programowaniu front-end, zwłaszcza testowaniu jednostkowym. To bardzo pomaga. Nie wiedziałem nawet, że istnieje funkcja spyOn.
tallkid24,
Jak mogę to przetestować, jeśli używam TestComponent do opakowywania MyComponent? Na przykład html = <my-component (myEventEmitter)="function($event)"></my-component>iw teście robię: tcb.overrideTemplate (TestComponent, html) .createAsync (TestComponent)
bekos
1
świetna odpowiedź - bardzo zwięzła i na temat - bardzo przydatny ogólny wzorzec
danday74
48

Możesz użyć szpiega, zależy od twojego stylu. Oto, w jaki sposób możesz łatwo wykorzystać szpiega, aby sprawdzić, czy emitnie został zwolniony ...

it('should emit on click', () => {
    spyOn(component.eventEmitter, 'emit');
    component.buttonClick();
    expect(component.eventEmitter.emit).toHaveBeenCalled();
    expect(component.eventEmitter.emit).toHaveBeenCalledWith('bar');
});
Joshua Michael Wagoner
źródło
Zaktualizowałem odpowiedź, aby nie używać niepotrzebnego async lub fakeAsync, co może być problematyczne, jak wskazano w poprzednich komentarzach. Ta odpowiedź pozostaje dobrym rozwiązaniem od wersji Angular 9.1.7. Jeśli coś się zmieni, zostaw komentarz, a zaktualizuję tę odpowiedź. dziękuję wszystkim, którzy komentowali / moderowali.
Joshua Michael Wagoner
Czy nie powinieneś być expectrzeczywistym szpiegiem (wynik spyOn()rozmowy)?
Jurij
Brakowało mi „component.buttonClick ()” po Spyonie. To rozwiązanie rozwiązało mój problem. Wielkie dzięki!
Pearl
2

Możesz zasubskrybować emiter lub powiązać z nim, jeśli jest to @Output(), w szablonie nadrzędnym i sprawdzić komponent nadrzędny, czy powiązanie zostało zaktualizowane. Możesz również wywołać zdarzenie kliknięcia, a następnie subskrypcja powinna zostać uruchomiona.

Günter Zöchbauer
źródło
Więc gdybym lubił emitter.subscribe (data => {}); w jaki sposób otrzymam wyjście next ()?
tallkid24
Dokładnie. Lub szablon w TestComponenthas <my-component (someEmitter)="value=$event">(gdzie someEmitterjest @Output()) to valuewłaściwość TextComponentpowinna zostać zaktualizowana o wysłane zdarzenie.
Günter Zöchbauer
0

Miałem wymóg przetestowania długości emitowanej tablicy. Tak więc zrobiłem to na tle innych Answers.

expect(component.myEmitter.emit).toHaveBeenCalledWith([anything(), anything()]);
prabhatojha
źródło
-2

Chociaż najwyżej ocenione odpowiedzi działają, nie są one przykładem dobrych praktyk testowania, więc pomyślałem, że poszerzę odpowiedź Güntera o kilka praktycznych przykładów.

Wyobraźmy sobie, że mamy następujący prosty element:

@Component({
  selector: 'my-demo',
  template: `
    <button (click)="buttonClicked()">Click Me!</button>
  `
})
export class DemoComponent {
  @Output() clicked = new EventEmitter<string>();

  constructor() { }

  buttonClicked(): void {
    this.clicked.emit('clicked!');
  }
}

Składnik jest testowanym systemem, szpiegowanie jego części powoduje przerwanie hermetyzacji. Testy komponentów kątowych powinny wiedzieć tylko o trzech rzeczach:

  • DOM (dostępny przez np fixture.nativeElement.querySelector );
  • Nazwy @Inputs i@Output S; i
  • Usługi współpracy (wstrzykiwane za pośrednictwem systemu DI).

Wszystko, co wiąże się z bezpośrednim wywoływaniem metod w instancji lub szpiegowaniem części komponentu, jest zbyt ściśle powiązane z implementacją i może powodować tarcie w refaktoryzacji - test podwójny powinien być używany tylko dla współpracowników. W tym przypadku, ponieważ nie mamy współpracowników, nie powinniśmy potrzebować żadnych fałszywych szpiegów ani innych podwójnych testów.


Jednym ze sposobów sprawdzenia tego jest zapisanie się bezpośrednio do emitera, a następnie wywołanie akcji kliknięcia (zobacz Komponent z wejściami i wyjściami ):

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

  beforeEach(async(() => {
    TestBed.configureTestingModule({
      declarations: [ DemoComponent ]
    })
    .compileComponents();
  }));

  beforeEach(() => {
    fixture = TestBed.createComponent(DemoComponent);
    component = fixture.componentInstance;
    fixture.detectChanges();
  });

  it('should emit when clicked', () => {
    let emitted: string;
    component.clicked.subscribe((event: string) => {
      emitted = event;
    });

    fixture.nativeElement.querySelector('button').click();

    expect(emitted).toBe('clicked!');
  });
});

Chociaż to współdziała bezpośrednio z wystąpieniem składnika, nazwa @Output jest częścią publicznego interfejsu API, więc nie jest zbyt ściśle powiązana.


Alternatywnie możesz utworzyć prostego hosta testowego (zobacz Komponent wewnątrz hosta testowego ) i faktycznie zamontować swój komponent:

@Component({
  selector: 'test-host',
  template: `
    <my-demo (clicked)="onClicked($event)"></my-demo>
  `
})
class TestHostComponent {
  lastClick = '';

  onClicked(value: string): void {
    this.lastClick = value;
  }
}

następnie przetestuj komponent w kontekście:

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

  beforeEach(async(() => {
    TestBed.configureTestingModule({
      declarations: [ TestHostComponent, DemoComponent ]
    })
    .compileComponents();
  }));

  beforeEach(() => {
    fixture = TestBed.createComponent(TestHostComponent);
    component = fixture.componentInstance;
    fixture.detectChanges();
  });

  it('should emit when clicked', () => {
    fixture.nativeElement.querySelector('button').click();

    expect(component.lastClick).toBe('clicked!');
  });
});

componentInstanceTu jest gospodarzem testu , więc możemy być pewni, że nie jesteśmy zbyt połączeniu ze składnikiem jesteśmy faktycznie testowania.

jonrsharpe
źródło