13th June 2024

A Comprehensive Guide to Unit Testing in Angular

Unit-Testing-In-Angular-VS-Online-img

Unit testing is a fundamental aspect of modern web development that ensures individual parts of your application work as intended. In Angular, unit testing involves testing components, services, pipes, and other parts of the application in isolation. This guide will walk you through the basics of unit testing in Angular, setting up your testing environment, writing and running tests, and best practices.

What is Unit Testing?

Unit testing is the process of testing the smallest parts of an application, known as units, to ensure they function correctly. In Angular, a unit can be a component, service, pipe, or directive. Unit tests are typically automated and help catch bugs early in the development process.

Why Unit Test in Angular?

  • Early Bug Detection: Identifies issues early in the development cycle.
  • Code Quality: Ensures that each unit of code works as expected.
  • Refactoring Safety: Provides confidence to refactor code without breaking functionality.
  • Documentation: Serves as documentation for the expected behavior of the code.

Setting Up Your Testing Environment

Angular projects come with a testing setup by default, using Jasmine as the testing framework and Karma as the test runner. Here’s how to get started:

1. Install Angular CLI (if not already installed):
                                
                                    
    npm install -g @angular/cli
  
                                
                            
2. Create a New Angular Project:
                                
                                    
    ng new my-angular-app
    cd my-angular-app
  
                                
                            
3. Run Tests:
                                
                                    
    ng test
  
                                
                            

This command starts the Karma test runner, which runs all the tests in your project and provides feedback on their status.

Different Ways to Achieve Unit Testing in Angular

Before diving into specific examples using Jasmine and Karma, let's explore the different ways you can achieve unit testing in Angular.

1. Jasmine and Karma
  • Jasmine: A behavior-driven development framework for testing JavaScript code. It provides functions to write tests and assertions.
  • Karma: A test runner that runs tests in various browsers and reports the results. It is often used in conjunction with Jasmine for running Angular tests.

This is the default setup provided by the Angular CLI and is widely used in the Angular community.

2. Jest

Jest is a JavaScript testing framework developed by Facebook, known for its simplicity and speed. It can be used as an alternative to Jasmine and Karma.

Advantages:
  • Faster test execution.
  • Better snapshot testing support.
  • Easier setup and configuration.

To use Jest in an Angular project, you can follow these steps:

1. Install Jest and related packages:
                                
                                    
    ng add @briebug/jest-schematic
  
                                
                            
2. Run Tests:
                                
                                    
  ng test
 
                                
                            

This command will run tests using Jest instead of Karma.

3. Mocha and Chai

Mocha is a feature-rich JavaScript test framework running on Node.js, and Chai is an assertion library. They can be used together to write and run unit tests in Angular.

Advantages:
  • Flexible and customizable.
  • Rich ecosystem of plugins and reporters.

To use Mocha and Chai in an Angular project, you need to configure the test setup manually, which can be more complex compared to using Jasmine and Karma.

Writing Unit Tests in Angular

Example: Testing a Component

Let's write a unit test for a simple Angular component using Jasmine and Karma.

Component Code (example.component.ts):
                                
                                    
  import { Component } from '@angular/core';

    @Component({
      selector: 'app-example',
      template: '<h1>{{ title }}</h1>'
    })
    export class ExampleComponent {
      title = 'Hello, World!';
    }
 
                                
                            
Unit Test (example.component.spec.ts):
                                
                                    
  import { ComponentFixture, TestBed } from '@angular/core/testing';
  import { ExampleComponent } from './example.component';

  describe('ExampleComponent', () => {
    let component: ExampleComponent;
    let fixture: ComponentFixture<ExampleComponent>;

    beforeEach(async () => {
      await TestBed.configureTestingModule({
        declarations: [ExampleComponent]
      }).compileComponents();
    });

    beforeEach(() => {
      fixture = TestBed.createComponent(ExampleComponent);
      component = fixture.componentInstance;
      fixture.detectChanges();
    });

    it('should create', () => {
      expect(component).toBeTruthy();
    });

    it('should have title "Hello, World!"', () => {
      expect(component.title).toBe('Hello, World!');
    });

    it('should render title in a h1 tag', () => {
      const compiled = fixture.nativeElement;
      expect(compiled.querySelector('h1').textContent).toContain('Hello, World!');
    });
  });

                                
                            
Breakdown of the Unit Test
  • Setup (beforeEach blocks): The beforeEach blocks set up the testing environment by configuring the testing module and creating the component instance.
  • Test Cases (it blocks): Each it block contains a single test case. The first test checks if the component is created successfully. The second test checks the value of the component's title property. The third test verifies that the title is rendered correctly in the template.

Testing Services

Testing services involves ensuring that the logic in your services works correctly.

Service Code (data.service.ts):
                                
                                    
  import { Injectable } from '@angular/core';

  @Injectable({
    providedIn: 'root'
  })
  export class DataService {
    getData() {
      return ['Data 1', 'Data 2', 'Data 3'];
    }
  }

                                
                            
Unit Test (data.service.spec.ts):
                                
                                    
  import { TestBed } from '@angular/core/testing';
  import { DataService } from './data.service';

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

    beforeEach(() => {
      TestBed.configureTestingModule({});
      service = TestBed.inject(DataService);
    });

    it('should be created', () => {
      expect(service).toBeTruthy();
    });

    it('should return data', () => {
      const data = service.getData();
      expect(data).toEqual(['Data 1', 'Data 2', 'Data 3']);
    });
  });

                                
                            
Mocking Dependencies

When unit testing components or services that depend on other services, it's essential to mock these dependencies to isolate the unit under test.

Component Code with Dependency (example.component.ts):
                                
                                    
  import { Component } from '@angular/core';
  import { DataService } from './data.service';

  @Component({
    selector: 'app-example',
    template: '<ul><li *ngFor="let item of data">{{ item }}</li></ul>'
  })
  export class ExampleComponent {
    data: string[];

    constructor(private dataService: DataService) {
      this.data = this.dataService.getData();
    }
  }

                                
                            
Unit Test with Mock (example.component.spec.ts):
                                
                                    
  import { ComponentFixture, TestBed } from '@angular/core/testing';
  import { ExampleComponent } from './example.component';
  import { DataService } from './data.service';

  class MockDataService {
    getData() {
      return ['Mock Data 1', 'Mock Data 2', 'Mock Data 3'];
    }
  }

  describe('ExampleComponent', () => {
    let component: ExampleComponent;
    let fixture: ComponentFixture<ExampleComponent>;
    let dataService: DataService;

    beforeEach(async () => {
      await TestBed.configureTestingModule({
        declarations: [ExampleComponent],
        providers: [{ provide: DataService, useClass: MockDataService }]
      }).compileComponents();
    });

    beforeEach(() => {
      fixture = TestBed.createComponent(ExampleComponent);
      component = fixture.componentInstance;
      dataService = TestBed.inject(DataService);
      fixture.detectChanges();
    });

    it('should create', () => {
      expect(component).toBeTruthy();
    });

    it('should get data from the service', () => {
      expect(component.data).toEqual(['Mock Data 1', 'Mock Data 2', 'Mock Data 3']);
    });
  });

                                
                            

Best Practices for Unit Testing in Angular

  • Write Tests Alongside Code: Develop tests simultaneously with the code to ensure complete coverage.
  • Isolate Tests: Ensure each test is independent and does not rely on the state or execution of other tests.
  • Use Mocks and Spies: Mock dependencies to isolate the unit under test.
  • Avoid Testing Implementation Details: Focus on testing the behavior rather than the internal implementation.
  • Keep Tests Fast: Ensure tests run quickly to facilitate continuous integration and rapid feedback.

Conclusion

Unit testing is a critical practice in Angular development that helps maintain high code quality, catch bugs early, and ensure your application behaves as expected. By setting up a robust testing environment, writing comprehensive tests, and following best practices, you can significantly improve the reliability and maintainability of your Angular applications. Whether you choose to use Jasmine and Karma, Jest, or Mocha and Chai, unit testing should be an integral part of your development workflow.

Let's develop your ideas into reality