By using this site, you agree to the Privacy Policy and Terms of Use.
Accept
World of SoftwareWorld of SoftwareWorld of Software
  • News
  • Software
  • Mobile
  • Computing
  • Gaming
  • Videos
  • More
    • Gadget
    • Web Stories
    • Trending
    • Press Release
Search
  • Privacy
  • Terms
  • Advertise
  • Contact
Copyright © All Rights Reserved. World of Software.
Reading: Here’s What the Pros Don’t Tell You About Angular Unit Testing | HackerNoon
Share
Sign In
Notification Show More
Font ResizerAa
World of SoftwareWorld of Software
Font ResizerAa
  • Software
  • Mobile
  • Computing
  • Gadget
  • Gaming
  • Videos
Search
  • News
  • Software
  • Mobile
  • Computing
  • Gaming
  • Videos
  • More
    • Gadget
    • Web Stories
    • Trending
    • Press Release
Have an existing account? Sign In
Follow US
  • Privacy
  • Terms
  • Advertise
  • Contact
Copyright © All Rights Reserved. World of Software.
World of Software > Computing > Here’s What the Pros Don’t Tell You About Angular Unit Testing | HackerNoon
Computing

Here’s What the Pros Don’t Tell You About Angular Unit Testing | HackerNoon

News Room
Last updated: 2025/02/20 at 6:48 AM
News Room Published 20 February 2025
Share
SHARE

Introduction

In this article, I would like to share the experience I gained over the years with unit testing in Angular. In a nutshell I will be speaking about the followings:

  • Why you should unit test?
  • Why you should mock and what are the advantages/disadvantages?
  • What are SIFERS and why you should care?
  • What is the Angular Testing Library (ATL)?
  • Testing using SIFERS
  • Querying DOM elements and dispatching events
  • What are jest-auto-spies and observer-spy?

Why?

I have seen many applications that do not contain any unit tests. Let me clarify why we need to write unit tests. Unit tests are a necessary aspect of any application. They provide us with assurance and confirmation about how the code should behave. They also serve as documentation to understand what the code is doing. Writing good tests helps us in understanding the design of the code. Not being able to write a unit test, indicates a bad design and usually tells us to refactor the code.

The more your tests resemble the way your software is used, the moreconfidence they can give you.

Mocks

To make sure you can concentrate on the code that has to be tested, you must properly mock external dependencies. For instance, you should mock any services or other components that the component you are testing utilizes. It’s not advised to import the real implementations (more on why below). Feel free to import pure components, though, if your component uses them as dependents. You may also import a shared module that contains all of its dependencies.

Disadvantages of not mocking

  • You will be using the real implementation and are forced to mock all of its properties, methods, etc. You will end up in a rabbit hole, where you are suddenly mocking classes that several layer down the dependency tree.
  • You will have to declare the nested components and provide all of its dependencies
  • It takes longer for your tests to execute since the complete dependency tree must be resolved first.
  • The state of your tests might not be correct.
  • Your tests will suddenly start to fail if a dependency downstream changes.
  • It becomes very difficult to debug the tests when an error occurs.

SIFERS

Let’s start with the setup. Instead of using the beforeEach to set up the testing environment, I use SIFERS.

Simple Injectable Functions Explicitly Returning State (SIFERS) is a way to capture what the tests should do when setting up the testing environment as well as returning a mutable clean state.

SIFERS use a setup function that can receive optional arguments to set up the testing environment. This is the biggest benefit compared to beforeEach. beforeEach gets called automatically before each unit test, hindering us to set any mocked values on the dependencies that are needed during the initialization of the component/service.

Using SIFERS allows us to be much more flexible with the testing environment to mock values before the component/service is initialized.

The setup function is then called in every test and can return a state for your test (Classes, Properties etc..).

One thing I like in my SIFERS is to try to keep the number of arguments small. If you need multiple arguments or your list is growing over a certain number of parameters, you can use an interface. This will keep your code organized and easy to read.

A simple setup function can look like this:

function setup({ value = false }) {
  const mockService: Partial<RealService> = {
    someFunction: jest.fn()
      .mockReturnValue(value ? 'foo' : 'bar'),
  };

  const service = new MyService(mockService);
  return {
    service,
    mockService,
  };
}

Using the above example, the tests can look like this:

it('returns foo when feature flag is enabled', () => {
  // Pass true into the setup to ensure that 
  // someFunction returns foo
  const { service } = setup(true);
  expect(service.someFunction()).toEqual('foo');
});

it('returns bar when feature flag is disabled', () => {
  // Pass false into the setup to ensure that 
  // someFunction returns bar
  const { service } = setup(false);
  expect(service.someFunction()).toEqual('bar');
});

I am not going into the full details of SIFERS here as it’s already very well explained by the author Moshe Kolodny.

Testing with SIFERS

Angular Testing Library (ATL)

I am a big fan of the ATL library and try to use it in all of my projects. ATL is a very lightweight solution to test Angular components. ATL is described as:

Angular Testing Library provides utility functions to interact with Angular components, in the same way as a user would.

Tim Deschryver

Let’s start with the setup of module. Instead of using TestBed.configureTestingModule, you need to use the render method. Keep in mind that the render method should only be used if you are testing components. Services can be tested without ATL and the render method.

There are many examples of how to use the ATL here. They contain everything from Components, Forms, Input/Output, NGRX, Directives, Angular Material, Signals etc. Tim Deschryer also has a very detailed article with lots of examples that I recommend reading.

Here is an example using a SIFER, and the render method. You will also notice that I am using the createSpyFromClass method to mock the classes, which automatically mocks all functions, properties and even observables for us automatically. More on that is covered later in the article.

import { render } from '@testing-library/angular';
import { createSpyFromClass } from 'jest-auto-spies';
// ... other imports

async function setup({ enableFlag = false }) {
  const mockMySomeService = createSpyFromClass(MyService);
  mockMySomeService.doSomething.mockReturnValue(enableFlag);

  const { fixture } = await render(AppComponent, {
    imports: [...],
    providers: [{ 
      provide: MyService, 
      useValue: mockMySomeService 
    }],
  });
}

Setting declarations

Similar to TestBed, you can pass in a collection of components and directives using declarations. The syntax is the same.

However if you are importing a module, that already contains the component, then you need to set the excludeComponentDeclaration to true. Some other useful properties that you may need to use in your tests from the ATL API. See the full API for examples.

Setting providers

Use the componentProviders to set the providers for your component.

Set @Input/@Output

Setting the @Input and @Output properties of the component can be achieved using componentProperties. This allows you to set them both at the same time.

If you need more control over those properties you can use the componentInputs or componentOutputs. In a TestBed based test, you would probably just set the input through the component instance itself.

Testing Services

To test services, you do not need to use the ATL or TestBed. Instead, you can pass the mocked dependencies directly into the constructor of the service to be tested. The below example mocks the LogService and TableService.

// some.service.ts
@Injectable({ providedIn: 'root' })
export class SomeService {
  constructor(
    private readonly logService: LogService, 
    private readonly tableService: TableService) {}
}

// some.service.spec.ts
async function setup() {
  const mockLogService = createSpyFromClass(LogService);
  const mockTableService = createSpyFromClass(TableService);

  const service = new SomeService(
    mockLogService, 
    mockTableService
  );

  return {
    service,
    mockLogService,
    mockTableService,
  };
}

Testing Components

A component should always test the behaviour of the public API. Private APIs are never tested explicitly. To test the components, use the DOM as much as possible. This is the same behaviour you would expect from your user and you want your test to mimic this as much as possible. The ATL helps us with this. This is also called a shallow testing.

Do not treat all public methods of your component as the public API that you can test directly from your unit tests. They are only public for your template. The public methods are called from the DOM (i.e. a button click) and should be tested in the same manner.

Example of a component to be tested:

// app-foo.component.ts
@Component({
  selector: 'app-foo',
  template: `
    <input 
      data-testid='my-input'
      (keydown)='handleKeyDown($event)' />`
})
export class FooComponent {
  constructor(private readonly someService: SomeService) {}

  handleKeyDown(value: string) {
    this.someService.foo(value);
  }
}

Your SIFER setup could look like this:

// app-foo.component.spec.ts
async function setup() {
  const mockSomeService = createSpyFromClass(SomeService);
  const { fixture } = await render(FooComponent, {
    providers: [{ 
      provide: SomeService, 
      useValue: mockSomeService 
    }],
  });

  return {
    fixture,
    mockSomeService,
    fixture.componentInstance
  }
}

Do not do this. The test is directly accessing the public API, bypassing the template entirely. You could remove the DOM input element entirely and the test would still pass. This is a false positive test and does not serve any purpose.

it('emits a value', async () => {
  const { mockSomeService, component } = await setup(...);
  component.handleKeyDown(value);

  expect(mockSomeService.foo)
    .toHaveBeenCalledWith(value);
})

This is the correct way to test the function. Here I am using screen to get access to the input element and userEvent to emit DOM events.

import { screen } from '@testing-library/angular';
import userEvent from '@testing-library/user-event';

it('emits a value', async () => {
  const { mockSomeService, component } = await setup(...);
  const textbox = screen.queryByTestId('my-input');

  userEvent.type(textbox, 'foo,');
  userEvent.keyboard('{Enter}');

  expect(mockSomeService.foo)
    .toHaveBeenCalledWith(value);
})

Query DOM Elements using screen

The screen API provides several powerful functions to query the DOM. Functions like waitFor or findBy return a promise and can be used to find elements that toggle their visibility dynamically based of some conditions.

It is recommended to query the elements in the following order. See the API for the full priority list and their descriptions.

  1. getByRole
  2. getByLabelText
  3. getByPlaceholderText
  4. getByText
  5. getByDisplayValue
  6. getByAltText
  7. getByTitle
  8. getByTestId

Dispatching DOM Actions

ATL comes with two APIs to dispatch events through the DOM:

userEvent is preferred over fireEvents (provided by the Events API). The difference as provided in the docs is:

fireEvent dispatches DOM events, whereas user-event simulates full interactions, which may fire multiple events and do additional checks along the way.

jest-auto-spies

To Mock classes, I use jest-auto-spies. jest-auto-spies return a mocked type safe class without having to manually define all of its functions and properties. Besides saving a lot of time, it also provides helper functions for observables, methods, getters, and setters. The below example is using the createSpyFromClass method to create a spy class.

If you need to provide dependencies into your module directly, you can use provideAutoSpy(MyClass), which is a shortcut for {provide: MyClass, useValue: createSpyFromClass(MyClass)}.

Keep in mind that this should only be used if you don’t need to mock any functions of that class. If you need to mock something from that class, then you should provide the mocked instance.

Here are a few examples:

Create a simple spy on a class

const mockMyService = createSpyFromClass(MyService);

Create a spy on class and emit a value on an observable

const mockMyService = createSpyFromClass(MyService, {
    observablePropsToSpyOn: ['foo$'],
});

mockMyService.foo$.nextWith('bar');

Create a spy on a class and function

const mockMyService = createSpyFromClass(MyService, {
    methodsToSpyOn: ['foo'],
});

mockMyService.foo.mockReturnValue('bar');

See jest-auto-spies for more helper functions and its usage.

Use observer-spy instead of subscribe / avoid the done callback

To test asynchronous code, I use subscribeSpyTo from the observer-spy library instead of subscribing to the observable. This also helps to get rid of the done callback.

The done function was introduced to test async code. However, it is very error prone and unpredictable. Because of that you might get false positives making your tests green, while other times you might get a timeout.

Note that there is a lint rule that you can use to prohibit the usage of the done callback.

You also do not need to unsubscribe from the observable, as there is an auto unsubscribe hookin place. This gets called automatically in afterEach.

Here are a few examples on how to use the library taken from their readme. See more examples in the readme.

const fakeObservable = of('first', 'second', 'third');
const observerSpy = subscribeSpyTo(fakeObservable);

// No need to unsubscribe, as the have an auto-unsubscribe in place.
// observerSpy.unsubscribe();

// Expectations:
expect(observerSpy.getFirstValue()).toBe('first');
expect(observerSpy.receivedNext()).toBeTruthy();
expect(observerSpy.getValues()).toEqual(fakeValues);
expect(observerSpy.getValuesLength()).toBe(3);
expect(observerSpy.getValueAt(1)).toBe('second');
expect(observerSpy.getLastValue()).toBe('third');
expect(observerSpy.receivedComplete()).toBeTruthy();

Resources

Summary

The article explored my experience with unit testing in Angular, emphasizing its importance for code quality and maintainability. It covered the necessity of proper mocking to isolate units and avoid reliance on real implementations. SIFERS were highlighted as a flexible approach for setting up the testing environment and enhancing test readability. The Angular Testing Library (ATL) was introduced as a valuable tool for component testing, mimicking user interactions as close as possible to real users. Additionally, the article mentioned useful tools like jest-auto-spies for efficient class mocking and provided resources for further exploration of testing practices in Angular.

Sign Up For Daily Newsletter

Be keep up! Get the latest breaking news delivered straight to your inbox.
By signing up, you agree to our Terms of Use and acknowledge the data practices in our Privacy Policy. You may unsubscribe at any time.
Share This Article
Facebook Twitter Email Print
Share
What do you think?
Love0
Sad0
Happy0
Sleepy0
Angry0
Dead0
Wink0
Previous Article I’m Not Convinced Ethical Generative AI Currently Exists
Next Article Google has dropped a new feature for iPhone similar to Circle-to-Search
Leave a comment

Leave a Reply Cancel reply

Your email address will not be published. Required fields are marked *

Stay Connected

248.1k Like
69.1k Follow
134k Pin
54.3k Follow

Latest News

Newark airport radar access briefly lost again
News
Does video game monetisation harm children – and what is Australia doing about it?
News
How to Implement Account-based Marketing: 7 Real-Life Examples
Computing
Sony Xperia 1 VII leaks big time ahead of launch event
News

You Might also Like

Computing

How to Implement Account-based Marketing: 7 Real-Life Examples

32 Min Read
Computing

What Happens When Blockchain Miners Cheat the System | HackerNoon

8 Min Read
Computing

OtterCookie v4 Adds VM Detection and Chrome, MetaMask Credential Theft Capabilities

10 Min Read
Computing

BREAKING: 7,000-Device Proxy Botnet Using IoT, EoL Systems Dismantled in U.S. – Dutch Operation

6 Min Read
//

World of Software is your one-stop website for the latest tech news and updates, follow us now to get the news that matters to you.

Quick Link

  • Privacy Policy
  • Terms of use
  • Advertise
  • Contact

Topics

  • Computing
  • Software
  • Press Release
  • Trending

Sign Up for Our Newsletter

Subscribe to our newsletter to get our newest articles instantly!

World of SoftwareWorld of Software
Follow US
Copyright © All Rights Reserved. World of Software.
Welcome Back!

Sign in to your account

Lost your password?