Picture of Brian Love wearing black against a dark wall in Portland, OR.

Brian Love

NgRx Testing: Components

Learn how to unit test Angular components using NgRx with Jest.

Download

In this post I’ll be referencing code in a very basic demo application that uses NgRx. You can clone the repository or download a zip file of the source code:

Series

This post is part of a series on testing NgRx using Jest:

Stack

The stack for this application will be:

Jest Test Runner

We’ll be using Jest to write unit tests for an Angular application that uses NgRx for state management. It’s much faster than Karma (even with headless Chrome) and uses a Jasmine-like API.

If you’re new to Jest, check out my post on using Jest with Angular.

jasmine-marbles

We’ll be using the jasmine-marbles module in order to mock observables.

Here are some links to more information about marble testing with NgRx:

In summary, we can use marble-diagram-like strings to describe an observable stream over time. This enables us to synchronously test asynchronous observable streams.

There are two primary functions that we will be using:

We’ll use these function to assert that the outcome of specific actions in our application result in the expected hot or cold observable stream that we describe.

Marble testing uses a string to describe the observable stream:

Container vs Presentation

In the sample application I am employing the component architecture of separating “container” components from “presentation” components. In summary:

Index Component

Let’s take a look at testing a container component in an application that is using NgRx.

First, let’s get things set up in the src/app/users/containers/index.component.spec.ts file:

describe('IndexComponent', () => {
  let component: IndexComponent;
  let fixture: ComponentFixture<IndexComponent>;
  let unsubscribe = new Subject<void>();

  beforeEach(async(() => {
    TestBed.configureTestingModule({
      declarations: [IndexComponent, UserListComponent],
      imports: [RouterTestingModule],
      providers: [
        {
          provide: Store,
          useValue: {
            dispatch: jest.fn(),
            pipe: jest.fn()
          }
        }
      ]
    }).compileComponents();
  }));

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

  afterEach(() => {
    unsubscribe.next();
    unsubscribe.complete();
  });
});

Let’s review the setup for the tests:

The standard “it should create” test follows:

it('should create', () => {
  expect(component).toBeTruthy();
});

Nothing special there. This is straight out-of-the box when we generated the component using the Angular CLI.

Let’s take a look at testing the ngOnInit() method:

ngOnInit() {
  this.store.dispatch(new LoadUsers());
  this.users = this.store.pipe(select(selectAllUsers));
}

We are simply dispatching the LoadUsers action to the store, and also using the selectAllUsers selector to get an array of users that is in the store, which happens to be an @ngrx/entity collection of User objects.

Let’s test it:

describe('ngOnInit()', () => {
  it('should dispatch an the LoadUsers action in ngOnInit lifecycle', () => {
    const action = new LoadUsers();
    const store = TestBed.get(Store);
    const spy = jest.spyOn(store, 'dispatch');

    fixture.detectChanges();

    expect(spy).toHaveBeenCalledWith(action);
  });

  it('should selectAllUsers', () => {
    const store = TestBed.get(Store);
    const users = generateUsers();
    store.pipe = jest.fn(() => hot('-a', { a: users }));

    fixture.detectChanges();

    const expected = cold('-a', { a: users });
    expect(component.users).toBeObservable(expected);
  });
});

We want to test both the dispatch() and pipe() methods that are invoked in the ngOnInit() method.

First, we create a test for dispatching the LoadUsers action:

Then, we create a test for selecting the users from the state object:

Finally, we should write a unit test to validate the users property within our component is a cold observable of an array of User objects:

describe('users', () => {
  it('should be an observable of an array of user objects', (done) => {
    const users = generateUsers();
    const store = TestBed.get(Store);
    store.pipe = jest.fn(() => cold('-a|', { a: users }));

    fixture.detectChanges();

    component.users.subscribe((componentUsers) => {
      expect(componentUsers).toEqual(users);
      done();
    });

    getTestScheduler().flush();
  });
});

In this test:

User List Component

Now that we have tested our container IndexComponent class, let’s look at an example of testing the UserListComponent. This component is a child of the IndexComponent and is designed to be a presentation component that is stateless.

Let’s look at the component template in src/app/users/components/user-list/user-list.component.html:

<ul>
  <li *ngFor="let user of users" trackBy="user.id">
    <a href="javascript:void(0)" (click)="selectUser.emit(user)"
      >{{user.firstName}} {{user.lastName}}</a
    >
  </li>
</ul>

As you can see, we are simply rendering an unordered list of users. Nothing fancy here.

First, we want to test that the list of users is rendered in src/app/users/components/user-list/user-list.component.spec.ts:

it('should display an unordered list of heroes', () => {
  const ulDebugEl = fixture.debugElement.query(By.css('ul'));
  const ulEl = ulDebugEl.nativeElement as HTMLUListElement;
  component.users = users;
  fixture.detectChanges();
  expect(ulEl.childElementCount).toBe(users.length);

  const firstLi = ulEl.querySelector('li:first-child');
  expect(firstLi.textContent).toEqual(
    `${users[0].firstName} ${users[0].lastName}`
  );
});

This test is pretty straight forward:

Now, let’s assert that the EventEmitter emits a user that is clicked:

it('should select a user when clicked', () => {
  const user = users[0];

  component.users = users;
  fixture.detectChanges();
  const anchorDebugEl = fixture.debugElement.query(
    By.css('ul > li:first-child > a')
  );

  let selectedUser: User;
  component.selectUser.subscribe(user => (selectedUser = user));

  anchorDebugEl.triggerEventHandler('click', user);
  expect(selectedUser).toEqual(user);
});

Let’s review the test:

Run Tests

It’s a good time to stop and run our tests to be sure that everything is passing:

$ npm test

Add User Component

Ok, now that we have tests for our IndexComponent and it’s child UserListComponent, let’s take a look at testing the AddComponent.

Here is the template in src/app/users/containers/add/add.component.html:

<app-user-form (userChange)="onUserChange($event)"></app-user-form>

We will be using the UserFormComponent to display a form for adding a new user, attaching an @Output() binding for the userChange, invoking the onUserChange() method when a value is emitted.

Within the AddComponent we define the onUserChange() method in src/app/users/containers/add/add.component.ts:

export class AddComponent {
  constructor(private store: Store<fromRoot.State>) {}

  onUserChange(user: User) {
    this.store.dispatch(new AddUser({ user: user }));
  }
}

In the onUserChange() method we dispatch() the AddUser action to the store.

Ok, let’s test that the AddUser action is dispatched when the onUserChange emits in src/app/users/containers/add/add.component.spec.ts:

it('should dispatch the AddUser action when onUserChange is invoked', () => {
  const user = generateUser();
  const action = new AddUser({ user });
  const spy = jest.spyOn(store, 'dispatch');

  fixture.detectChanges();

  component.onUserChange(user);
  expect(spy).toHaveBeenCalledWith(action);
});

Edit User Component

Along with the ability to add a user, we also want to be able to edit an existing user. The EditComponent template is very similar to the AddComponent, with the addition of the user input binding:

<app-user-form
  [user]="user$ | async"
  (userChange)="onUserChange($event)"
></app-user-form>

The EditComponent is routed with an id parameter in order to specify the user to edit. We’ll use the id parameter to dispatch two actions: SelectUser and LoadUser.

Here is the EditComponent source in src/app/users/containers/edit/edit.component.ts:

export class EditComponent implements OnInit {
  user$: Observable<User>;

  constructor(
    private activatedRoute: ActivatedRoute,
    private store: Store<fromRoot.State>
  ) {}

  ngOnInit() {
    const PARAM_ID = 'id';
    this.user$ = this.activatedRoute.paramMap.pipe(
      filter(paramMap => paramMap.has(PARAM_ID)),
      map(paramMap => paramMap.get(PARAM_ID)),
      tap(id => {
        this.store.dispatch(new SelectUser({ id: +id }));
        this.store.dispatch(new LoadUser({ id: +id }));
      }),
      switchMap(id => this.store.pipe(select(selectSelectedUser)))
    );
  }

  onUserChange(user: User) {
    this.store.dispatch(new UpdateUser({ user: user }));
  }
}

Before we can start testing our component we need to wire up the TestBed:

beforeEach(async(() => {
  TestBed.configureTestingModule({
    declarations: [EditComponent, UserFormComponent],
    imports: [FormsModule, RouterTestingModule, ReactiveFormsModule],
    providers: [
      {
        provide: ActivatedRoute,
        useValue: {
          paramMap: new BehaviorSubject(
            convertToParamMap({
              id: user.id,
            }),
          ),
        },
      },
      {
        provide: Store,
        useValue: {
          dispatch: jest.fn(),
          pipe: jest.fn(() => hot('-a', { a: user })),
        },
      },
    ],
  }).compileComponents();
}));

Let’s review the beforeEach() method:

With our TestBed ready to go, let’s test the ngOnInit() lifecycle method:

describe('ngOnInit', () => {
  it('should dispatch SelectUser action for specified id parameter', () => {
    const action = new SelectUser({ id: user.id });
    const spy = jest.spyOn(store, 'dispatch');

    fixture.detectChanges();

    expect(spy).toHaveBeenCalledWith(action);
  });

  it('should dispatch LoadUser action for specified id parameter', () => {
    const action = new LoadUser({ id: user.id });
    const spy = jest.spyOn(store, 'dispatch');

    fixture.detectChanges();

    expect(spy).toHaveBeenCalledWith(action);
  });
});

Our “ngOnInit” test suite contains two tests: asserting the dispatching of the SelectUser and LoadUser actions.

Next, let’s test the onUserChange() method that is invoked when the userChange EventEmitter emits the updated user object from the <user-form> component.

describe('onUserChange', () => {
  it('should dispatch the UpdateUser action when onUserChange is invoked', () => {
    const user = generateUser();
    const action = new UpdateUser({ user });
    const spy = jest.spyOn(store, 'dispatch');

    fixture.detectChanges();

    component.onUserChange(user);
    expect(spy).toHaveBeenCalledWith(action);
  });
});

This is identical to the test for the AddComponent.

User Form Component

With both the AddComponent and EditComponent tested, the next logical step is to create a unit test for the UserFormComponent.

Here is the template in src/app/users/components/user-form/user-form.component.html:

<form [formGroup]="form">
  <input type="text" placeholder="First Name" formControlName="firstName" />
  <input type="text" placeholder="Last Name" formControlName="lastName" />
  <button (click)="onSave()">Save</button>
</form>

I am using the ReactiveFormsModule to create a simple form for the user that contains two inputs: first name and last name. When the “Save” button in clicked, the onSave() method in the component is invoked.

In the UserFormComponent we define the onSave() method:

onSave() {
  if (this.form.invalid) {
    return;
  }

  this.userChange.emit({
    ...this.user,
    ...this.form.value
  });
}

Fist, let’s assert that the form values are patched when the ngOnChanges() lifecycle method is invoked:

it('should patch values into the form', () => {
  const user = generateUser();

  component.ngOnChanges({
    user: new SimpleChange(null, user, true),
  });

  expect(component.form.value).toEqual({
    firstName: user.firstName,
    lastName: user.lastName,
  });
});

In this test we:

Then, let’s assert that the userChange output event is emitted when the form is submitted:

it('should emit the userChange event when submitted', () => {
    const user = generateUser();
    const firstName = 'Brian';
    const firstNameDebugEl = fixture.debugElement.query(
      By.css('input[formControlName="firstName"]')
    );
    const firstNameEl = firstNameDebugEl.nativeElement as HTMLInputElement;
    const buttonDebugEl = fixture.debugElement.query(By.css('button'));

    fixture.detectChanges();

    let updatedUser: User;
    component.userChange.subscribe(user => (updatedUser = user));

    component.user = user;
    component.ngOnChanges({
      user: new SimpleChange(null, user, true)
    });

    firstNameEl.value = firstName;
    firstNameEl.dispatchEvent(newEvent('input'));

    buttonDebugEl.triggerEventHandler('click', null);

    expect(updatedUser).toEqual({
      ...user,
      firstName
    });
  });

In order to test our component we need to:

After populating the form with the initial user data, updating the first name input value, and subscribing to the userChange EventEmitter we can expect() that the updated user that is emitted equals the user that was provided, with the update first name value.

Conclusion

Unit testing is extremely value, and using a test runner like Jest makes the experience enjoyable with fast iterations.

A big thank you to the NgRx community and all of the documentation on using Rx Marbles for unit testing observables and asynchronous events in Angular. 👏😀👍