Skip to main content

Angular Testing Introduction

Testing is a crucial aspect of developing robust Angular applications. It helps you ensure your code works as expected, prevents regressions when making changes, and improves the overall quality of your application. In this introduction, we'll explore the basics of Angular testing and why it's essential for every Angular developer.

Why Test Angular Applications?

Testing provides numerous benefits for Angular developers:

  • Early bug detection: Find and fix issues before they reach production
  • Improved code quality: Testing encourages better design patterns and modular code
  • Regression prevention: Ensures new changes don't break existing functionality
  • Documentation: Tests serve as documentation for how your components should behave
  • Refactoring confidence: Make changes with confidence, knowing tests will catch mistakes

Types of Tests in Angular

Angular applications typically involve three main types of tests:

1. Unit Tests

Unit tests focus on testing individual components, services, pipes, and directives in isolation. These are the most common type of tests in Angular applications.

2. Integration Tests

Integration tests verify that different parts of your application work together correctly, such as components interacting with services or other components.

3. End-to-End (E2E) Tests

E2E tests simulate user interactions with your application, testing the complete flow from the user interface through all layers of your application.

Angular Testing Tools

Angular comes with a robust testing framework built-in. The key tools you'll use include:

Jasmine

Jasmine is the default testing framework used with Angular. It provides functions to structure your tests in a behavior-driven development (BDD) style:

typescript
describe('Component: MyComponent', () => {
it('should create the component', () => {
expect(component).toBeTruthy();
});

it('should have title "My First Component"', () => {
expect(component.title).toEqual('My First Component');
});
});

Karma

Karma is a test runner that executes your tests in various browsers. It's configured in the karma.conf.js file in your Angular project.

TestBed

TestBed is Angular's utility for creating testing modules and components. It allows you to configure a testing module that emulates an Angular @NgModule.

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

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

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

Protractor / Cypress

These tools are used for end-to-end testing. While Protractor has been the default E2E testing tool for Angular, Cypress is increasingly popular and provides a more modern approach to E2E testing.

Writing Your First Unit Test

Let's write a simple unit test for an Angular component:

Component to Test

typescript
// hello.component.ts
import { Component } from '@angular/core';

@Component({
selector: 'app-hello',
template: '<h1>Hello, {{name}}!</h1>'
})
export class HelloComponent {
name = 'World';

setName(name: string): void {
this.name = name;
}
}

Test File

typescript
// hello.component.spec.ts
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { HelloComponent } from './hello.component';

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

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

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

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

it('should have default name as "World"', () => {
expect(component.name).toEqual('World');
});

it('should update name when setName is called', () => {
component.setName('Angular');
expect(component.name).toEqual('Angular');
});

it('should display the correct greeting', () => {
component.setName('Developer');
fixture.detectChanges();
const compiled = fixture.nativeElement;
expect(compiled.querySelector('h1').textContent).toContain('Hello, Developer!');
});
});

Anatomy of a Test

Let's break down the key elements of an Angular test:

1. Test Suite

A test suite is created with describe() and contains a collection of related test cases.

typescript
describe('ComponentName', () => {
// Test cases go here
});

2. Test Cases

Individual test cases are defined with it() or test() functions:

typescript
it('should do something specific', () => {
// Test logic goes here
});

3. Setup and Teardown

The beforeEach() and afterEach() functions handle setup and teardown operations for each test:

typescript
beforeEach(() => {
// Setup code runs before each test
});

afterEach(() => {
// Cleanup code runs after each test
});

4. Assertions

Assertions verify that your code behaves as expected using Jasmine's expect function:

typescript
expect(actualValue).toEqual(expectedValue);
expect(something).toBeTruthy();
expect(array).toContain(item);

Testing Angular Services

Services often contain business logic and API interactions, making them important to test:

typescript
// data.service.ts
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';

@Injectable({
providedIn: 'root'
})
export class DataService {
private apiUrl = 'https://api.example.com/data';

constructor(private http: HttpClient) { }

getData(): Observable<any[]> {
return this.http.get<any[]>(this.apiUrl);
}
}
typescript
// data.service.spec.ts
import { TestBed } from '@angular/core/testing';
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { DataService } from './data.service';

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

beforeEach(() => {
TestBed.configureTestingModule({
imports: [HttpClientTestingModule],
providers: [DataService]
});

service = TestBed.inject(DataService);
httpMock = TestBed.inject(HttpTestingController);
});

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

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

it('should retrieve data from the API', () => {
const dummyData = [{ id: 1, name: 'Item 1' }, { id: 2, name: 'Item 2' }];

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

const req = httpMock.expectOne('https://api.example.com/data');
expect(req.request.method).toBe('GET');
req.flush(dummyData);
});
});

Running Tests in Angular

To run your tests in an Angular project:

  1. Use the Angular CLI command:

    bash
    ng test
  2. This will build your application, start Karma, and run all the tests found in files with the .spec.ts extension.

  3. Test results will appear in your console and browser window, showing which tests passed and failed.

Best Practices for Angular Testing

  1. Keep tests simple and focused: Each test should verify a single piece of functionality.

  2. Test behavior, not implementation: Focus on what your components do, not how they do it.

  3. Avoid testing private methods: Test the public API of your components and services.

  4. Mock external dependencies: Use TestBed to provide mock versions of services and other dependencies.

  5. Use descriptive test names: Your test names should clearly describe what they're testing.

  6. Organize tests to match your application structure: Keep test files alongside the files they test.

  7. Write testable code: Design your components and services with testing in mind.

Real-World Testing Example: Shopping Cart Component

Let's look at a more complex example - testing a shopping cart component:

Component Code

typescript
// shopping-cart.component.ts
import { Component } from '@angular/core';
import { CartService } from '../services/cart.service';
import { Product } from '../models/product.model';

@Component({
selector: 'app-shopping-cart',
template: `
<div class="cart">
<h2>Your Shopping Cart</h2>
<div *ngIf="items.length === 0" class="empty-cart">Your cart is empty</div>
<ul *ngIf="items.length > 0">
<li *ngFor="let item of items">
{{ item.name }} - ${{ item.price }}
<button (click)="removeItem(item)">Remove</button>
</li>
</ul>
<div class="total">Total: ${{ calculateTotal() }}</div>
<button [disabled]="items.length === 0" (click)="checkout()">Checkout</button>
</div>
`
})
export class ShoppingCartComponent {
items: Product[] = [];

constructor(private cartService: CartService) {
this.items = this.cartService.getItems();
}

removeItem(product: Product): void {
this.cartService.removeItem(product);
this.items = this.cartService.getItems();
}

calculateTotal(): number {
return this.items.reduce((sum, item) => sum + item.price, 0);
}

checkout(): void {
this.cartService.checkout();
this.items = [];
}
}

Test Code

typescript
// shopping-cart.component.spec.ts
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { ShoppingCartComponent } from './shopping-cart.component';
import { CartService } from '../services/cart.service';
import { Product } from '../models/product.model';

describe('ShoppingCartComponent', () => {
let component: ShoppingCartComponent;
let fixture: ComponentFixture<ShoppingCartComponent>;
let cartService: jasmine.SpyObj<CartService>;

const mockProducts: Product[] = [
{ id: 1, name: 'Product 1', price: 10 },
{ id: 2, name: 'Product 2', price: 15 }
];

beforeEach(async () => {
// Create a spy object for the CartService
const cartServiceSpy = jasmine.createSpyObj('CartService', ['getItems', 'removeItem', 'checkout']);

await TestBed.configureTestingModule({
declarations: [ShoppingCartComponent],
providers: [
{ provide: CartService, useValue: cartServiceSpy }
]
}).compileComponents();

cartService = TestBed.inject(CartService) as jasmine.SpyObj<CartService>;

// Set up the spy to return mock data
cartService.getItems.and.returnValue(mockProducts);
});

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

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

it('should display items from the cart service', () => {
const items = fixture.debugElement.queryAll(By.css('li'));
expect(items.length).toBe(2);
expect(items[0].nativeElement.textContent).toContain('Product 1');
expect(items[1].nativeElement.textContent).toContain('Product 2');
});

it('should calculate the correct total', () => {
expect(component.calculateTotal()).toBe(25);
const totalElement = fixture.debugElement.query(By.css('.total'));
expect(totalElement.nativeElement.textContent).toContain('Total: $25');
});

it('should remove an item when Remove button is clicked', () => {
// Set up spy to update the items list when removeItem is called
cartService.getItems.and.returnValue([mockProducts[1]]);

// Find and click the first Remove button
const removeButtons = fixture.debugElement.queryAll(By.css('button'));
removeButtons[0].triggerEventHandler('click', null);

// Verify the service was called correctly
expect(cartService.removeItem).toHaveBeenCalledWith(mockProducts[0]);

// Update the view
fixture.detectChanges();

// Verify the UI was updated
const items = fixture.debugElement.queryAll(By.css('li'));
expect(items.length).toBe(1);
expect(items[0].nativeElement.textContent).toContain('Product 2');
});

it('should call checkout service when Checkout button is clicked', () => {
// Find and click the checkout button
const checkoutButton = fixture.debugElement.queryAll(By.css('button'))[2];
checkoutButton.triggerEventHandler('click', null);

// Verify the service was called
expect(cartService.checkout).toHaveBeenCalled();

// Verify the cart is cleared
expect(component.items.length).toBe(0);
});

it('should show empty cart message when there are no items', () => {
// Change the cart to be empty
cartService.getItems.and.returnValue([]);
component.items = [];
fixture.detectChanges();

// Check for the empty cart message
const emptyMessage = fixture.debugElement.query(By.css('.empty-cart'));
expect(emptyMessage).toBeTruthy();
expect(emptyMessage.nativeElement.textContent).toContain('Your cart is empty');

// Check that the checkout button is disabled
const checkoutButton = fixture.debugElement.query(By.css('button[disabled]'));
expect(checkoutButton).toBeTruthy();
});
});

Summary

In this introduction to Angular Testing, we've covered:

  1. The importance and benefits of testing Angular applications
  2. The three main types of tests: unit, integration, and end-to-end
  3. Essential testing tools in the Angular ecosystem
  4. How to write basic unit tests for components and services
  5. The structure and anatomy of Angular tests
  6. Best practices for effective Angular testing
  7. A real-world example of testing a shopping cart component

Testing is a valuable skill for any Angular developer, and mastering it will help you build more reliable and maintainable applications. As you become more comfortable with the basics, you can explore more advanced testing techniques like mocking complex dependencies, testing asynchronous code, and implementing comprehensive test coverage.

Additional Resources

Practice Exercises

  1. Create a simple counter component with increment and decrement buttons, then write tests for its functionality.
  2. Write tests for an authentication service that handles user login and logout.
  3. Test a form component that validates user input and displays error messages.
  4. Practice mocking HTTP requests by testing a component that fetches and displays data from an API.
  5. Write E2E tests for a multi-step form using Cypress or Protractor.


If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)