Angular Testing Tips and Tricks

Tips and tricks to help you get the most out of testing

In this article, I’ll explore a few simple tips and tricks that I use every day while writing unit tests in Angular. Implementing these tips will help you:

write tests that are more robust to changes and refactoring

simplify the process of writing tests

make tests more useful and ensuring tests aren’t passing when they shouldn’t

make running your tests more performant

Tip 1. Use jasmine.createSpyObj<T>

Use the generic version of jasmine.createSpyObj . This ensures type-safety of your spies, allowing the compiler to verify that the methods being mocked actually exist. Let’s assume we accidentally created the spy as follows (misspelled delete as dlete ):

The compiler will throw the following error for the generic call: error TS2345: Argument of type ‘(“dlete” | “tggl”)[]’ is not assignable to parameter of type ...

Had we used the non-generic version ( jasmine.createSpyObj(‘TodoService’, [‘dlete’, ‘tggl’]); ), the compiler would not have thrown an error. Enjoy spending 15 minutes wondering why your tests keep failing (I speak from experience 😥).

Tip 2. Create your spies in the beforeEach block

You might be tempted to save a line and initialise the spy when declaring it:

initialising todoService in the describe block means the same instance will be used across all tests

Don’t do this. This will maintain the same spy between tests, leading to tests passing when they shouldn’t and failing randomly (depending on the execution order).

As an example, let’s assume you had one test checking to see that delete had been called, and then the next test expecting that it wasn’t called:

it('should call delete', () => {

// do some stuff

expect(todoService.delete).toHaveBeenCalled();

}); it('should not call delete', () => {

// do other stuff

expect(todoService.delete).not.toHaveBeenCalled();

});

The second test will fail, as delete had been called in the first test. Creating the spy in the beforeEach block will ensure each test receives a fresh instance:

declare todoService in the describe block but initialise it in beforeEach to ensure a new instance per test

Tip 3. Use Mock Services for shared services

If a service is being used in multiple places, extract it out into its own mock class. Instead of creating a spy everywhere the service is used, you can define it once and just inject the mock version where required. This is the approach that we take:

Here, we’ve created a mock for one of our services, ControlRoomStateService . As this service provides an observable through getState$ , we’re instantiating that observable in this class. We’ve also created some helper methods to change the control room state. This ensures that if the implementation of the ControlRoomStateService ever changes, we’ll only have to change it one place.

Here’s how we can set up our component tests to use this mock service:

And use the mock service to simplify our tests:

Tip 4. Extract element query code into functions.

By extracting querying code into their own function, it makes it easier to refactor tests later on. As an example, take the following:

it('should show error', () => {

// setup error state

expect(fixture.nativeElement.querySelector('.error')).toBeFalsy();

}); it('should not show error', () => {

// setup good state

expect(fixture.nativeElement.querySelector('.error')).toBeTruthy();

});

If, as an example, we later changed the class from error to invalid-title , only the second test would fail. A developer might go in and make the following change:

it('should show error', () => {

// setup error state

expect(fixture.nativeElement.querySelector('.error')).toBeFalsy();

}); it('should not show error', () => {

// setup good state

expect(fixture.nativeElement.querySelector('.invalid-title')).toBeTruthy();

});

Both tests would now pass, however the first test is now useless — we’ll never display the error class (it’s possible, however, that invalid-title would always be displayed, given we don’t have a test to check for that). Consider the following implementation instead:

it('should show error', () => {

// setup error state

expect(getErrorEl()).toBeFalsy();

}); it('should not show error', () => {

// setup good state

expect(getErrorEl()).toBeTruthy();

}); function getErrorEl(): HTMLElement {

return fixture.nativeElement.querySelector('.error');

}

This time, fixing the second unit test will ensure that the first one is also updated.

Tip 5. Use NgMocks to easily interact with child components

See this article for an in-depth look at testing child components.

NgMocks makes it super easy to test child components. Instead of using NO_ERRORS_SCHEMA or CUSTOM_ERRORS_SCHEMA , we can simply declare a component with MockComponent . This:

ensures type-safety of our component’s inputs and outputs

allows us to verify that the inputs are set correctly

helps test that our component-under-test responds correctly to output events from the child component.

Tip 6. Focus tests to only the component/service that you’re testing

As your application grows, running all the unit tests every time you make a change will be prohibitively slow. When testing a component or service, you can run tests in a particular describe block by changing describe to fdescribe . Similarly, to run a single unit test, change it(...) to fit(...) . Just make sure you revert these back before pushing your code to master!

Tip 7. Disable sourceMaps for faster unit testing

It’s not an issue when your application is still small, but as it grows, source maps can substantially slow down the speed at which your tests run. When I’m developing, I always run with source maps disabled:

ng test --sourceMap=false

In our application, which has ~1200 tests, focusing on a single component to run 15 tests takes about 4 seconds with source maps disabled. With source maps enabled, it takes a whopping 24 seconds 😱 . That’s a massive difference in productivity.

Of course, there are reasons why you might want source maps enabled (debugging tests through the browser). In practice, so long as tests are appropriately isolated, I rarely debug through the browser. On the rare occasion where I do, I simply enable source maps until I resolve the issue, then turn it back off to continue testing and developing.