Validating the heroes container component

Container components exist primarily to feed data into presentational components. This tells us right away what kind of observable properties and methods our container components must expose.

In the earlier piece “Container components with Angular”, we pulled the HeroesContainerComponent out of a component that had been doing too much.

To wire up the presentational heroes component, HeroesContainerComponent must provide:

  • An observable property that streams all heroes
  • A method for adding a hero
  • A method for removing a hero

Preparing test doubles

Our container component reaches out to a HeroService for both commands and queries against the application state and persistence layers. In TypeScript, declaring a type of HeroService does not force us to use an actual instance of that class. We simply need to supply an object that matches the interface — that is, an object with methods and properties whose signatures line up with those of the hero service class.


See how TypeScript types can catch developers off guard, especially those coming from classic C-family object-oriented languages like C# and Java.

Sorry C# and Java developers, this is not how TypeScript works


The hero service exposes a fairly broad interface with 7 public methods. A single component will hardly ever need all of them, which means the service violates the Interface Segregation Principle — one of the SOLID principles laid out by Robert “Uncle Bob” Martin. There are ways to fix that, but we will save that discussion for another day.

// heroes.container.spec.ts
import { asapScheduler, of as observableOf } from 'rxjs';

import { femaleMarvelHeroes } from '../../test/female-marvel-heroes';
import { Hero } from '../hero';
import { HeroService } from '../hero.service';
import { HeroesContainerComponent } from './heroes.container';

describe(HeroesContainerComponent.name, () => {
  function createHeroServiceStub(): jasmine.SpyObj<HeroService> {
    const stub: jasmine.SpyObj<HeroService> = jasmine.createSpyObj(
      HeroService.name,
      [
        'addHero',
        'deleteHero',
        'getHeroes',
      ]);
    resetHeroServiceStub(stub);

    return stub;
  }

  function resetHeroServiceStub(stub: jasmine.SpyObj<HeroService>): void {
    stub.addHero
      .and.callFake(({ name }: Partial<Hero>) => observableOf({
        id: 42,
        name,
      }, asapScheduler))
      .calls.reset();
    stub.deleteHero
      .and.callFake((hero: Hero) => observableOf(hero, asapScheduler))
      .calls.reset();
    stub.getHeroes
      .and.returnValue(observableOf(femaleMarvelHeroes, asapScheduler))
      .calls.reset();
  }

  const heroServiceStub: jasmine.SpyObj<HeroService> = createHeroServiceStub();

  afterEach(() => {
    resetHeroServiceStub(heroServiceStub);
  });
});
Enter fullscreen mode Exit fullscreen mode
Heroes: Setting up a HeroService stub for testing the container component.

So instead we build a hero service stub that includes only the methods we care about — an object whose methods return data in exactly the shape we want. This way, we keep the service out of a unit test suite that is strictly concerned with the component in isolation.

When we build observables from arrays, we use the asapScheduler so that values are emitted asynchronously, just as they would be in production. Skipping that step can hide edge cases — especially in integration tests or when working with the Angular TestBed. We want to follow best practices and steer clear of those problems.


Find out more about why RxJS schedulers matter during testing in “Testing Observables in Angular” by Netanel Basal.


Unit tests that run incredibly fast

When we test a container component, we can keep the Angular Compiler completely out of the picture, because a container component does not expose a data binding API. Its template only wires up one or more presentational components, so there is no user interaction and no tricky UI logic worth verifying. That means we can skip the Angular TestBed utilities altogether.

We simply handle the component as an ordinary class and construct instances ourselves by passing dependencies into its constructor. With compilation, dependency injection, and the component lifecycle out of the way, our unit tests execute remarkably quickly.

The biggest boost in speed comes from the fact that Angular recompiles components for every test case — a full compilation pass for each it in the suite. If the component-under-test stores its styles and template in separate files rather than inline in the Component decorator, that only makes things slower, because the compiler must read, parse, and compile several files before it can proceed to the next test case.

Verifying RxJS observables

We set up a Jasmine Spy to watch the heroes$ property by subscribing to it. That gives us the ability to assert what data is emitted, when it arrives, and how often.

// heroes.container.spec.ts
import { fakeAsync, tick } from '@angular/core/testing';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';

import { HeroService } from '../hero.service';
import { HeroesContainerComponent } from './heroes.container';

describe(HeroesContainerComponent.name, () => {
  let container: HeroesContainerComponent;
  const destroy: Subject<void> = new Subject();
  const heroServiceStub: jasmine.SpyObj<HeroService> = createHeroServiceStub();
  const observer: jasmine.Spy = jasmine.createSpy('heroes observer');

  beforeEach(fakeAsync(() => {
    container = new HeroesContainerComponent(heroServiceStub);
    container.heroes$.pipe(takeUntil(destroy)).subscribe(observer);
    tick();
  }));

  afterEach(() => {
    destroy.next();
    observer.calls.reset();
    resetHeroServiceStub(heroServiceStub);
  });

  afterAll(() => {
    destroy.complete();
  });
});
Enter fullscreen mode Exit fullscreen mode
Heroes: Observing the heroes$ property of the container component.

For each test case, we instantiate a container component and attach the spy to the heroes$ property. In the afterEach and afterAll hooks, we dispose of the subscriptions and the subject we created during the tests.

We stub only the hero service methods that the container component actually calls. As we add test cases one by one, we introduce the spies and stub methods exactly when a particular test requires them.

Testing a straightforward observable property

The heroes$ observable should stream all the hero fakes we handed to the hero service stub.

// heroes.container.spec.ts
describe('emits all heroes', () => {
  it('all heroes are emitted after subscribing', () => {
    expect(observer).toHaveBeenCalledWith(femaleMarvelHeroes);
  });

  it(`delegates to ${HeroService.name}`, () => {
    expect(heroServiceStub.getHeroes).toHaveBeenCalledTimes(1);
  });
});
Enter fullscreen mode Exit fullscreen mode
Heroes: Testing initial heroes state.

In the first test case we check that our spy observed a single emission containing the female Marvel heroes we use as fakes. In the second test we also confirm that the heroes are emitted precisely once.

That second test is not strictly necessary from a pure testing standpoint — we do not really care how the container component obtains its data. That said, it has been useful in practice to confirm that the application state layer was queried exactly once, so that we do not accidentally fire off multiple back-end requests.

Handling microtasks

The hero service stub emits its value asynchronously. We rely on the Angular testing utilities fakeAsync and tick to write our tests in a synchronous style by flushing the JavaScript event loop queue when we are ready.

Angular leverages Zone.js to pull off this trick. A call to tick first flushes microtasks — things like promises and observables driven by the asapScheduler. After that, macrotasks are processed, such as setTimeout and setInterval callbacks as well as observables that rely on asyncScheduler.


For short examples that explain schedulers, microtasks, and macrotasks, check out “What are schedulers in RxJS” by Kwinten Pisman.


RxJS ships with a utility that mirrors fakeAsync, called fakeSchedulers. It behaves much like the Angular version. The catch is that it currently needs to be imported from a framework-specific sub-package depending on which test framework you use. Many Angular projects stick with Karma and Jasmine, as Tour of Heroes does, but we may want to keep the door open to swapping the test runner, testing framework, or test doubles library later.


See how RxJS lets you fake time in “RxJS: Testing with Fake Time” by Nicholas Jamieson.


Testing hero addition

The add method should notify the server at the persistence layer and update the persistent state. To confirm that, we check whether the heroes$ observable emits the newly added hero once the server has responded.

// heroes.container.spec.ts
describe('adds a hero', () => {
  it('emits the specified hero when server responds', fakeAsync(() => {
    const wonderWoman = 'Wonder Woman';

    container.add(wonderWoman);
    tick();

    expect(observer).toHaveBeenCalledWith([
      ...femaleMarvelHeroes,
      { id: 42, name: wonderWoman },
    ]);
  }));
});
Enter fullscreen mode Exit fullscreen mode
Heroes: Testing addition of a hero.

Our hero service stub is set up to answer the addHero command by default.

// heroes.container.spec.ts
it(`delegates to ${HeroService.name}`, () => {
  const hawkeye = 'Hawkeye (Kate Bishop)';

  container.add(hawkeye);

  expect(heroServiceStub.addHero).toHaveBeenCalledTimes(1);
  expect(heroServiceStub.addHero).toHaveBeenCalledWith({ name: hawkeye });
});
Enter fullscreen mode Exit fullscreen mode
Heroes: Testing addition of a hero.

The container component delegates to the hero service whenever a hero is added. We verify that a partial hero carrying the specified name is handed to the hero service method addHero, and that the method is invoked only once.

That is enough to prove the integration with the persistence layer. Making sure the server state is actually updated falls on the hero service, so that belongs to the hero service test suite — not to the heroes container component tests.


For deciding which behaviours deserve a test, I follow The Unit Testing Minimalist approach by Sandi Metz. To dig deeper, watch “Magic Tricks of Testing” from Ancient City Ruby 2013.


Up to now we have confirmed the delegation to the hero service and how the application state responds when the server succeeds.

What happens when the server fails, the connection drops, or something else goes wrong? We simulate that by simply not emitting the specified hero through the heroes$ observable. Let us modify the hero service stub and check that this behaviour holds.

// heroes.container.spec.ts
it('does not emit the specified hero when server fails', fakeAsync(() => {
  heroServiceStub.addHero.and.returnValue(
    throwError(new Error('server error'), asapScheduler));
  const scarletWitch = 'Scarlet Witch';

  container.add(scarletWitch);
  tick();

  expect(observer).not.toHaveBeenCalledWith([
    ...femaleMarvelHeroes,
    { id: 42, name: scarletWitch },
  ]);
}));
Enter fullscreen mode Exit fullscreen mode
Heroes: Testing addition of a hero.

Errors thrown by an observable must be handled carefully. Here we apply a pessimistic update strategy, meaning the persistent state is only changed after the server state update has been confirmed.

We could also show a message to the user when an error occurs. Or we could prompt them to retry the operation, or even adopt an automatic retry strategy to cope with a temporary loss of connection or brief server downtime.

Testing hero deletion

The delete method tells the persistence layer what to do. We verify that expectation by spying on the deleteHero method of our stubbed hero service.

// heroes.container.spec.ts
describe('deletes a hero', () => {
  it(`delegates to ${HeroService.name}`, () => {
    const gamora: Hero = femaleMarvelHeroes.find(x => x.name === 'Gamora');

    container.delete(gamora);

    expect(heroServiceStub.deleteHero).toHaveBeenCalledTimes(1);
    expect(heroServiceStub.deleteHero).toHaveBeenCalledWith(gamora);
  });
});
Enter fullscreen mode Exit fullscreen mode
Heroes: Testing deletion of a hero.

Deletion follows the optimistic update strategy. The hero is removed from the persistent state right away. We confirm that in another test case.

// heroes.container.spec.ts
it('emits all other heroes immediately', fakeAsync(() => {
  const elektra: Hero = femaleMarvelHeroes.find(x => x.name === 'Elektra');

  container.delete(elektra);
  tick();

  expect(observer).toHaveBeenCalledWith(
    femaleMarvelHeroes.filter(x => x.id !== elektra.id));
}));
Enter fullscreen mode Exit fullscreen mode
Heroes: Testing deletion of a hero.

Because the state change happens asynchronously, we bring in fakeAsync and tick to observe it. That is perfectly acceptable — we can then check that the specified hero no longer appears in the heroes state.

The other half of the optimistic update strategy is that the persistent state must be rolled back if the server update fails.

// heroes.container.spec.ts
it('emits the specified hero when server fails', fakeAsync(() => {
  heroServiceStub.deleteHero.and.returnValue(
    throwError(new Error('timeout'), asapScheduler));
  const storm: Hero = femaleMarvelHeroes.find(x => x.name === 'Storm');

  container.delete(storm);
  tick();

  const emittedHeroes: Hero[]  = observer.calls.mostRecent().args[0];
  emittedHeroes.sort(compareIdAscending);
  expect(emittedHeroes).toEqual(femaleMarvelHeroes);
}));
Enter fullscreen mode Exit fullscreen mode
Heroes: Testing deletion of a hero.

In this last test case, we change the stubbed hero service so it simulates a server timeout after the hero has been removed. We then confirm that all the heroes are back in the heroes state.

Wrap-Up

When a container component is tested in the context of application state management, the focus lies on how user-triggered events are mapped to discrete commands.

The test suite confirms that those commands reach the intended targets—whether that be a service, an NgRx action dispatcher, or another analogous mechanism. Counting the exact number of dispatched commands is another frequent assertion, done to guard against unintended side effects or state corruption.

Beyond confirming that a command was issued, tests also inspect the observable outcomes, such as updated state flowing through a store or an observable on a service.

We chose to cover persistence update strategies directly, even though such logic typically lives outside the presentation layer, in services or NgRx effects.

For validating the data flow that a container component exposes, we assert both the payload and the timing of emissions. Stubbed dependencies played a central role here, and we also counted the precise number of queries made to those stubs to prevent costly operations.

Importantly, no lifecycle hooks are ever invoked in these tests. In fact, nothing about the container component class or its test approach is tied to Angular specifically.

Constructing the container component instance produces zero side effects. That gives us complete control over the encapsulated data flow, which makes the component's behavior far easier to trace and understand.

All this integration logic would have been noticeably more cumbersome and significantly slower to exercise through a UI component—without adding any real value to the tests.

The complete heroes container component spec is available in the GitHub repository.

Suggested Reading

Be sure to check out the foundational post “Model-View-Presenter with Angular”.

The related companion GitHub repo, along with other articles and supporting materials, can also be accessed from there.

If you're looking to separate state and back-end concerns from your Angular UI, container components are the answer. Learn how in "Container components with Angular".

Editor

I would like to extend a special thanks to you, Max Koretskyi, for the editorial guidance that brought this article to its final form. Your generosity in sharing your writing expertise with the developer community is truly appreciated.

Reviewers

My gratitude goes out to every reviewer whose insights shaped this piece. Your feedback was essential!