Sitemap

Unit tests in Angular 101

11 min readApr 10, 2025

Most of us have seen the testing pyramid. Unit tests take the bottom-most widest level there. Which means that, in general, this is the most written kind of tests in software development. Despite that, we often are more focused on code creation than making sure it doesn’t break in the future. Let’s take a moment to focus on unit tests and how you can make your life easier when writing them.

Press enter or click to view image in full size

Randomize tests

This is the simplest and, yet, one of the most useful things you can do. Especially in the environment where not all developers are highly experienced in writing unit tests.

By default, Jest runs all tests one-by-one. They are not isolated within the same file, which means that one test can affect the other. Furthermore, it’s possible that a test passes only because of the side effects from another test. By randomizing the order of tests execution, you make sure that tests are independent from each other. That you are cleaning up in tests, which ensures robustness.

To enable tests randomization, you need to open your jest.config.ts and add randomize: true . Or, you can pass the --randomize flag to the CLI command when executing it. One important thing to keep your eye on — seed. A seed is a randomly generated number (unless provided manually) that defines execution order. If you see that in some random moments your tests fail — this is, most likely, due to a specific execution order. I.e., you have caught what you’ve been looking for — side effects.

To replicate the behavior, grab the seed from the failed job logs and run tests using flag --randomize --seed <seed> . This will run tests in the same order as they were executed in the failed job. It’s especially useful when the job fails in the pipelines.

Enabling randomization has performance implications. However, IMO, they are worth it. You could also run randomized tests on some schedule if you see that enabling it gives you a significant slowdown.

Describe

The first rule, which I think is also very intuitive, — your tests should have at least one top-level describe with the name of the unit that you are testing. E.g., in most of the cases you should have just one describe with the class name (component/directive/pipe/service). In some scenarios, however, you could have several top-level describes. E.g., when you have a utils file that has several exported functions. Then, you’d want to have as many describes as there are exported functions.

Another important describe — is the method name that you are testing. This, together, helps to organize your tests in a nice tree where you can see that there are tests, e.g., for a MyService and its methods getUsers and submitArticle .

describe('MyService', () => {
describe('getUsers', () => {})

describe('submitArticle', () => {})
})

These two I consider to be a must-have in the tests. You can add more nested describes if you see that the structure could make use of an extra splitting and extra context.

Name your tests

Test names are also important. You should clearly state what you expect from your tests. Not just it('should work') or similar. At first, it may be deemed redundant as the code describes itself. However, with time, the code tends to get more complicated, add more scenarios, checks, etc. You could end up in a situation where you don’t know what the test was actually testing.

Take a look at the examples below. Which is clearer?

it('should handle submit');

it('when user unauthenticated should not let submit');

Make sure not to test several scenarios in one it . You don’t want to have any if/else statements. If you have a data set, you could organize you tests or describes using it.each and describe.each to run the same testing scenario using different values. This helps both cover all possible cases and have less code to maintain in tests.

Stub dependencies

One of the key points of unit tests — testing them as a separate unit. This means that you need to stub all (or as many as possible) dependencies because you don’t want to test dependencies. You want to test your code. The fewer dependencies you leave for unit tests — the better.

Most of the time, you are going to stub your services. And, this is where the useValue , useClass , that often are asked in interviews, come into play. Let’s imagine that we have a service that makes an HTTP request and does some mapping. How do we test it? Very simple. We provide the minimum mock needed to test the code. Also, we don’t want to waste type on typing, hence we’ll use the infamous any . In tests, it’s okay to use it as we don’t care about type correctness. Only that our code does the correct thing given the circumstances.

describe('MyService', () => {
let service: MyService;

let httpMock: any;

beforeEach(() => {
httpMock = {
get: jest.fn()
};

TestBed.configureTestingModule({
providers: [ { provide: HttpClient, useValue: httpMock } ]
}).runInInjectionContext(() => {
service = new MyService();
});
});

describe('getData', () => {
it('should make a call and return mapped data', async () => {
// After we create a mock, we can add/chain calls to it
// to return a value once or always. Calling multiple times
// `mockReturnValueOnce` gives you a stack of return values.
// I.e., each new call to the mocked method will return a new
// value each time.

// First, we establish the return value our service "returns"
httpMock.get.mockReturnValueOnce(of([]));

// Second, we call the method
const result = await firstValueFrom(service.getData());

// Third, we confirm the expectations
expect(result).toStrictEqual([]);
expect(httpMock.get).toHaveBeenCalledWith('/api/users/get');
});
});
});

Sometimes, however, your code can also rely on some utilities. They also should be mocked. E.g., if we were using date-fns library, we’d write the following.

import { format } from 'date-fns';

jest.mock('date-fns', () => ({
format: jest.fn()
}));

describe('MyService', () => {
// ...

describe('getData', () => {
// `jest.mocked` is simply a type helper to tell TS that
// you can call whatever is available on the `jest.fn` while
// using another function.
// You could also typecast to `any` and call the `mockReturnValueOnce`
// as it's just a matter of typing.
// We provided a `jest.fn` instance in the `jest.mock` part
jest.mocked(format).mockReturnValueOnce('2000-01-01');

// proceed with test and expectations
});
});

There are also times when you want to stub only a part of package. For me, most of the time, it’s Capacitor package.

jest.mock('@capacitor/core', () => ({
...jest.requireActual('@capacitor/core'),
isNativePlatform: jest.fn()
});

By using jest.requireActual we get a real implementation, and, then, we override whatever parts of the module we want.

Testing private methods

Developers often stumble upon private methods. The most intuitive way of testing them would be by making them public (in case of non-exported functions make them exported) or, if it’s non-native private methods (TypeScript private ), simply use as any on the class.

However, this, while working, is an improper way of testing. Private members are private for a reason. They are simply an implementation detail that cannot (should not) be used as a standalone method. The way you test it is by calling the public API. When writing tests, you should not consider your private methods as separate methods to be tested. Instead, think of them as part of the public method’s code. It’s just moved in a different place for readability.

@Injectable()
export class AppService {
#http = inject(HttpClient);

#cache: Promise<ResponseData>;

getData() {
return this.#getCacheOrMakeRequest();
}

async #getCacheOrMakeRequest() {
if (!this.#cache) {
const response = await firstValueFrom(this.#http.get('/api/users'));
this.#cache = #cache;
}

return this.#cache;
}
}

This is a slightly exaggerated example, but it gives you the same “itch” to make it public. Resist and test the public method only. Private method is just a detail of your public method.

describe('AppService', () => {
let service: AppService;

let httpMock: any;

beforeEach(() => {
httpMock = { get: jest.fn() };

TestBed.configureTestingModule({
providers: [ { provide: HttpClient, useValue: httpMock } ]
}).runInInjectionContext(() => {
service = new AppService();
});
});

describe('getData', () => {
it('should make only one request', async () => {
httpMock.get.mockReturnValueOnce(of({ data: 1 }));

await firstValueFrom(service.getData());
const result = await firstValueFrom(service.getData());

expect(result).toStrictEqual({ data: 1 });
expect(httpMock.get).toHaveBeenCalledTimes(1);
});
});
});

ESM dependencies issue

This is the issue that you encounter as soon as your code includes something else apart from the default angular packages. Almost any library, today, distribute ESM. Jest is CJS. Hence, it does not know how to work with ESM and it throws:

SyntaxError: Unexpected token …

Fear not. The issue is very easy to overcome. You simply need to adjust your transformIgnorePatterns property to contain a RegExp that would tell Jest that your library should be ignored. The simplest is to have the RegExp built dynamically. This will improve the readability and make it straightforward to add new libraries in the future to the list.

// jest.config.ts

const esModules = ['@angular', '@ionic', 'lodash-es'];

export default {
// ...
transformIgnorePatterns: [
`node_modules/(?!(.*\\.mjs|${esModules.join('|')}))`
// or for pnpm users `node_modules/.pnpm/(?!(.*\\.mjs|${esModules.join('|')}))`
]
}

Working with dates

Tests should be static. Therefore, if your code performs any manipulations with the dates, you should make it return a very specific date you know you should get upfront. I.e., you should not perform any date manipulations in the tests yourself.

This is easily done by utilizing jest.useFakeTimers() and jest.setSystemTime(new Date('2000-01-01')) . After calling these, you can be sure what “today” is. And you can safely expect a specific static string as a date upfront.

Also, Jest has a fantastic function called jest.advanceTimersByTime() where you can simulate a passage of time. To see how time dependent code behaves with the passage of time.

Working with asynchronous methods

When working with asynchronous methods we usually have three scenarios: a method that returns an observable, a method that returns a promise, a method that doesn’t return anything but has an observable/promise handling under the hood.

Observable and promise

Those are the simplest to test. In case with observable, you want to await firstValueFrom(yourObservable$) and with the promise simply await . While you could try to subscribe, or use then and put your expectations there — it’s a risky approach. Oftentimes, when I see such tests — they pass regardless. Simply because Jest doesn’t know that it needs to wait for something. And a test without expectations is a successfully passed test. Which is wrong.

// Working with a promise
it('should return user data', async () => {
expect(await service.getUser()).toStrictEqual(expectedUserResult);
});

// Working with an observable
it('should return user data', async () => {
expect(await (firstValueFrom(service.getUser$())).toStrictEqual(expectedUserResult);
});

You can risk putting expectations in the subscribe/then, but make sure you use expect.assertions(N) for Jest to know that there are should be N assertions and hence it should wait when it reaches the end of the test suite.

it('should get user data', () => {
expect.assertions(1);

service.getUser().subscribe(result => {
expect(result).toStrictEqual(expectedUserResult);
});
});

Sometimes, however, your asynchronous code can have timers (delay, debounce, setTimeout etc etc). For these scenarios we can use Angular’s testing helpers: fakeAsync and tick.

beforeEach(() => {
httpClientMock = {
get: jest.fn()
};

// ...
});

// `fakeAsync` makes the executed code to use special timers that
// can be easily advanced by other helpers.
// Beware! If you create a timer outside of the `fakeAsync`
// the following will not work for you!
// The timers _have_ to be created as part of the `fakeAsync`
// callback execution.
it('should make typeahead requests', fakeAsync(() => {
// When you work with timers, you don't want to await them.
// The longer the timers, the slower the tests.
jest.expectations(2);
// Simulate an endpoint that takes 1 second to complete
httpClientMock.get.mockReturnValue(
timer(1_000).pipe(
map(() => mockUserData)
)
);

service.getData().subscribe(data => {
expect(data).toStrictEqual(mockUserData);
});

expect(httpClientMock.get).toHaveBeenCalledWith('/users/1');
// Simulate a passage of 1 second and run microtasks (and makrotasks)
tick(1_000);
// You can also test a more fine-grained scneraios by advancing
// by smaller values of time to verify that something doesn't
// happen too early.
}));

In general, you should attempt to make your timers to be created inside fakeAsync . If not, ticks won’t work and you’ll need to use some workarounds. They, usually, would invoke something like

jest.useFakeTimers();
// Start the timers
jest.advanceTimersByTime(1_000);
jest.useRealTimers();
// A rare escape hatch when the timer doesn't want to start
// because it needs a new tick. So we have to manually create it.
// Ideally, you shouldn't need it, but with timers created somewhere
// "outside" sometimes you have to. So, it's better to know of such option.
await (firstValueFrom(Promise.resolve()));

Another approach would be to use await jest.runAllTimersAsync() .

jest.useFakeTimers();
await jest.runAllTimersAsync();
// or await jest.advanceTimersByTimeAsync(1_000);

Keeping the console clean

One of the many things developers often forget — keeping the console clean! It doesn’t matter if it’s a build output, eslint output, stylelint output, warnings/errors in the browser during runtime, console logs/errors/warnings/whatever during Jest execution. The logs are there for a reason. And if you see them — 99% something is off, and you need to handle them.

In general, when you run Jest, you don’t want any output to be present except for the PASS logs for successful tests.

Sometimes, however, your code prints something to the console. E.g., you are making a request and you want to log an error to the console should it fail for developers to know what happened and where. How can we fix that in Jest? By using spies.

Press enter or click to view image in full size
it('should log error on request fail', () => {
httpClientMock.get.mockReturnValue(
throwError(() =>
new HttpErrorResponse({ status: 400 })
)
);
// Setup a spy
const spy = jest.spyOn(console, 'error')
// We "remove" the underlying implementation.
// If we don't call the method – we will still
// get a spy, however the underlying method will
// still execute which we don't want.
.mockImplementation();

// Subscription happens inside the method
// so it doesn't return anything
service.updateData();

expect(spy).toHaveBeenCalledWith('Error fetching data');
});

In other scenarios, especially with effects and other kinds of async code — errors in the console, usually, tell us that the code we have called actually doesn’t work (perhaps due to insufficient mocks) and we need to revisit, debug and fix that. Either, by supplying extra mocks, or by adjusting tests/implementation.

Bonus tip. Working with inputs

There are times when your component has an input which you want to set for the testing purposes. There are two ways to proceed:
1. The “right” one. You should use a host component testing which would render your component. And, that component, would set the tested component’s inputs. Easy-peasy.
2. The path of the chosen ones. Stub Angular APIs.

I’ll describe the second. Usually, my teams and I avoid using TestBed as much as possible due to execution time penalty. We prefer to do the UI testing in the E2E or by QAs, and we want our unit tests to be as fast as possible. Therefore, we can mock the APIs. It’s not a proper method because we kinda hack the framework and replace its implementation, so in future updates it may break, but, so far, I don’t think this one would be the case.

jest.mock('@angular/core', () => {
const actual = jest.requireActual('@angular/core');

return {
...actual,
input: actual.signal
};
})

You may need to adjust the mock for your scenario as this example covers only the basic usage but, you get the idea.

We simply replace input with a regular signal . And, after that, we can simply (component.myInput as any).set(value) and it will work as inputs are still signals just with a hidden setter method.

Also, recently I discovered an alternative approach thanks to my team member Boris who found this in Angular’s source code 😎.

import { SIGNAL, signalSetFn } from '@angular/core/primitives/signals';

describe('MyComponent', () => {
// ...

it('should do something', () => {
signalSetFn(component.myInput[SIGNAL], 'value');

// proceed with testing
});
});

This one looks like pure dark magic using Angular’s internals 😁.

--

--

Sergey Gultyayev
Sergey Gultyayev

Written by Sergey Gultyayev

A front-end developer who uses Angular as a main framework and loves it

Responses (1)