Angular @Input and @Output testing

In this article, I'll show how to test Angular component with inputs and outputs.

Setup

If unsure how to set up Angular with Jest please refer to the article: barcioch.pro/angular-with-jest-setup

Component

Create a component with one input and one output:

  • the component takes a title value and displays it in div

  • when user clicks button the value from the title field is emitted through buttonClick EventEmitter.

// test.component.ts

import { Component, EventEmitter, Input, Output } from '@angular/core';

@Component({
  selector: 'app-test',
  templateUrl: './test.component.html',
  styleUrls: ['./test.component.css']
})
export class TestComponent {
  @Input() title = '';
  @Output() buttonClick = new EventEmitter<string>();

  click(): void {
    this.buttonClick.emit(this.title);
  }
}
<!-- test.component.html -->

<div class="title">{{ title }}</div>
<button
    type="button"
    (click)="click()"
    class="button">Button</button>

Test

To test a component I'll wrap it in the wrapper component, so it will work as used in the real application. It will also make it easy to pass the values and validate outputs.

@Component({
  selector: 'app-dummy',
  template: `<app-test [title]="title" (buttonClick)="buttonClicked($event)"></app-test>`,
  styleUrls: ['./test.component.css']
})
export class DummyComponent {
  title?: string;

  click(): void {  }

  buttonClicked(value: string): void {  }
}

The test works as follows:

  • declare all needed components
declarations: [
  TestComponent,
  DummyComponent,
],
  • create the fixture and component reference
fixture = TestBed.createComponent(DummyComponent);
component = fixture.componentInstance;
  • spy on buttonClicked method
jest.spyOn(component, 'buttonClicked');
  • test component default settings
describe('when no title is passed ', () => {
  it('should display empty title', () => {
    expect(getTitle().nativeElement.innerHTML).toBe('');
  });

  describe('and user clicks button', () => {
    beforeEach(() => {
      getButton().nativeElement.click();
      fixture.detectChanges();
    });

    it('should emit empty string', () => {
      expect(component.buttonClicked).toHaveBeenCalledTimes(1);
      expect(component.buttonClicked).toHaveBeenCalledWith('');
    });
  });
});

Note that I'm clicking the HTML Button element

getButton().nativeElement.click();

and validating the wrapper's method call

expect(component.buttonClicked).toHaveBeenCalledTimes(1);
expect(component.buttonClicked).toHaveBeenCalledWith('');

Test the passed value and output

  describe('when "my title" value is passed as "title"', () => {
    beforeEach(() => {
      component.title = 'my title';
      fixture.detectChanges();
    });

    it('should display "my title"', () => {
      expect(getTitle().nativeElement.innerHTML).toBe('my title');
    });

    describe('and user clicks button', () => {
      beforeEach(() => {
        getButton().nativeElement.click();
        fixture.detectChanges();
      });

      it('should emit empty string', () => {
        expect(component.buttonClicked).toHaveBeenCalledTimes(1);
        expect(component.buttonClicked).toHaveBeenCalledWith('my title');
      });
    });
  });

Set value directly on the wrapper and run change detection

component.title = 'my title';
fixture.detectChanges();

The rest is similar to the previous test.

Full test:

import { ComponentFixture, TestBed } from '@angular/core/testing';
import { TestComponent } from './test.component';
import { Component, DebugElement } from '@angular/core';
import { By } from '@angular/platform-browser';

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

  beforeEach(async () => {
    await TestBed.configureTestingModule({
      declarations: [
        TestComponent,
        DummyComponent,
      ],
    }).compileComponents();

    fixture = TestBed.createComponent(DummyComponent);
    component = fixture.componentInstance;
    jest.spyOn(component, 'buttonClicked');
  });


  describe('when no title is passed ', () => {
    it('should display empty title', () => {
      expect(getTitle().nativeElement.innerHTML).toBe('');
    });

    describe('and user clicks button', () => {
      beforeEach(() => {
        getButton().nativeElement.click();
        fixture.detectChanges();
      });

      it('should emit empty string', () => {
        expect(component.buttonClicked).toHaveBeenCalledTimes(1);
        expect(component.buttonClicked).toHaveBeenCalledWith('');
      });
    });
  });

  describe('when "my title" value is passed as "title"', () => {
    beforeEach(() => {
      component.title = 'my title';
      fixture.detectChanges();
    });

    it('should display "my title"', () => {
      expect(getTitle().nativeElement.innerHTML).toBe('my title');
    });

    describe('and user clicks button', () => {
      beforeEach(() => {
        getButton().nativeElement.click();
        fixture.detectChanges();
      });

      it('should emit empty string', () => {
        expect(component.buttonClicked).toHaveBeenCalledTimes(1);
        expect(component.buttonClicked).toHaveBeenCalledWith('my title');
      });
    });
  });

  const getTitle = (): DebugElement => {
    return fixture.debugElement.query(By.css('.title'));
  }

  const getButton = (): DebugElement => {
    return fixture.debugElement.query(By.css('.button'));
  }
});

@Component({
  selector: 'app-dummy',
  template: `
    <app-test [title]="title" (buttonClick)="buttonClicked($event)"></app-test>`,
  styleUrls: ['./test.component.css']
})
export class DummyComponent {
  title: string = '';

  buttonClicked(value: string): void { }
}

Source code

gitlab.com/barcioch-blog-examples/series-an..