Brian Love
Google Developer Expert in Angular, software engineer and skier located in Denver, CO

NgRx Testing: Components

Reading time ~13 minutes

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:

  • Angular 6
  • NgRx 6
  • Jest and the angular-jest-preset module
  • The jasmine-marbles module
  • Faker for creating fake data

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:

  • hot() creates a hot observable stream.
  • cold() creates a cold observable stream.

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:

  • - a dash represents a frame of virtual time passing, which is 10 milliseconds, but that’s not really that important.
  • a-z0-9 any alphanumeric character represents a value. We can use the second argument to the hot() and cold() functions to specify the values that each marker represents.
  • () paranthesis group together values into a single frame.
  • ^ the carrot represents the start of a subscription in a cold observable stream.
  • | the pipe represents the completion notification.
  • # the pound sign (or hash symbol) represents the error notification.

Container vs Presentation

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

  • Container components retrieve data and mutate the state of your application.
  • Presentation components are highly reusable components that do not rely on the state of the application. They rely on data that is input into the component and emit events as output.

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:

  • As you can see, Jest uses Jasmine-like methods for setting up the test suite using the describe(), along with global methods like beforeEach() and afterEach().
  • First, we set up the TestBed in the first beforeEach() method.
  • We add the child components IndexComponent and UserListComponent to the declaration array just like we would add in the module.
  • We are importing the RouterTestingModule to mock the <router-outlet> element.
  • We are mocking out an object for the Store, with two properties: dispatch() and pipe().
  • Then, in the second beforeEach() method we create the component fixture and store a reference to the component instance.
  • Finally, we use the afterEach() method to tear down any subscriptions in our tests using the takeUntil operator in RxJS.

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:

  • First, we new up the LoadUsers action class.
  • Then, we get a reference to the Store using the TestBed.get() method.
  • And then we set up a spy on the dispatch() method using the jest.spyOn() function.
  • We then invoke the ngOnInit() method by triggering change detection using the detectChanges() method on the fixture.
  • Finally, we are expecting the dispatch() method to have been called with the LoadUsers action.

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

  • First, we get a reference to the Store using the TestBed.get() method.
  • Next, I use an exported generateUsers() function to generate a fake array of User objects. I am using the faker library to generate some fake data.
  • Next, we are mocking the pipe() method on the store instance. The mock function returns a new hot() observable stream. The observable stream is composed of a frame (virtual “time” has passed) followed by a value, a, which is the users array.

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:

  • First, we generate an array of fake User objects using the generateUsers() function.
  • We get a reference to the Store object that was injected into our component using the TestBed.get() method.
  • We want to mock out the pipe method to return the cold observable stream of User objects. We are creating a cold observable because the HttpClient returns a cold observable.
  • We then invoke the ngOnInit() method by triggering change detection using the detectChanges() method on the fixture.
  • Aftering invoking the ngOnInit() method in our component, we can subscribe() to the users property, expecting that the last() notification is the array of users.
  • Note that we use the done() function provided by Jest to signal the completion of asynchronous tests.
  • Finally, we need to flush() the tasks in the test scheduler. This will trigger the cold observable to emit the notification.

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:

  • First, we get reference to the DebugElement for the unordered list element.
  • Then, we get reference to the HTMLUListElement object that is the nativeElement property on the DebugElement. Note that we have to cast the type to explicitly instruct TypeScript that the DOM element is of type HTMLUListElement.
  • We then set the users property in the component. In this case I have a an array of User objects that is stored in a function-block variable named users that is result of invoking the generateUsers() function, which uses faker to generate fake data.
  • Next, we trigger the change detection in Angular using the detectChanges() method on the fixture.
  • We expect() that the childElementCount of the unordered list should match the length of the array of users.
  • We further obtain the first list item element and expect that the textContent equals the rendered first name and last name of the first user.

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:

  • First, we store a reference to the first user (which we will assert is the user that is clicked/touched).
  • Like in our previous test, we set the users property to the array of User objects that we generated using faker.
  • We then trigger change detection in Angular.
  • Using the DebugElement we obtain a reference to the <a> element that is the direct descendent of the first list item in the list.
  • We subscribe to the EventEmitter for the selectedUser output event.
  • Using the triggerEventHandler() method we can trigger the click event on the first user, supplying the user event object.
  • Finally, we expect() that the selectedUser is equal to the user object that was triggered in the click event.

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);
});
  • First, we use the generateUser() function that is imported from the model to create a fake User object using faker.
  • We new up the AddUser action, specifying the payload that is the user object.
  • Then, create a spy on the dispatch() method in the store using jest.spyOn().
  • We then trigger change detection and invoke the onUserChange() method with the user object.
  • Finally, we assert that the dispatch() method is invoked with the AddUser 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:

  • First, we specify the EditComponent and UserFormComponent as we would in the module.
  • We add the necessary imports for working with reactive forms and the router.
  • In the providers array we need to mock out the ActivatedRoute and Store. We use a BehaviorSubject to mock the paramMap property on the ActivatedRoute, specifying the id of the fake user. And we mock out both the dispatch() and pipe() methods on the Store. Note that the pipe() mock returns a hot observable which emits the user object after a single frame.

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:

  • Generate a fake user
  • Invoke the ngOnChanges() method, supplying a new SimpleChange object for a user.
  • Expect the value of the FormGroup to include the user’s first and last name.

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:

  • Provide a fake user that is input into the component.
  • Trigger the ngOnChanges() lifecycle method, which will populate the form using the patchValue() method on the FormGroup.
  • Subscribe to the userChange output.
  • Update the value of the firstName input.
  • Trigger the click event on the submit button.

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. 👏😀👍

Brian Love

Hi, I'm Brian. I am interested in TypeScript, Angular and Node.js. I'm married to my best friend Bonnie, I live in Denver and I ski (a lot).