#native_company# #native_desc#
#native_cta#

Testing Components


Learning Objectives

  • How to test a component’s inputs as well as its outputs.

  • How to interact with a component’s 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 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 is somewhat trickier, especially if we want to test from the view.

Firstly let’s see how we can track what gets emitted 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("[email protected]");
  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 let’s 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 = "[email protected]";
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 = "[email protected]"; (1)
  passwordEl.nativeElement.value = "123456";

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

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

  expect(user.email).toBe("[email protected]");
  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 component’s 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 = "[email protected]";
        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("[email protected]");
        expect(user.password).toBe("123456");
    });
});

Caught a mistake or want to contribute to the book? Edit this page on GitHub!



Advanced JavaScript

This unique course teaches you advanced JavaScript knowledge through a series of interview questions. Bring your JavaScript to the 2021's today.

Level up your JavaScript now!