Component testing in Angular is crucial for ensuring that individual components function as expected. This involves testing the component's template, logic, and interaction with other components or services. In this section, we will cover the following:

  1. Introduction to Component Testing
  2. Setting Up the Testing Environment
  3. Writing Basic Component Tests
  4. Testing Component Inputs and Outputs
  5. Testing Component Templates
  6. Practical Exercises

  1. Introduction to Component Testing

Component testing focuses on verifying the behavior of individual components in isolation. This ensures that each component works correctly before integrating it into the larger application.

Key Concepts:

  • Isolation: Testing components independently from the rest of the application.
  • TestBed: Angular's primary API for configuring and initializing the environment for unit tests.
  • Fixtures: Used to create an instance of the component and access its properties and methods.

  1. Setting Up the Testing Environment

Before writing tests, ensure your Angular project is set up for testing. Angular CLI projects come pre-configured with Jasmine and Karma for testing.

Steps:

  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
    

  1. Writing Basic Component Tests

Let's start with a simple component and write basic tests for it.

Example Component:

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

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

Test File:

// src/app/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();
  });

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

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

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

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

Explanation:

  • TestBed: Configures the testing module.
  • ComponentFixture: Provides access to the component instance and its template.
  • beforeEach: Initializes the component and fixture before each test.
  • it: Defines individual test cases.

  1. Testing Component Inputs and Outputs

Components often interact with other components through inputs and outputs. Let's test these interactions.

Example Component with Input and Output:

// src/app/greet.component.ts
import { Component, Input, Output, EventEmitter } from '@angular/core';

@Component({
  selector: 'app-greet',
  template: `<button (click)="greet()">Greet</button>`
})
export class GreetComponent {
  @Input() name: string;
  @Output() greeted = new EventEmitter<string>();

  greet() {
    this.greeted.emit(`Hello, ${this.name}!`);
  }
}

Test File:

// src/app/greet.component.spec.ts
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { GreetComponent } from './greet.component';

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

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

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

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

  it('should emit greeted event with the correct message', () => {
    component.name = 'Angular';
    spyOn(component.greeted, 'emit');

    component.greet();

    expect(component.greeted.emit).toHaveBeenCalledWith('Hello, Angular!');
  });
});

Explanation:

  • @Input: Decorator to bind a property to an input.
  • @Output: Decorator to bind an event emitter to an output.
  • spyOn: Jasmine function to spy on a method and track its calls.

  1. Testing Component Templates

Testing the component's template involves verifying that the DOM updates correctly based on the component's state.

Example Component with Template:

// src/app/counter.component.ts
import { Component } from '@angular/core';

@Component({
  selector: 'app-counter',
  template: `
    <p>Count: {{count}}</p>
    <button (click)="increment()">Increment</button>
  `
})
export class CounterComponent {
  count: number = 0;

  increment() {
    this.count++;
  }
}

Test File:

// src/app/counter.component.spec.ts
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { CounterComponent } from './counter.component';

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

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

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

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

  it('should increment count on button click', () => {
    const compiled = fixture.nativeElement;
    const button = compiled.querySelector('button');
    button.click();
    fixture.detectChanges();
    expect(compiled.querySelector('p').textContent).toContain('Count: 1');
  });
});

Explanation:

  • fixture.nativeElement: Provides access to the component's DOM.
  • button.click(): Simulates a button click.
  • fixture.detectChanges(): Triggers change detection to update the DOM.

  1. Practical Exercises

Exercise 1: Testing a Toggle Component

Create a ToggleComponent that toggles a boolean value and displays its state.

Component:

// src/app/toggle.component.ts
import { Component } from '@angular/core';

@Component({
  selector: 'app-toggle',
  template: `
    <p>{{isOn ? 'On' : 'Off'}}</p>
    <button (click)="toggle()">Toggle</button>
  `
})
export class ToggleComponent {
  isOn: boolean = false;

  toggle() {
    this.isOn = !this.isOn;
  }
}

Test File:

// src/app/toggle.component.spec.ts
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ToggleComponent } from './toggle.component';

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

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

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

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

  it('should toggle isOn state on button click', () => {
    const compiled = fixture.nativeElement;
    const button = compiled.querySelector('button');
    button.click();
    fixture.detectChanges();
    expect(compiled.querySelector('p').textContent).toContain('On');
    button.click();
    fixture.detectChanges();
    expect(compiled.querySelector('p').textContent).toContain('Off');
  });
});

Exercise 2: Testing a List Component

Create a ListComponent that displays a list of items and allows adding new items.

Component:

// src/app/list.component.ts
import { Component } from '@angular/core';

@Component({
  selector: 'app-list',
  template: `
    <ul>
      <li *ngFor="let item of items">{{item}}</li>
    </ul>
    <input [(ngModel)]="newItem" />
    <button (click)="addItem()">Add Item</button>
  `
})
export class ListComponent {
  items: string[] = [];
  newItem: string = '';

  addItem() {
    if (this.newItem) {
      this.items.push(this.newItem);
      this.newItem = '';
    }
  }
}

Test File:

// src/app/list.component.spec.ts
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { FormsModule } from '@angular/forms';
import { ListComponent } from './list.component';

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

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

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

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

  it('should add new item to the list', () => {
    component.newItem = 'Test Item';
    const button = fixture.nativeElement.querySelector('button');
    button.click();
    fixture.detectChanges();
    expect(component.items).toContain('Test Item');
    expect(fixture.nativeElement.querySelector('ul').textContent).toContain('Test Item');
  });
});

Conclusion

In this section, we covered the basics of component testing in Angular, including setting up the testing environment, writing basic tests, and testing component inputs, outputs, and templates. We also provided practical exercises to reinforce the learned concepts. By mastering component testing, you ensure that your Angular components are reliable and maintainable, leading to a more robust application.

© Copyright 2024. All rights reserved