Unit Testing in Angular: Best Practices and Tools

Unit testing is an essential part of Angular development as it helps ensure that individual units of code are working correctly. In this tutorial, we will explore the best practices and tools for unit testing in Angular.

unit testing angular best practices tools

Introduction

Unit testing involves testing individual components, services, pipes, directives, and forms in isolation to verify their functionality. By testing each unit separately, developers can catch bugs early on and ensure the overall quality of the application.

Setting up a unit testing environment is the first step in Angular unit testing. We will begin by installing Karma and Jasmine, two popular testing frameworks for Angular.

Installing Karma and Jasmine

To install Karma and Jasmine, run the following commands in your Angular project directory:

npm install karma --save-dev
npm install karma-jasmine jasmine-core --save-dev

Next, create a configuration file for Karma by running:

npx karma init

This will generate a karma.conf.js file where you can configure Karma for your Angular project.

Writing the first unit test

Now that we have Karma and Jasmine installed, let's write our first unit test. Create a new file app.component.spec.ts and add the following code:

import { TestBed } from '@angular/core/testing';
import { AppComponent } from './app.component';

describe('AppComponent', () => {
  beforeEach(async () => {
    await TestBed.configureTestingModule({
      declarations: [
        AppComponent
      ],
    }).compileComponents();
  });

  it('should create the app', () => {
    const fixture = TestBed.createComponent(AppComponent);
    const app = fixture.componentInstance;
    expect(app).toBeTruthy();
  });
});

In this example, we import the TestBed module from @angular/core/testing to configure and create a testing module for the AppComponent. We then use the createComponent method to create an instance of the AppComponent and assert that it exists.

Best Practices for Unit Testing

To ensure effective unit testing in Angular, it is important to follow some best practices. Let's explore a few of them.

Isolating components for testing

When testing components, it is important to isolate them from other dependencies and external services. The TestBed module provides a convenient way to configure and create a testing module for components.

import { ComponentFixture, TestBed } from '@angular/core/testing';
import { MyComponent } from './my.component';

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

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

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

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

In this example, we use the ComponentFixture to create a fixture for the MyComponent. We then access the component instance using the componentInstance property of the fixture.

Mocking dependencies with Jasmine spies

In some cases, components may have dependencies on external services or modules. To isolate the component during testing, we can use Jasmine spies to mock these dependencies.

import { TestBed } from '@angular/core/testing';
import { MyComponent } from './my.component';
import { MyService } from './my.service';

describe('MyComponent', () => {
  let component: MyComponent;
  let myService: MyService;

  beforeEach(async () => {
    await TestBed.configureTestingModule({
      declarations: [MyComponent],
      providers: [{
        provide: MyService,
        useValue: jasmine.createSpyObj('MyService', ['getData'])
      }]
    }).compileComponents();

    myService = TestBed.inject(MyService);
    component = TestBed.createComponent(MyComponent).componentInstance;
  });

  it('should call getData method on MyService', () => {
    component.getData();
    expect(myService.getData).toHaveBeenCalled();
  });
});

In this example, we use the provide property of the TestBed.configureTestingModule method to provide a mocked version of the MyService using Jasmine's createSpyObj function.

Testing asynchronous code

Angular applications often involve asynchronous operations such as HTTP requests or timers. To test asynchronous code, we can use the async and fakeAsync functions provided by Angular's testing utilities.

import { ComponentFixture, TestBed, fakeAsync, tick } from '@angular/core/testing';
import { MyComponent } from './my.component';
import { MyService } from './my.service';

describe('MyComponent', () => {
  let component: MyComponent;
  let fixture: ComponentFixture<MyComponent>;
  let myService: MyService;

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

    fixture = TestBed.createComponent(MyComponent);
    component = fixture.componentInstance;
    myService = TestBed.inject(MyService);
  });

  it('should update data after async operation', fakeAsync(() => {
    const testData = 'Test Data';
    spyOn(myService, 'getData').and.returnValue(Promise.resolve(testData));

    component.getData();
    tick();

    expect(component.data).toEqual(testData);
  }));
});

In this example, we use the fakeAsync function to wrap our test case and simulate asynchronous behavior. We use tick to advance the virtual clock and wait for the asynchronous operation to complete.

Using code coverage tools

Code coverage tools help measure how much of your code is being tested. Angular provides built-in support for code coverage using the ng test --code-coverage command. This generates a coverage report that can be viewed in the browser.

Testing Angular Services

Angular services play a crucial role in application development. Let's explore how to create and test services in Angular.

Creating and testing services

To create a service, run the following command:

ng generate service my-service

This will generate a my-service.service.ts file that contains the service implementation. To test the service, create a corresponding my-service.service.spec.ts file and write your tests.

import { TestBed } from '@angular/core/testing';
import { MyService } from './my-service.service';

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

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

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

In this example, we use the TestBed.configureTestingModule method to configure the testing module for the service. We then use the inject method to retrieve an instance of the service.

Mocking HTTP requests

Services often make HTTP requests to fetch data from remote servers. To isolate the service during testing, we can mock the HTTP requests using the HttpClientTestingModule module.

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

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

  beforeEach(() => {
    TestBed.configureTestingModule({
      imports: [HttpClientTestingModule],
      providers: [MyService]
    });
    service = TestBed.inject(MyService);
    httpTestingController = TestBed.inject(HttpTestingController);
  });

  afterEach(() => {
    httpTestingController.verify();
  });

  it('should fetch data from the server', fakeAsync(() => {
    const testData = 'Test Data';

    service.getData().subscribe((data) => {
      expect(data).toEqual(testData);
    });

    const req = httpTestingController.expectOne('http://example.com/api/data');
    expect(req.request.method).toEqual('GET');
    req.flush(testData);

    tick();
  }));
});

In this example, we import the HttpClientTestingModule module and provide it in the testing module. We then use the HttpTestingController to intercept and mock HTTP requests. We expect a single request to http://example.com/api/data with the GET method and flush a mock response.

Testing Angular Pipes

Angular pipes are used to transform data in templates. Let's explore how to create and test pipes in Angular.

Creating and testing custom pipes

To create a custom pipe, run the following command:

ng generate pipe my-pipe

This will generate a my-pipe.pipe.ts file that contains the pipe implementation. To test the pipe, create a corresponding my-pipe.pipe.spec.ts file and write your tests.

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

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

  beforeEach(() => {
    pipe = new MyPipe();
  });

  it('should transform input', () => {
    const input = 'Test Input';
    const transformed = pipe.transform(input);

    expect(transformed).toEqual('Transformed Test Input');
  });
});

In this example, we create an instance of the pipe and call the transform method with an input value. We then assert that the transformed output matches the expected value.

Testing built-in pipes

Angular provides a set of built-in pipes that can be used to format data. To test these pipes, we can leverage the same approach as testing custom pipes.

import { UpperCasePipe } from '@angular/common';

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

  beforeEach(() => {
    pipe = new UpperCasePipe();
  });

  it('should transform input to uppercase', () => {
    const input = 'test input';
    const transformed = pipe.transform(input);

    expect(transformed).toEqual('TEST INPUT');
  });
});

In this example, we import the UpperCasePipe from @angular/common and create an instance of the pipe. We then call the transform method with an input value and assert that the transformed output matches the expected value.

Testing Angular Directives

Angular directives are used to manipulate the DOM. Let's explore how to create and test directives in Angular.

Creating and testing custom directives

To create a custom directive, run the following command:

ng generate directive my-directive

This will generate a my-directive.directive.ts file that contains the directive implementation. To test the directive, create a corresponding my-directive.directive.spec.ts file and write your tests.

import { MyDirective } from './my-directive.directive';

describe('MyDirective', () => {
  let directive: MyDirective;

  beforeEach(() => {
    directive = new MyDirective();
  });

  it('should apply the directive', () => {
    // TODO: Write your test case here
  });
});

In this example, we create an instance of the directive and write our test case. You can add additional logic to the test case based on your specific directive implementation.

Testing built-in directives

Angular provides a set of built-in directives that can be used to manipulate the DOM. To test these directives, we can leverage the same approach as testing custom directives.

import { NgIf } from '@angular/common';

describe('NgIf', () => {
  let directive: NgIf;

  beforeEach(() => {
    directive = new NgIf(null, null, null);
  });

  it('should apply the directive', () => {
    // TODO: Write your test case here
  });
});

In this example, we import the NgIf directive from @angular/common and create an instance of the directive. We then write our test case based on the expected behavior of the directive.

Testing Angular Forms

Angular forms play a crucial role in user input handling. Let's explore how to test form validation and submission.

Testing form validation

To test form validation, we can create an instance of the form and set its values. We can then access the form controls and check their validity.

import { TestBed } from '@angular/core/testing';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { MyFormComponent } from './my-form.component';

describe('MyFormComponent', () => {
  let component: MyFormComponent;

  beforeEach(async () => {
    await TestBed.configureTestingModule({
      imports: [FormsModule, ReactiveFormsModule],
      declarations: [MyFormComponent]
    }).compileComponents();

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

  it('should validate form fields', () => {
    const form = component.myForm;
    const usernameControl = form.controls.username;

    usernameControl.setValue('');
    expect(usernameControl.valid).toBeFalsy();

    usernameControl.setValue('test');
    expect(usernameControl.valid).toBeTruthy();
  });
});

In this example, we import the FormsModule and ReactiveFormsModule to enable form handling in our tests. We create an instance of the MyFormComponent and access the form controls using the controls property of the form.

Testing form submission

To test form submission, we can create an instance of the form and simulate user interactions. We can then trigger the form submission and assert the expected outcome.

import { TestBed, fakeAsync, tick } from '@angular/core/testing';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { MyFormComponent } from './my-form.component';

describe('MyFormComponent', () => {
  let component: MyFormComponent;

  beforeEach(async () => {
    await TestBed.configureTestingModule({
      imports: [FormsModule, ReactiveFormsModule],
      declarations: [MyFormComponent]
    }).compileComponents();

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

  it('should submit the form', fakeAsync(() => {
    spyOn(component, 'onSubmit');

    const form = component.myForm;
    const submitButton = fixture.nativeElement.querySelector('button[type="submit"]');

    form.controls.username.setValue('test');
    submitButton.click();
    tick();

    expect(component.onSubmit).toHaveBeenCalled();
  }));
});

In this example, we use Jasmine's spyOn function to spy on the onSubmit method of the component. We access the form controls and simulate user interactions by setting the value of the username control and clicking the submit button. We then use tick to wait for any asynchronous operations to complete before asserting that the onSubmit method has been called.

Conclusion

In this tutorial, we have explored the best practices and tools for unit testing in Angular. We have learned how to set up a unit testing environment, write unit tests for components, services, pipes, directives, and forms, and follow best practices for effective unit testing. By following these practices and using the provided tools, developers can ensure the quality and reliability of their Angular applications.