131

I have a component that uses an EventEmitter and the EventEmitter is used when someone on the page is clicked. Is there any way that I can observe the EventEmitter during a unit test, and use TestComponentBuilder to click the element that triggers the EventEmitter.next() method and see what was sent?

tallkid24
  • 1,697
  • 4
  • 16
  • 20

6 Answers6

317

Your test could be:

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');
});

when your component is:

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

  buttonClick() {
    this.myEventEmitter.emit('hello');
  }
}
cexbrayat
  • 17,772
  • 4
  • 27
  • 22
  • 1
    If it is a anchor that I'm clicking instead of a button, would the query selector just be a instead of button? I'm using something exactly like that component, but the 'expect(value).toBe('hello');' never gets run. I wonder if it is because it is an anchor instead. – tallkid24 Feb 10 '16 at 16:25
  • I updated my answer with a cleaner way to test, using a spy instead of a real emitter, and I think it should work (that's what I actually do for the samples in my ebook). – cexbrayat Feb 10 '16 at 17:38
  • This works great thanks! I am new to front end development, especially unit testing it. This helps a lot. I didn't even know the spyOn function existed. – tallkid24 Feb 10 '16 at 18:20
  • How can I test this if use a TestComponent to wrap MyComponent? For example html = `` and in the test I do: tcb.overrideTemplate(TestComponent, html).createAsync(TestComponent) – bekos Feb 16 '16 at 10:54
  • It seems like it doesn't work in a more recent version of Angular/Jasmine. I'm on Angular 11 and your setup produces the following message on the usage of toHaveBeenCalledWith(): `TS2345: Argument of type '"hello"' is not assignable to parameter of type 'boolean | AsymmetricMatcher '.` Edit: it's possible to get around it by using on the spyOn() – Panossa May 02 '22 at 14:49
  • how to test an emitter if instead of a tag button is the tag o the child in the parent ? example : . It seems the querySelector in this tag doesnt work , and also doesnt work with debugElement.query because it doesnt have event . – Raquel Santos Oct 13 '22 at 08:39
75

You could use a spy, depends on your style. Here's how you would use a spy easily to see if emit is being fired off...

it('should emit on click', () => {
    spyOn(component.eventEmitter, 'emit');
    component.buttonClick();
    expect(component.eventEmitter.emit).toHaveBeenCalled();
    expect(component.eventEmitter.emit).toHaveBeenCalledWith('bar');
});
  • I have updated the answer to not unnecessary use of async or fakeAsync, which can be problematic as pointed out in previous comments. This answer remains a good solution as of Angular 9.1.7. If anything changes, please leave a comment and I will update this answer. thanks for all who commented/moderated. – Joshua Michael Calafell Jun 03 '20 at 18:27
  • Shouldn't you `expect` the actual spy (result of `spyOn()` call)? – Yuri Jul 30 '20 at 07:51
  • I missed the "component.buttonClick()" after the Spyon. This solution resolved my issue. Thanks alot! – Pearl Aug 21 '20 at 13:31
23

Although the highest voted answers work, they are not demonstrating good testing practices, so I thought I would expand on Günter's answer with some practical examples.

Let's imagine we have the following simple component:

@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!');
  }
}

The component is the system under test, spying on parts of it breaks encapsulation. Angular component tests should only know about three things:

  • The DOM (accessed via e.g. fixture.nativeElement.querySelector);
  • Names of the @Inputs and @Outputs; and
  • Collaborating services (injected via the DI system).

Anything that involves directly invoking methods on the instance or spying on parts of the component is too closely coupled to the implementation, and will add friction to refactoring - test doubles should only be used for the collaborators. In this case, as we have no collaborators, we shouldn't need any mocks, spies or other test doubles.


One way to test this is by subscribing directly to the emitter, then invoking the click action (see Component with inputs and outputs):

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!');
  });
});

Although this interacts directly with the component instance, the name of the @Output is part of the public API, so it's not too tightly coupled.


Alternatively, you can create a simple test host (see Component inside a test host) and actually mount your component:

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

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

then test the component in context:

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!');
  });
});

The componentInstance here is the test host, so we can be confident we're not overly coupled to the component we're actually testing.

jonrsharpe
  • 115,751
  • 26
  • 228
  • 437
2

You can subscribe to the emitter or bind to it, if it is an @Output(), in the parent template and check in the parent component if the binding was updated. You can also dispatch a click event and then the subscription should fire.

Günter Zöchbauer
  • 623,577
  • 216
  • 2,003
  • 1,567
  • So if I did like emitter.subscribe (data => { }); how would I get the next() output? – tallkid24 Feb 10 '16 at 15:43
  • Exactly. Or the template in the `TestComponent` has `` (where `someEmitter` is an `@Output()`) then the `value` property of `TextComponent` should be updated with the sent event. – Günter Zöchbauer Feb 10 '16 at 15:45
0

I had a requirement to test the length of the emitted array. So this is how I did this on top of other Answers.

expect(component.myEmitter.emit).toHaveBeenCalledWith([anything(), anything()]);
prabhatojha
  • 1,925
  • 17
  • 30
0

TEST: @Output() value new EventEmitter();

HTML

<input [(ngModel)]="_myInputValue" class="my-input" (keyup)="value.emit($event.target.value)" />

SPEC

it('should emit a value when user types input', () =>
{
  spyOn(component.value, 'emit');

  element.query(By.css('.my-input'))
  .triggerEventHandler('keyup', {target: {value: 'my input value'}});

  fixture.detectChanges();

  expect(component.value.emit).toHaveBeenCalled();
  expect(component.value.emit).toHaveBeenCalledWith('my input value');
}

NOTE: triggerEventHandler()

  • First argument can use 'keyup', 'keydown.enter', 'keyup.esc', etc...
  • The second argument is the $event object, i.e. "$event.target.value" from HTML.
Ravikumar B
  • 779
  • 1
  • 14
  • 25
SoEzPz
  • 14,958
  • 8
  • 61
  • 64