Testing Components

Learning Objectives

  • How to test a components inputs as well as it’s outputs.

  • How to interact with a components view.

Test setup

We’ll continue with our example of testing a LoginComponent. We are going to change our component into a more complex version with inputs, outputs, a domain model and a form, like so:

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

export class User { (1)
  constructor(public email: string, public password: string) {
  }
}

@Component({
  selector: 'app-login',
  template: `
<form>
  <label>Email</label>
  <input type="email"
         #email>
  <label>Password</label>
  <input type="password"
         #password>
  <button type="button" (2)
          (click)="login(email.value, password.value)"
          [disabled]="!enabled">Login
  </button>
</form>
`
})
export class LoginComponent {
  @Output() loggedIn = new EventEmitter<User>(); (3)
  @Input() enabled = true; (4)

  login(email, password) { (5)
    console.log(`Login ${email} ${password}`);
    if (email && password) {
      console.log(`Emitting`);
      this.loggedIn.emit(new User(email, password));
    }
  }
}
1 We create a User class which holds the model of a logged in user.
2 The button is sometimes disabled depending on the enabled input property value and on clicking the button we call the login function.
3 The component has an output event called loggedIn.
4 The component has an input property called enabled.
5 In the login function we emit a new user model on the loggedIn event.

The component is more complex and uses inputs, outputs and emits a domain model on the output event.

Note

We are not using the AuthService any more.

We also bootstrap our test suite file like so:

describe('Component: Login', () => {

  let component: LoginComponent;
  let fixture: ComponentFixture<LoginComponent>;
  let submitEl: DebugElement;
  let loginEl: DebugElement;
  let passwordEl: DebugElement;

  beforeEach(() => {

    TestBed.configureTestingModule({
      declarations: [LoginComponent]
    });

    // create component and test fixture
    fixture = TestBed.createComponent(LoginComponent);

    // get test component from the fixture
    component = fixture.componentInstance;

    submitEl = fixture.debugElement.query(By.css('button'));
    loginEl = fixture.debugElement.query(By.css('input[type=email]'));
    passwordEl = fixture.debugElement.query(By.css('input[type=password]'));
  });
});

We just have a few more debug elements stored on our test suite which we’ll inspect or interact with in our test specs.

Testing @Inputs

To test inputs we need to do things:

  1. We need to be able to change the input property enabled on our component.

  2. We need to check that the button is enabled or disabled depending on the value of our input property.

Solving the first is actually very easy.

Just because it’s an @Input doesn’t change the fact it’s a still just a simple property which we can change like any other property, like so:

it('Setting enabled to false disables the submit button', () => {
  component.enabled = false;
});

For the second we need to check the disabled property value of the buttons DOM element like so:

it('Setting enabled to false disables the submit button', () => {
    component.enabled = false;
    fixture.detectChanges();
    expect(submitEl.nativeElement.disabled).toBeTruthy();
});

Note

We also need to call fixture.detectChanges() to trigger change detection and update the view.

Testing @Outputs

Testing outputs it somewhat trickier, especially if we want to test from the view.

Firstly lets see how we can track what gets emmitted by the output event and add some expectations for it.

it('Entering email and password emits loggedIn event', () => {
  let user: User;

  component.loggedIn.subscribe((value) => user = value);

  expect(user.email).toBe("test@example.com");
  expect(user.password).toBe("123456");
});

The key line above is

component.loggedIn.subscribe((value) => user = value);

Since the output event is actually an Observable we can subscribe to it and get a callback for every item emitted.

We store the emitted value to a user object and then add some expectations on the user object.

How do we actually trigger an event to be fired? We could call the component.login(…​) function ourselves but for the purposes of this lecture we want to trigger the function from the view.

Firstly lets set some values to our email and password input controls in the view. We’ve already got references to both those fields in our setup function so we just set the values like so:

loginEl.nativeElement.value = "test@example.com";
passwordEl.nativeElement.value = "123456";

Next we trigger a click on the submit button, but we want to do that after we’ve subscribed to our observable like so:

it('Entering email and password emits loggedIn event', () => {
  let user: User;
  loginEl.nativeElement.value = "test@example.com"; (1)
  passwordEl.nativeElement.value = "123456";

  component.loggedIn.subscribe((value) => user = value);

  submitEl.triggerEventHandler('click', null); (2)

  expect(user.email).toBe("test@example.com");
  expect(user.password).toBe("123456");
});
1 Setup data in our input controls.
2 Trigger a click on our submit button, this synchronously emits the user object in the subscribe callback!

Summary

We can test inputs by just setting values on a components input properties.

We can test outputs by subscribing to an EventEmitters observable and storing the emitted values on local variables.

In combination with the previous lectures and the ability to test inputs and outputs we should now have all the information we need to test components in Angular.

In the next lecture we will look at how to test Directives.

Listing

Listing 1. login.component.ts
import {Component, EventEmitter, Input, Output} from '@angular/core';

export class User {
  constructor(public email: string, public password: string) {
  }
}

@Component({
  selector: 'app-login',
  template: `
<form>
  <label>Email</label>
  <input type="email"
         #email>
  <label>Password</label>
  <input type="password"
         #password>
  <button type="button"
          (click)="login(email.value, password.value)"
          [disabled]="!enabled">Login
  </button>
</form>
`
})
export class LoginComponent {
  @Output() loggedIn = new EventEmitter<User>();
  @Input() enabled = true;

  login(email, password) {
    console.log(`Login ${email} ${password}`);
    if (email && password) {
      console.log(`Emitting`);
      this.loggedIn.emit(new User(email, password));
    }
  }
}
Listing 2. login.component.spec.ts
/* tslint:disable:no-unused-variable */
import {TestBed, ComponentFixture, inject, async} from '@angular/core/testing';
import {LoginComponent, User} from './login.component';
import {Component, DebugElement} from "@angular/core";
import {By} from "@angular/platform-browser";


describe('Component: Login', () => {

  let component: LoginComponent;
  let fixture: ComponentFixture<LoginComponent>;
  let submitEl: DebugElement;
  let loginEl: DebugElement;
  let passwordEl: DebugElement;

  beforeEach(() => {

    // refine the test module by declaring the test component
    TestBed.configureTestingModule({
      declarations: [LoginComponent]
    });


    // create component and test fixture
    fixture = TestBed.createComponent(LoginComponent);

    // get test component from the fixture
    component = fixture.componentInstance;

    submitEl = fixture.debugElement.query(By.css('button'));
    loginEl = fixture.debugElement.query(By.css('input[type=email]'));
    passwordEl = fixture.debugElement.query(By.css('input[type=password]'));
  });

  it('Setting enabled to false disabled the submit button', () => {
    component.enabled = false;
    fixture.detectChanges();
    expect(submitEl.nativeElement.disabled).toBeTruthy();
  });

  it('Setting enabled to true enables the submit button', () => {
    component.enabled = true;
    fixture.detectChanges();
    expect(submitEl.nativeElement.disabled).toBeFalsy();
  });

  it('Entering email and password emits loggedIn event', () => {
    let user: User;
    loginEl.nativeElement.value = "test@example.com";
    passwordEl.nativeElement.value = "123456";

    // Subscribe to the Observable and store the user in a local variable.
    component.loggedIn.subscribe((value) => user = value);

    // This sync emits the event and the subscribe callback gets executed above
    submitEl.triggerEventHandler('click', null);

    // Now we can check to make sure the emitted value is correct
    expect(user.email).toBe("test@example.com");
    expect(user.password).toBe("123456");
  });
})
;

Learn Angular 5 For FREE

I've released my 700 page Kick Starter funded Angular 5 book for FREE