Angular Testing Made Easy: Understanding the Testing Pyramid and Focusing on Unit Testing 2023

William Bastidas
williambastidasblog
13 min readJan 28, 2023

Testing is an essential part of building a robust and maintainable application. It helps to ensure that your code is working as expected and that any changes you make in the future don’t break existing functionality.

This tutorial will explain the testing pyramid in Angular, and show you how to write unit tests using the built-in testing framework, Jasmine. By the end of this tutorial, you will have an idea of how to structure your unit tests in Angular, and how to use the tools provided by the framework to start write and run tests.

The testing pyramid in Angular: How It Works?

The testing pyramid in Angular is a concept that emphasizes the importance of different types of tests in an Angular application. The testing pyramid is a visual representation of the proportion of different types of tests that should be written for an application.

At the bottom of the pyramid are unit tests, which test individual pieces of code in isolation. These tests are fast, easy to write, and provide the most value for the least amount of effort. They are usually written using frameworks like Jasmine or Jest.

Above unit tests are integration tests, which test how different units of code work together. These tests are slower and more complex than unit tests, but are still important for ensuring that the different parts of the application are working together correctly.

At the top of the pyramid are end-to-end (E2E) tests, which test the entire application as a whole. These tests are the slowest and most expensive to write and maintain, but are essential for ensuring that the application behaves correctly from the user’s perspective.

The testing pyramid in Angular emphasizes that the majority of tests should be unit tests, with fewer integration tests and even fewer E2E tests.

This approach helps ensure that the majority of testing effort is focused on the most important and valuable tests, while still providing adequate coverage for the entire application.

So, we have:

  1. Unit Tests: These tests focus on individual units of code, such as components, services, and pipes. They are fast and easy to write, and they should make up the majority of your tests.
  2. Integration Tests: These tests focus on the interaction between different units of code. They are slightly slower and more complex than unit tests, but they are still relatively fast and easy to write.
  3. E2E Tests: These tests focus on the end-to-end functionality of your application. They are slower and more complex than unit and integration tests, but they are still important for ensuring that your application behaves correctly in a real-world scenario.

In Angular, you can use the built-in testing framework, Jasmine, to write unit tests and integration tests. You can also use a tool like Protractor to write E2E tests.

When you are writing tests, it is important to keep the testing pyramid in mind. The majority of your tests should be unit tests, with fewer integration tests and even fewer E2E tests. This will help you keep your tests fast and maintainable.

It’s also important to note that Angular also has a feature called Ivy which is the new rendering engine for Angular. Ivy can change how you test your application, but the concept of the testing pyramid still applies.

In general, different roles within a development team may be responsible for different types of tests in Angular.

Unit tests are usually the responsibility of developers. They write the code and they are familiar with the inner workings of the codebase. They write these tests in order to ensure that the code they wrote is working correctly and will continue to work correctly even as the codebase evolves.

Integration tests are usually the responsibility of developers and QA engineers. These tests typically require a deeper understanding of the codebase and how different parts of the application work together.

Developers may write these tests to ensure that the different units of code they wrote are working together correctly. QA engineers may also write these tests to ensure that the application meets the requirements and behaves correctly from the user’s perspective.

End-to-end (E2E) tests are usually the responsibility of QA engineers. These tests test the application as a whole, including the user interface and how the application behaves in different situations. QA engineers write these tests to ensure that the application behaves correctly from the user’s perspective and that it meets the requirements.

It’s also common that the development team use test automation frameworks or tools to run these tests, but they are the ones who write the test cases and scenarios.

Focusing on Unit Testing

There are several core elements within unit tests in Angular that are commonly used and I’d like to define them first:

  1. describe: The describe function is used to group together related test cases. It takes two arguments: a string describing the group of tests, and a callback function that contains the test cases.
  2. it: The it function is used to define individual test cases. It takes two arguments: a string describing the test case, and a callback function that contains the code for the test.
  3. beforeEach: The beforeEach function is used to set up any state or variables that are needed for the tests. It is run before each it function.
  4. expect: The expect function is used to make assertions about the code being tested. It is used to check that the expected output is produced by the code.
  5. spyOn: The spyOn function is used to create a spy on a function that is called within the code being tested. This is useful for testing that a function is called with the correct arguments, or for testing that a function is called a certain number of times.
  6. fakeAsync and tick: The fakeAsync and tick functions are used to test asynchronous code. fakeAsync wraps a test case and tick is used to move the virtual clock forward in time.
  7. async and flush : async and flush function are used to test asynchronous code. async wraps a test case and flush is used to move the virtual clock forward in time.

TestBed and ComponentFixture are also important core elements within unit tests in Angular.

  1. TestBed: TestBed is a powerful testing module provided by the Angular testing framework. It allows you to configure and create components and services for testing, and provides a way to interact with the component under test.
  2. ComponentFixture: ComponentFixture is a class that is provided by the Angular testing framework. It is used to interact with the component being tested, and provides methods for querying the component's template and state, as well as for triggering events on the component.

In Angular unit tests, TestBed is used to configure a testing module and create a component instance. ComponentFixture is used to interact with the component instance, by querying its elements, properties and methods.

For example,

beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [MyComponent],
imports: [],
}).compileComponents();

fixture = TestBed.createComponent(MyComponent);
component = fixture.componentInstance;
fixture.detectChanges();
}));

In this example, We are configuring the testing module and creating a component instance using TestBed.createComponent() method, and the ComponentFixture object is created by calling the TestBed.createComponent() method. Then we can interact with the component by accessing its properties and methods via the component object, and also query its elements via the fixture object.

ComponentFixture provides methods like detectChanges() which runs change detection on the component, debugElement and nativeElement which are used to access the component's template, and whenStable() which is used to wait for asynchronous activities to complete before running tests.

These elements are commonly used in Angular unit tests, but other elements may be used as well depending on the specific requirements of the test.

In addition to the core elements of unit tests in Angular, there are a few other important elements that could be mentioned:

  1. Mocks: Mocks are objects that mimic the behavior of real objects in order to test the code that depends on them. In Angular, mocks are often used to replace services that are called by the component or directive being tested.
  2. TestBed.override: The TestBed.override method allows you to override the configuration of a module or component for the purpose of testing. This is often used to change the providers of a component, or to add a spy on a service.
  3. TestBed.get: TestBed.get method is used to get the instance of a service or component that has been created by the TestBed. This allows you to access the methods and properties of the service or component for the purpose of testing.
  4. TestBed.inject: TestBed.inject method is used to get the instance of a service or component that has been created by the TestBed. This allows you to access the methods and properties of the service or component for the purpose of testing.
  5. TestBed.compileComponents() : TestBed.compileComponents() method is used to compile the components after the configureTestingModule is called.
  6. DebugElement: DebugElement is a testing-specific wrapper around a DOM element. It allows you to interact with the element's properties and trigger events in the same way that you would interact with a real element in the browser.
  7. Fixture.whenStable(): whenStable method is used to wait for all the asynchronous activities to complete before running the test.
  8. Fixture.detectChanges(): detectChanges method is used to run change detection on the component and update the view with the latest data.
  9. Fixture.debugElement.query(By.css()): debugElement.query method is used to query for elements in the component's template.
  10. Fixture.debugElement.injector: debugElement.injector is used to get the Injector associated with a specific debug element. It can be used to retrieve instances of services that are provided to the component.

Unit test examples

  1. Component test:
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { DebugElement } from '@angular/core';
import { MyComponent } from './my.component';

describe('MyComponent', () => {
let component: MyComponent;
let fixture: ComponentFixture<MyComponent>;
let debugElement: DebugElement;
let htmlElement: HTMLElement;

// This function is executed before each test case.
// It sets up the test environment by configuring the TestBed module,
// creating the ComponentFixture and setting up the variables for the component,
// the DebugElement, and the native HTML element.
beforeEach(() => {
// Configure the TestBed module to test the MyComponent
TestBed.configureTestingModule({
declarations: [ MyComponent ]
});

// Create the ComponentFixture for the MyComponent
fixture = TestBed.createComponent(MyComponent);
// Get the instance of the component
component = fixture.componentInstance;
// Get the DebugElement for the component's element
debugElement = fixture.debugElement.query(By.css('p'));
// Get the native HTML element for the component's element
htmlElement = debugElement.nativeElement;
});

// Test case to check that the original title is displayed in the DOM
it('should display original title', () => {
// Detect changes in the component
fixture.detectChanges();
// Expect the text content of the component's element to contain the component's title
expect(htmlElement.textContent).toContain(component.title);
});

// Test case to check that a different title can be displayed in the DOM
it('should display a different test title', () => {
// Change the component's title
component.title = 'Test Title';
// Detect changes in the component
fixture.detectChanges();
// Expect the text content of the component's element to contain the new title
expect(htmlElement.textContent).toContain('Test Title');
});
});

In this example, we are testing the MyComponent component. We are using the TestBed module to configure the testing environment and create a ComponentFixture for the component.

We are also using the DebugElement to interact with the component's element in the test and the expect function from the testing framework to check that the text content of the component's element contains the expected text.

The beforeEach function sets up the test environment before each test case, and the two it functions define two test cases: one to check that the original title is displayed in the DOM and another one to check that a different title can be displayed in the DOM.

2. Service test:

import { TestBed } from '@angular/core/testing';
import { MyService } from './my.service';
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';

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

// This function is executed before each test case.
// It sets up the test environment by configuring the TestBed module and creating the MyService instance.
beforeEach(() => {
// Configure the TestBed module to test the MyService
TestBed.configureTestingModule({
imports: [HttpClientTestingModule],
providers: [MyService]
});
// Get the instance of the MyService
service = TestBed.get(MyService);
httpTestingController = TestBed.get(HttpTestingController);
});

// Test case to check that the service method getData() returns the expected data
it('should return expected data', () => {
const mockData = {id: 1, name: 'Test Data'};

// Call the service method
service.getData().subscribe(data => {
// Expect the data to be equal to the mock data
expect(data).toEqual(mockData);
});

// Expect a GET request to the specified url
const req = httpTestingController.expectOne('/api/data');
// Expect the request to be a GET request
expect(req.request.method).toEqual('GET');
// Respond to the request with the mock data
req.flush(mockData);
// Verify that there are no outstanding requests
httpTestingController.verify();
});
});

In this example, we are testing the MyService service. We are using the TestBed module to configure the testing environment and create an instance of the service.

We are also using the HttpClientTestingModule to handle the HTTP requests and the HttpTestingController to set up the expected requests and responses.

The beforeEach function sets up the test environment before each test case, and the it function defines a test case to check that the service's getData() method returns the expected data.

In the test case, we are calling the getData() method, expecting that the returned data is equal to the mock data, we are expecting a GET request to the specified url, and we are responding to the request with the mock data and finally verifying that there are no outstanding requests.

3. Pipe test

import { MyPipe } from './my.pipe';

describe('MyPipe', () => {
let pipe: MyPipe;

beforeEach(() => {
// Create an instance of the MyPipe
pipe = new MyPipe();
});

// Test case to check that the pipe correctly transforms the input data
it('should transform input data', () => {
// Input data
const input = 'input string';
// Expected output
const output = 'INPUT STRING';

// Call the pipe's transform method and pass in the input data
const result = pipe.transform(input);
// Expect the output to be equal to the expected output
expect(result).toEqual(output);
});
});

In this example, we are testing the MyPipe pipe. We are using the beforeEach function to create an instance of the pipe before each test case.

The it function defines a test case to check that the pipe correctly transforms the input data. In the test case, we are passing an input string to the pipe's transform() method, expecting that the returned result is equal to the expected output and comparing it with the output value.

In this case the pipe is taking an input string and changing it to uppercase, so we are passing an input string and expecting an output string in uppercase.

Focus on testing the logic and the outputs

These are just some examples to give you an idea of how unit tests are written in Angular. Keep in mind that you should test the behavior of your components, services, and pipes in isolation, and that you should focus on testing the logic and the outputs of your code, rather than the implementation details. This is important because implementation details can change over time, and the code may still work correctly even if those implementation details change.

For example, if you have a function that takes two numbers as input and returns the sum of those numbers, you would write a unit test to ensure that the function returns the correct result, rather than testing how the function calculates that result. This way, even if you change the algorithm to calculate the sum, as long as it continues to return the correct result, the test will continue to pass.

By focusing on the behavior rather than the implementation, unit tests are less likely to need to be updated when code changes, making them more reliable and maintainable over time.

Also, it’s important to note that these examples use Jasmine as the testing framework, which is the default testing framework in Angular, but you could use other frameworks like Mocha or Jest

Why unit tests are the bottom of the pyramid?

There are several reasons why unit tests are currently more widely used than other types of tests in Angular:

  1. Unit tests are fast: Because unit tests only test a small, isolated piece of code, they run much faster than other types of tests such as integration or end-to-end tests. This allows developers to quickly and frequently run their tests, making it easy to catch and fix bugs early in the development process.
  2. Unit tests are easy to write: Unit tests are relatively simple to write and understand. They are often written in the same language as the code they are testing, making them accessible to developers of all skill levels.
  3. Unit tests promote good design: Unit tests encourage developers to write code that is modular, testable, and easy to understand. By writing unit tests, developers are forced to think about how their code can be broken down into smaller, testable units.
  4. Unit tests make code more robust: By thoroughly testing the individual units of code, unit tests can help ensure that the code is working correctly and that it will continue to work correctly even as the codebase grows and evolves.
  5. Unit tests improve the development process: By running unit tests frequently, developers can catch and fix bugs early on in the development process, which can save time and resources in the long run. Additionally, unit tests can help make refactoring and code changes less risky by providing a safety net that ensures that changes to the codebase do not break existing functionality.

Conclusion

In conclusion, unit testing is an essential part of developing high-quality Angular applications. By writing unit tests, you can ensure that your code is working correctly and that it will continue to work correctly even as your codebase grows and evolves.

The testing pyramid in Angular, which emphasizes unit tests over other types of tests, is a useful guide for structuring your tests. To write effective unit tests, it’s important to understand the core elements of unit tests in Angular.By including these elements in your tests, you can write tests that are efficient, maintainable, and easy to understand.

Did you enjoy the post? Do you think there’s anything that could be improved or done differently? Don’t hesitate to leave it in the comments! You can also connect with me on my Twitter, facebook y LinkedIn accounts. ☺

Don’t forget to share it and feel free to give it a clap or two! 👏

--

--

William Bastidas
williambastidasblog

Developer | Web | Mobile | Ionic | TypeScript | JavaScript | Angular | UI | UX | Git | Html | CSS | Agile | Frontend | PWA. Always in Learning mode…