1. Introduction to Test-Driven Development (TDD)
Test-Driven Development (TDD) is a software development process where you write tests before writing the actual code. This approach helps ensure your code is reliable, maintainable, and bug-free.
The TDD cycle consists of writing a failing test, writing code to pass the test, and then refactoring the code while keeping the tests green.
2. Why Use TDD in Angular?
Angular is a powerful framework for building scalable web applications. Using TDD with Angular helps you:
-
Catch bugs early in the development process
-
Write modular, testable code
-
Improve code quality and maintainability
-
Facilitate refactoring with confidence
-
Document your code behavior through tests
3. Setting Up Your Angular Environment for TDD
To start TDD in Angular, you need to set up your development environment with the Angular CLI and testing tools.
-
Install Angular CLI:
Run
npm install -g @angular/cli
in your terminal.
-
Create a new Angular project:
ng new tdd-angular-app --routing --style=scss
-
Navigate to your project folder:
cd tdd-angular-app
-
Run the development server:
ng serve
-
Run tests:
ng test
to launch Karma test runner.
4. Writing Your First Test in Angular
Angular uses Jasmine and Karma by default for unit testing. Let's write a simple test for a component.
Step 1: Generate a new component
ng generate component hello-world
Step 2: Write a test in
hello-world.component.spec.ts
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { HelloWorldComponent } from './hello-world.component';
describe('HelloWorldComponent', () => {
let component: HelloWorldComponent;
let fixture: ComponentFixture<HelloWorldComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ HelloWorldComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(HelloWorldComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create the component', () => {
expect(component).toBeTruthy();
});
it('should render "Hello World!" in a h1 tag', () => {
const compiled = fixture.nativeElement as HTMLElement;
expect(compiled.querySelector('h1')?.textContent).toContain('Hello World!');
});
});
Step 3: Update the component template
hello-world.component.html
<h1>Hello World!</h1>
Run
ng test
to see your tests pass.
5. The Red-Green-Refactor Cycle Explained
The core of TDD is the Red-Green-Refactor cycle:
-
Red:
Write a failing test that defines a desired improvement or new function.
-
Green:
Write the minimum amount of code to make the test pass.
-
Refactor:
Clean up the new code, ensuring tests still pass.
6. Testing Angular Components Step-by-Step
Components are the building blocks of Angular apps. Testing them ensures UI behaves as expected.
Example: Counter Component
// counter.component.ts
import { Component } from '@angular/core';
@Component({
selector: 'app-counter',
template: \`
<button (click)="decrement()">-</button>
<span>{{ count }}</span>
<button (click)="increment()">+</button>
\`
})
export class CounterComponent {
count = 0;
increment() {
this.count++;
}
decrement() {
this.count--;
}
}
// counter.component.spec.ts
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { CounterComponent } from './counter.component';
import { By } from '@angular/platform-browser';
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 start count at 0', () => {
expect(component.count).toBe(0);
});
it('should increment count when increment() is called', () => {
component.increment();
expect(component.count).toBe(1);
});
it('should decrement count when decrement() is called', () => {
component.decrement();
expect(component.count).toBe(-1);
});
it('should update the displayed count when buttons are clicked', () => {
const incrementBtn = fixture.debugElement.query(By.css('button:last-child'));
const decrementBtn = fixture.debugElement.query(By.css('button:first-child'));
const countSpan = fixture.debugElement.query(By.css('span')).nativeElement;
incrementBtn.triggerEventHandler('click', null);
fixture.detectChanges();
expect(countSpan.textContent).toBe('1');
decrementBtn.triggerEventHandler('click', null);
fixture.detectChanges();
expect(countSpan.textContent).toBe('0');
});
});
7. Testing Angular Services with TDD
Services contain business logic and data access. Testing them ensures your app behaves correctly.
Example: Simple Calculator Service
// calculator.service.ts
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class CalculatorService {
add(a: number, b: number): number {
return a + b;
}
subtract(a: number, b: number): number {
return a - b;
}
}
// calculator.service.spec.ts
import { TestBed } from '@angular/core/testing';
import { CalculatorService } from './calculator.service';
describe('CalculatorService', () => {
let service: CalculatorService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(CalculatorService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
it('should add two numbers correctly', () => {
expect(service.add(2, 3)).toBe(5);
});
it('should subtract two numbers correctly', () => {
expect(service.subtract(5, 3)).toBe(2);
});
});
8. Mocking Dependencies and Using Spies
When testing components or services that depend on other services, you should mock those dependencies to isolate tests.
Example: Mocking a DataService in a component test
// data.service.ts
export class DataService {
fetchData() {
return 'real data';
}
}
// component that uses DataService
import { Component } from '@angular/core';
import { DataService } from './data.service';
@Component({
selector: 'app-data',
template: '<p>{{ data }}</p>'
})
export class DataComponent {
data = '';
constructor(private dataService: DataService) {}
ngOnInit() {
this.data = this.dataService.fetchData();
}
}
// data.component.spec.ts
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { DataComponent } from './data.component';
import { DataService } from './data.service';
describe('DataComponent', () => {
let component: DataComponent;
let fixture: ComponentFixture<DataComponent>;
let mockDataService: jasmine.SpyObj<DataService>;
beforeEach(async () => {
const spy = jasmine.createSpyObj('DataService', ['fetchData']);
await TestBed.configureTestingModule({
declarations: [ DataComponent ],
providers: [
{ provide: DataService, useValue: spy }
]
}).compileComponents();
mockDataService = TestBed.inject(DataService) as jasmine.SpyObj;
});
beforeEach(() => {
fixture = TestBed.createComponent(DataComponent);
component = fixture.componentInstance;
});
it('should display mocked data', () => {
mockDataService.fetchData.and.returnValue('mocked data');
fixture.detectChanges();
const compiled = fixture.nativeElement as HTMLElement;
expect(compiled.querySelector('p')?.textContent).toBe('mocked data');
});
});
9. End-to-End Testing with Protractor and Cypress
End-to-end (E2E) tests simulate real user interactions. Angular CLI includes Protractor by default, but Cypress is a popular alternative.
Example: Simple E2E test with Cypress
// cypress/integration/sample_spec.js
describe('Angular App', () => {
it('should display welcome message', () => {
cy.visit('http://localhost:4200');
cy.contains('Welcome');
});
});
To run Cypress:
npm install cypress --save-dev
npx cypress open
10. Best Practices for TDD in Angular
-
Write small, focused tests that cover one behavior at a time.
-
Keep tests fast to encourage frequent runs.
-
Use descriptive test names to document expected behavior.
-
Mock external dependencies to isolate units under test.
-
Run tests automatically on every code change using watch mode.
-
Refactor tests regularly to remove duplication and improve clarity.
-
Integrate tests into your CI/CD pipeline for continuous quality assurance.
11. Additional Resources & Source Code Channels
To deepen your understanding and get hands-on practice, explore these resources:
Sample Source Code Repository
You can find a complete Angular TDD sample project here:
View on GitHub