I'll be honest—when I first started with Angular, I skipped testing. I thought it was too time-consuming, and my code worked fine without it. Then I had to refactor a large feature, and I broke three things I didn't even know existed. That's when I learned the value of good tests. They give you confidence to refactor, catch bugs before they reach production, and serve as documentation for how your code should work.
Angular provides excellent testing support out of the box with Jasmine (the testing framework) and Karma (the test runner). TestBed makes it easy to configure testing modules, mock dependencies, and test components in isolation. The testing utilities are powerful enough to test complex scenarios while being simple enough to write tests quickly.
📖 Want the complete guide with more examples and advanced patterns? Check out the full article on my blog for an in-depth tutorial with additional code examples, troubleshooting tips, and real-world use cases.
What is Angular Testing?
Angular Testing provides:
- Jasmine - Behavior-driven testing framework
- Karma - Test runner for running tests in browsers
- TestBed - Testing utilities for configuring test modules
- Component Testing - Test components in isolation
- Service Testing - Test business logic and HTTP calls
- Pipe & Directive Testing - Test custom transformations
- Async Testing - Test asynchronous operations
- Mocking - Mock dependencies with spies
Component Testing
Test Angular components with TestBed:
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { BusinessListComponent } from './business-list.component';
import { BusinessService } from 'src/services/business.service';
import { of } from 'rxjs';
import { ReactiveFormsModule } from '@angular/forms';
import { HttpClientTestingModule } from '@angular/common/http/testing';
describe('BusinessListComponent', () => {
let component: BusinessListComponent;
let fixture: ComponentFixture<BusinessListComponent>;
let businessService: jasmine.SpyObj<BusinessService>;
beforeEach(async () => {
const spy = jasmine.createSpyObj('BusinessService', ['GetBusinesses']);
await TestBed.configureTestingModule({
declarations: [BusinessListComponent],
providers: [
{ provide: BusinessService, useValue: spy }
],
imports: [ReactiveFormsModule, HttpClientTestingModule]
}).compileComponents();
fixture = TestBed.createComponent(BusinessListComponent);
component = fixture.componentInstance;
businessService = TestBed.inject(BusinessService) as jasmine.SpyObj<BusinessService>;
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should load businesses on init', () => {
const mockBusinesses = [{ id: 1, name: 'Business 1' }];
businessService.GetBusinesses.and.returnValue(of({ data: mockBusinesses }));
fixture.detectChanges();
expect(businessService.GetBusinesses).toHaveBeenCalled();
expect(component.businesses).toEqual(mockBusinesses);
});
it('should display businesses in template', () => {
component.businesses = [{ id: 1, name: 'Business 1' }];
fixture.detectChanges();
const compiled = fixture.nativeElement;
expect(compiled.querySelector('.business-name').textContent).toContain('Business 1');
});
});
Key Points:
- Use
TestBed.configureTestingModule()to configure test module - Mock dependencies with
jasmine.createSpyObj() - Use
fixture.detectChanges()to trigger change detection - Query DOM with
fixture.nativeElementorfixture.debugElement
Testing Component Inputs and Outputs
describe('BusinessCardComponent', () => {
let component: BusinessCardComponent;
let fixture: ComponentFixture<BusinessCardComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [BusinessCardComponent]
}).compileComponents();
fixture = TestBed.createComponent(BusinessCardComponent);
component = fixture.componentInstance;
});
it('should display business name', () => {
component.business = { id: 1, name: 'Test Business' };
fixture.detectChanges();
const compiled = fixture.nativeElement;
expect(compiled.textContent).toContain('Test Business');
});
it('should emit event on click', () => {
spyOn(component.businessSelected, 'emit');
component.business = { id: 1, name: 'Test Business' };
fixture.detectChanges();
const button = fixture.nativeElement.querySelector('button');
button.click();
expect(component.businessSelected.emit).toHaveBeenCalledWith(component.business);
});
});
Testing Component Forms
describe('BusinessFormComponent', () => {
let component: BusinessFormComponent;
let fixture: ComponentFixture<BusinessFormComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [BusinessFormComponent],
imports: [ReactiveFormsModule]
}).compileComponents();
fixture = TestBed.createComponent(BusinessFormComponent);
component = fixture.componentInstance;
});
it('should validate required fields', () => {
const form = component.businessForm;
expect(form.valid).toBeFalsy();
form.patchValue({ name: 'Test Business' });
expect(form.valid).toBeTruthy();
});
it('should submit form with valid data', () => {
spyOn(component.formSubmit, 'emit');
component.businessForm.patchValue({
name: 'Test Business',
email: 'test@example.com'
});
component.onSubmit();
expect(component.formSubmit.emit).toHaveBeenCalled();
});
});
Service Testing
Test Angular services:
import { TestBed } from '@angular/core/testing';
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { BusinessService } from './business.service';
import { environment } from 'src/environments/environment';
describe('BusinessService', () => {
let service: BusinessService;
let httpMock: HttpTestingController;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [HttpClientTestingModule],
providers: [BusinessService]
});
service = TestBed.inject(BusinessService);
httpMock = TestBed.inject(HttpTestingController);
});
afterEach(() => {
httpMock.verify(); // Verify no outstanding requests
});
it('should fetch businesses', () => {
const mockBusinesses = [{ id: 1, name: 'Business 1' }];
service.GetBusinesses({}).subscribe(businesses => {
expect(businesses.data).toEqual(mockBusinesses);
});
const req = httpMock.expectOne(`${environment.ApiUrl}business/get`);
expect(req.request.method).toBe('POST');
req.flush({ data: mockBusinesses });
});
it('should handle errors', () => {
service.GetBusinesses({}).subscribe(
() => fail('should have failed'),
error => expect(error.status).toBe(500)
);
const req = httpMock.expectOne(`${environment.ApiUrl}business/get`);
req.flush('Error', { status: 500, statusText: 'Server Error' });
});
it('should create business', () => {
const newBusiness = { name: 'New Business' };
const createdBusiness = { id: 1, ...newBusiness };
service.SaveBusiness(newBusiness, 0).subscribe(business => {
expect(business).toEqual(createdBusiness);
});
const req = httpMock.expectOne(`${environment.ApiUrl}business/0/details`);
expect(req.request.method).toBe('POST');
expect(req.request.body).toEqual(newBusiness);
req.flush(createdBusiness);
});
});
Testing Services with Dependencies
describe('UserService', () => {
let service: UserService;
let httpMock: HttpTestingController;
let loggerService: jasmine.SpyObj<LoggerService>;
beforeEach(() => {
const loggerSpy = jasmine.createSpyObj('LoggerService', ['log', 'error']);
TestBed.configureTestingModule({
imports: [HttpClientTestingModule],
providers: [
UserService,
{ provide: LoggerService, useValue: loggerSpy }
]
});
service = TestBed.inject(UserService);
httpMock = TestBed.inject(HttpTestingController);
loggerService = TestBed.inject(LoggerService) as jasmine.SpyObj<LoggerService>;
});
it('should log errors', () => {
service.GetUser(1).subscribe(
() => fail('should have failed'),
error => {
expect(loggerService.error).toHaveBeenCalled();
}
);
const req = httpMock.expectOne('/api/users/1');
req.flush('Error', { status: 500, statusText: 'Server Error' });
});
});
Pipe Testing
Test custom pipes:
import { TruncatePipe } from './truncate.pipe';
describe('TruncatePipe', () => {
let pipe: TruncatePipe;
beforeEach(() => {
pipe = new TruncatePipe();
});
it('should create', () => {
expect(pipe).toBeTruthy();
});
it('should truncate long text', () => {
const result = pipe.transform('This is a very long text', 10);
expect(result).toBe('This is a ...');
});
it('should return original text if shorter than limit', () => {
const result = pipe.transform('Short', 10);
expect(result).toBe('Short');
});
it('should handle null values', () => {
const result = pipe.transform(null, 10);
expect(result).toBe('');
});
});
Testing Async Pipes
import { CurrencyPipe } from '@angular/common';
import { TestBed } from '@angular/core/testing';
describe('CurrencyPipe', () => {
let pipe: CurrencyPipe;
beforeEach(() => {
TestBed.configureTestingModule({
providers: [CurrencyPipe]
});
pipe = TestBed.inject(CurrencyPipe);
});
it('should format currency', () => {
const result = pipe.transform(1000, 'USD');
expect(result).toContain('1,000');
});
});
Directive Testing
Test custom directives:
import { Component, DebugElement } from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { HighlightDirective } from './highlight.directive';
@Component({
template: '<p appHighlight="yellow">Test</p>'
})
class TestComponent {}
describe('HighlightDirective', () => {
let fixture: ComponentFixture<TestComponent>;
let element: DebugElement;
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [TestComponent, HighlightDirective]
});
fixture = TestBed.createComponent(TestComponent);
element = fixture.debugElement.query(By.directive(HighlightDirective));
});
it('should create directive', () => {
expect(element).toBeTruthy();
});
it('should apply highlight on mouseenter', () => {
element.triggerEventHandler('mouseenter', null);
fixture.detectChanges();
expect(element.nativeElement.style.backgroundColor).toBe('yellow');
});
it('should remove highlight on mouseleave', () => {
element.triggerEventHandler('mouseenter', null);
fixture.detectChanges();
element.triggerEventHandler('mouseleave', null);
fixture.detectChanges();
expect(element.nativeElement.style.backgroundColor).toBe('');
});
});
Async Testing
Test asynchronous operations:
import { fakeAsync, tick, flush } from '@angular/core/testing';
import { of, delay } from 'rxjs';
describe('Async Operations', () => {
it('should handle async operations with fakeAsync', fakeAsync(() => {
let value = false;
setTimeout(() => {
value = true;
}, 1000);
expect(value).toBe(false);
tick(1000);
expect(value).toBe(true);
}));
it('should handle multiple timers', fakeAsync(() => {
let value = 0;
setTimeout(() => value++, 100);
setTimeout(() => value++, 200);
setTimeout(() => value++, 300);
expect(value).toBe(0);
tick(300);
expect(value).toBe(3);
}));
it('should flush all timers', fakeAsync(() => {
let value = false;
setTimeout(() => {
value = true;
}, 1000);
flush(); // Flush all pending timers
expect(value).toBe(true);
}));
it('should test observables', (done) => {
service.getData().subscribe(data => {
expect(data).toBeDefined();
done();
});
});
it('should test observables with fakeAsync', fakeAsync(() => {
let result: any;
service.getData().subscribe(data => {
result = data;
});
tick();
expect(result).toBeDefined();
}));
});
Testing RxJS Operators
import { of, throwError } from 'rxjs';
import { delay, catchError } from 'rxjs/operators';
describe('RxJS Testing', () => {
it('should test delayed observables', fakeAsync(() => {
let result: any;
of('data').pipe(delay(1000)).subscribe(data => {
result = data;
});
tick(1000);
expect(result).toBe('data');
}));
it('should test error handling', (done) => {
throwError(() => new Error('Test error'))
.pipe(
catchError(error => {
expect(error.message).toBe('Test error');
done();
return of(null);
})
)
.subscribe();
});
});
Testing Guards
Test route guards:
import { TestBed } from '@angular/core/testing';
import { Router } from '@angular/router';
import { AuthGuard } from './auth.guard';
import { AuthService } from './auth.service';
describe('AuthGuard', () => {
let guard: AuthGuard;
let authService: jasmine.SpyObj<AuthService>;
let router: jasmine.SpyObj<Router>;
beforeEach(() => {
const authSpy = jasmine.createSpyObj('AuthService', ['isAuthenticated']);
const routerSpy = jasmine.createSpyObj('Router', ['navigate']);
TestBed.configureTestingModule({
providers: [
AuthGuard,
{ provide: AuthService, useValue: authSpy },
{ provide: Router, useValue: routerSpy }
]
});
guard = TestBed.inject(AuthGuard);
authService = TestBed.inject(AuthService) as jasmine.SpyObj<AuthService>;
router = TestBed.inject(Router) as jasmine.SpyObj<Router>;
});
it('should allow access when authenticated', () => {
authService.isAuthenticated.and.returnValue(true);
expect(guard.canActivate()).toBe(true);
});
it('should redirect when not authenticated', () => {
authService.isAuthenticated.and.returnValue(false);
expect(guard.canActivate()).toBe(false);
expect(router.navigate).toHaveBeenCalledWith(['/login']);
});
});
Testing Interceptors
Test HTTP interceptors:
import { TestBed } from '@angular/core/testing';
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { HTTP_INTERCEPTORS, HttpClient } from '@angular/common/http';
import { AuthInterceptor } from './auth.interceptor';
describe('AuthInterceptor', () => {
let http: HttpClient;
let httpMock: HttpTestingController;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [HttpClientTestingModule],
providers: [
{
provide: HTTP_INTERCEPTORS,
useClass: AuthInterceptor,
multi: true
}
]
});
http = TestBed.inject(HttpClient);
httpMock = TestBed.inject(HttpTestingController);
});
it('should add authorization header', () => {
http.get('/api/data').subscribe();
const req = httpMock.expectOne('/api/data');
expect(req.request.headers.has('Authorization')).toBeTruthy();
expect(req.request.headers.get('Authorization')).toBe('Bearer token');
});
});
Testing Best Practices
- Write tests for each component, service, pipe, and directive
- Use TestBed - For component and service testing
- Mock dependencies - With Jasmine spies
- Use HttpClientTestingModule - For HTTP testing
- Test both success and error scenarios
- Use fakeAsync and tick - For async operations
- Query DOM elements - With debugElement
- Test component inputs, outputs, and events
- Maintain high test coverage - Aim for 80%+
- Keep tests isolated and independent
- Use descriptive test names - Clear what is being tested
- Clean up after each test - Use afterEach for cleanup
- Use beforeEach for setup - Common test configuration
- Test user interactions - Click, input, form submissions
- Test edge cases - Null values, empty arrays, errors
Common Testing Patterns
Test Setup Helper
export function createComponentFixture<T>(
component: Type<T>,
imports: any[] = [],
providers: any[] = []
): ComponentFixture<T> {
TestBed.configureTestingModule({
declarations: [component],
imports: [HttpClientTestingModule, ...imports],
providers
}).compileComponents();
return TestBed.createComponent(component);
}
Mock Factory
export function createMockService<T>(methods: string[]): jasmine.SpyObj<T> {
return jasmine.createSpyObj<T>(methods);
}
Test Data Builders
export class BusinessBuilder {
private business: any = {
id: 1,
name: 'Test Business',
isActive: true
};
withId(id: number): BusinessBuilder {
this.business.id = id;
return this;
}
withName(name: string): BusinessBuilder {
this.business.name = name;
return this;
}
inactive(): BusinessBuilder {
this.business.isActive = false;
return this;
}
build(): any {
return { ...this.business };
}
}
// Usage
const business = new BusinessBuilder()
.withId(1)
.withName('Test')
.build();
Karma Configuration
Configure Karma for your tests:
// karma.conf.js
module.exports = function(config) {
config.set({
basePath: '',
frameworks: ['jasmine', '@angular-devkit/build-angular'],
plugins: [
require('karma-jasmine'),
require('karma-chrome-launcher'),
require('karma-jasmine-html-reporter'),
require('karma-coverage'),
require('@angular-devkit/build-angular/plugins/karma')
],
client: {
clearContext: false
},
coverageReporter: {
dir: require('path').join(__dirname, './coverage'),
subdir: '.',
reporters: [
{ type: 'html' },
{ type: 'text-summary' }
]
},
reporters: ['progress', 'kjhtml'],
port: 9876,
colors: true,
logLevel: config.LOG_INFO,
autoWatch: true,
browsers: ['Chrome'],
singleRun: false,
restartOnFileChange: true
});
};
Resources and Further Reading
- 📚 Full Angular Testing Guide - Complete tutorial with advanced examples, troubleshooting, and best practices
- Angular Services Guide - Testing services with dependency injection
- Angular Reactive Forms Guide - Testing forms and validators
- Angular HTTP Client Guide - Testing HTTP calls and interceptors
- Angular Testing Documentation - Official Angular testing guide
- Jasmine Documentation - Jasmine testing framework
- Karma Documentation - Karma test runner
Conclusion
Testing is crucial for building reliable Angular applications. With Jasmine and Karma, you can write comprehensive tests for all parts of your application. Following these patterns ensures code quality and enables confident development.
Key Takeaways:
- Jasmine - Behavior-driven testing framework
- Karma - Test runner for browser-based testing
- TestBed - Configure test modules and create components
- Component Testing - Test components in isolation
- Service Testing - Test business logic and HTTP calls
- Pipe & Directive Testing - Test custom transformations
- Async Testing - Use fakeAsync and tick for async operations
- Mocking - Use Jasmine spies for dependencies
- Best Practices - High coverage, isolated tests, descriptive names
Whether you're building a simple dashboard or a complex enterprise application, comprehensive testing provides the confidence you need to refactor and deploy with peace of mind.
What's your experience with Angular Testing? Share your tips and tricks in the comments below! 🚀
💡 Looking for more details? This is a condensed version of my comprehensive guide. Read the full article on my blog for additional examples, advanced patterns, troubleshooting tips, and more in-depth explanations.
If you found this guide helpful, consider checking out my other articles on Angular development and frontend development best practices.
Top comments (0)