😎 Improvement #3: Test components and services with Spectator.

Spectator is an Angular unit testing library built on top of TestBed and developed by the same folks who built Akita. It can aid with dramatically reducing your test suite boilerplate, and it provides a very clean API for writing your unit tests.

For most Angular applications I’ve worked with, there are two types of services: those that make HTTP calls and those that do not. We’ll use Spectator to test both types.

Non-HTTP Injectables

Let’s pretend we have a notification service in our app that wraps around the Angular Material MatSnackBar . It looks like this:

This service has one dependency: MatSnackBar , and one function: notify . Let’s tackle the spec with Spectator. First, install some needed dependencies:

npm i -D @netbasal/spectator ng-mocks

A spec file for this might ultimately look like:

Woah, there is a lot going on here. Let’s talk about everything happening in the initialization of our test suite, step by step.

let snackBar: SpyObject<MatSnackBar>;

How MatSnackBar works is irrelevant to us. Injecting the real MatSnackBar into this test suite would break unit isolation and would be considered an anti-pattern. There are a few approaches to maintaining unit isolation in instances like this, and my favorite is to make each of the injectable’s dependencies into a SpyObject . SpyObject gives us the means to easily test that NotificationService is calling MatSnackBar ’s functions with the expected arguments, regardless of what MatSnackBar might do with those arguments once they’re received.

let spectator: SpectatorService<NotificationService> = createService({

service: NotificationService,

mocks: [MatSnackBar]

});

Here’s where the magic starts happening. We use createService to stand up a SpectatorService instance for NotificationService . We don’t have to worry about manually stubbing or mocking out MatSnackBar , the magical mocks array takes care of that automatically for us.

We can now condense our beforeEach contents to one line of code:

beforeEach(() => {

snackBar = spectator.get(MatSnackBar);

});

Before every test, (re)initialize our SpyObject . Simple! Now, time to write some tests!

it('exists', () => {

expect(spectator.service).toBeDefined();

});

No matter what you’re testing, it’s never a bad idea for the first test in your suite to be a sanity check. This may wind up saving you a lot of time down the road, and combined with Wallaby you can instantly see in your editor if you forgot to mock a dependency. Note that we use spectator.service to access our NotificationService instance.

it('can pop open a snackbar notification', () => {

spectator.service.notify('mock notification');

expect(snackBar.open).toHaveBeenCalledWith(

'mock notification',

'CLOSE',

{

duration: 7000

}

);

});

Digging a little deeper, we call our notify function and ensure it tells MatSnackBar to do what it’s supposed to do. Notice we don’t need to write any code for spyOn in order to use toHaveBeenCalledWith . Our SpyObject boilerplate takes care of that for us automagically. This is a test that is both understandable and easy on the eyes.

Injectables with HTTP calls

We need to grab some generic entities from our back-end. Here’s what a very simple HTTP service injectable (with no error handling) might look like:

Our test needs to validate that the GET call is actually being made. With Spectator, this is pretty easy!

Hopefully this is a little more straightforward than our previous test suite, but it doesn’t hurt to break it down anyway.

const httpService: () => SpectatorHTTP<EntityDataService> = createHTTPFactory<

EntityDataService

>(EntityDataService);

This is the toughest part of the suite to unpack, and you really don’t need to understand what’s happening here, other than knowing this is a recipe for building an httpService that you can use to test this Angular injectable.

SpectatorHTTP is a fancy wrapper around a few of the classes the Angular team provides for testing HTTP API calls.

it('exists', () => {

const { dataService } = httpService();

expect(dataService).toBeDefined();

});

Like our previous test suite, it’s never a bad idea to start with a sanity check. dataService is the actual EntityDataService instance that is generated by createHTTPFactory .

it('can get entities from the server', () => {

const { dataService, expectOne } = httpService(); dataService.getEntities().subscribe();

expectOne('/api/entities', HTTPMethod.GET);

});

This should be very readable. We get our service instance and expectOne , an assertion that accepts a URL path and an HTTP method. We subscribe to the observable our getEntities function returns in order to make the API call, then we use expectOne to assert that the call itself was made and sent correctly.

Non-HTTP injectables that depend on HTTP injectables

For a third and final service-layer example, get ready for injectable-ception as we test an injectable that uses our API service from the previous example. This service will be responsible for interacting with our API service, and then sending the API response to our state management system’s store. In other words, this injectable will have two dependencies. See below:

This service directly interacts with EntityDataService and AwesomeEntityStore . When we get a response from our API call, we send the entities to the store, presumably to be consumed by some component somewhere. Thankfully, testing this with Spectator is fairly simple!

Like the previous tests, let’s walk through this step by step.

let dataService: SpyObject<EntityDataService>;

let store: SpyObject<AwesomeEntityStore>; let spectator: SpectatorService<EntityService> = createService({

service: EntityService,

mocks: [EntityDataService, AwesomeEntityStore]

});

Things should start feeling more familiar now! We declare some SpyObject instances for our two dependencies, and pass those same dependencies into the mocks array.

beforeEach(() => {

dataService = spectator.get(EntityDataService);

store = spectator.get(AwesomeEntityStore);

});

Initialize those SpyObject instances so we can use them in our tests. Since they’re in the beforeEach , they’ll reinitialize after every unit test.

it('can try to get all the awesome entities and put them in the store', () => {

dataService.getEntities.andReturn(

of(

[{ id: 1 }]

)

); spectator.service.getAllEntities().subscribe(); expect(store.set).toHaveBeenCalledWith(

[{ id: 1 }]

);

});

Here’s where the magic happens! We tell dataService that if the getEntities function is called, it will return an observable with an array containing an AwesomeEntity . We can then monitor store ’s set function, and ensure the correct argument is passed.

This is all awesome, but things really start to get interesting when we move over to testing our components. A typical application has two different types of components: presentational (dumb/stateless) components and container (smart/stateful) components. Spectator makes testing both types pretty easy.

Presentational components

Tests for presentational components are some of the easiest to write with Spectator, but the recipe for testing components is overall a little different than testing services.

In the example below, we have PresentationalButtonComponent . It has one input: a label, and one output: a string emission.

If you’re not using presentational components in your Angular app, now would be a great time to consider them! It is an ideal design pattern to have root components managing state, while their templates assemble your app views using small stateless presentational components. One of the benefits of this pattern is these stateless components are very easy to test. Take a look:

Another woah. It’s very easy to stand up this test using Spectator! Our total boilerplate is down to a mere seven lines, and our tests are all very readable thanks to Spectator’s super-clean API! Let’s do another walkthrough.

let spectator: Spectator<PBComponent>;

const createComponent = createTestComponentFactory<PBComponent>({

component: PBComponent

}); beforeEach(() => {

spectator = createComponent();

});

This is the recipe to stand up any presentational component test suite with Spectator. The only thing that createTestComponentFactory ’s options object needs is a component value with your component class specified. Then, beforeEach test, run createComponent() .

it('exists', () => {

expect(spectator.component).toBeDefined();

});

Our sanity check. The component class instance is accessed through spectator.component . There are other things available through our spectator variable too, such as spectator.fixture for the component fixture.

// JSDOM

it('renders a button with a default label if no label is given', () => {

expect(spectator.query('button')).toHaveText('Submit');

});

In this test suite, I have two JSDOM tests and one test against the component class. Here, we use spectator.query to find a <button> element in the template, and assert that it should have the text “Submit”.

There are those who believe the JSDOM tests would be better handled by tools such as Protractor or Cypress and these two specs have no place in the unit test suite. My opinion on that is outside the scope of this article. I’m offering the JSDOM tests as an example if this is an approach you’d like to take. Remember, your JSDOM tests will have no impact on code coverage because they are testing against a simulated DOM rather than your TypeScript code.

// JSDOM

it('renders a button with an input label if one is given', () => {

spectator.component.buttonLabel = 'Mock Label';

spectator.detectChanges(); expect(spectator.query('button')).toHaveText('Mock Label');

});

There’s a little more going on here, but it’s still not too tough to follow. First, we provide a value to our buttonLabel input. Then, we kick off detectChanges() so it’ll show up in the DOM (otherwise the value will still be “Submit”). After the test arrangement is done, we can run our toHaveText assertion just like the previous test!

// Component Class

it('can emit a message', () => {

const pressEmitSpy = spyOn(spectator.component.press, 'emit'); spectator.component.handleButtonClick();

expect(pressEmitSpy).toHaveBeenCalledWith('Submit was pressed!');

});

As the comment states, this is a test against our actual TypeScript code in the component class. We aren’t simulating a button press and ensuring handleButtonClick() was called (this would start crossing into integration test territory), we’re just making sure than when it is called, it emits the message we’d expect.

To do this, we spyOn our EventEmitter ’s emit function, and use the toHaveBeenCalledWith assertion like we did with our injectable tests.

😱 Container + stateful components

Last but not least, we’ll write some tests for a stateful component. I’ve found stateful component unit tests to be the most stressful tests to write for Angular apps, but Spectator greatly simplifies things. Check it out:

Our stateful component is deceptively simple. Two key takeaways here:

We’re injecting a service: AwesomeEntitiesQuery , which is called in the ngOnInit() .

, which is called in the . Our template uses the presentational component from the previous example with the <app-presentational-button> tag.

It would normally take a lot of boilerplate to set up a test suite for this component, but Spectator can make this very clean for us. Here’s the spec:

Our friends SpyObject and mocks have returned, but we also have a new addition to the fray: shallow: true . Time for another walkthrough!

let query: SpyObject<AwesomeEntitiesQuery>; let spectator: Spectator<StatefulComponent>;

const createComponent = createTestComponentFactory<StatefulComponent>({

component: StatefulComponent,

mocks: [AwesomeEntitiesQuery],

shallow: true

});

Similar to our previous tests where we had service dependencies, our stateful component does too: AwesomeEntitiesQuery . And just like our previous tests, we have a magical mocks array that takes care of everything for us.

We also see shallow: true . Why is this here? Well, our child component <app-presentational-button> in the template complicates things. We also don’t actually care what this component is doing or how it works. Shallow rendering mocks child components. By taking advantage of shallow rendering, we can pretend <app-presentational-button> is transformed into something like an empty div . For a more detailed explanation of shallow rendering, the React documentation offers a short blurb on the topic.

beforeEach(() => {

spectator = createComponent();

query = spectator.get<AwesomeEntitiesQuery>(AwesomeEntitiesQuery);

});

This is a combination of the beforeEach calls in our previous examples with services and components. Here, we create our component and we also set up our SpyObject .

it('gets all the awesome entities on initialization', done => {

query.selectAll.andReturn(of([]));

spectator.component.ngOnInit(); spectator.component.awesomeEntities$.subscribe(val => {

expect(val).toEqual([]);

done();

});

});