Use Angular's TestBed to configure testing modules, test components with ComponentFixture, mock services with jasmine spies, and test Angular forms and HTTP calls.
Angular's dependency injection system and module architecture make unit testing both powerful and opinionated. TestBed is the Angular-specific test harness that mirrors the real module bootstrap process in a lightweight, in-memory environment. Every Angular unit test uses TestBed to configure which declarations, imports, and providers are available — giving you precise control over what is real and what is mocked.
This lesson covers the patterns that make Angular testing productive: configuring TestBed correctly, working with ComponentFixture, using jasmine spies for service isolation, and testing the Angular-specific concerns of reactive forms and HTTP clients.
TestBed.configureTestingModule mirrors NgModule — declare the component under test, import only what it needs, and provide mock versions of its dependencies.
1// src/app/user-profile/user-profile.component.ts
2@Component({
3 selector: 'app-user-profile',
4 template: `
5 <div *ngIf="user">
6 <h1>{{ user.name }}</h1>
7 <p>{{ user.email }}</p>
8 <button (click)="onEdit()">Edit Profile</button>
9 </div>
10 <p *ngIf="!user">Loading...</p>
11 `
12})
13export class UserProfileComponent implements OnInit {
14 user: User | null = null;
15
16 constructor(private userService: UserService) {}
17
18 ngOnInit(): void {
19 this.userService.getCurrentUser().subscribe(u => this.user = u);
20 }
21
22 onEdit(): void {
23 // navigate to edit page
24 }
25}
26
27// src/app/user-profile/user-profile.component.spec.ts
28import { ComponentFixture, TestBed } from '@angular/core/testing';
29import { By } from '@angular/platform-browser';
30import { of } from 'rxjs';
31import { UserProfileComponent } from './user-profile.component';
32import { UserService } from '../services/user.service';
33
34describe('UserProfileComponent', () => {
35 let component: UserProfileComponent;
36 let fixture: ComponentFixture<UserProfileComponent>;
37 let mockUserService: jasmine.SpyObj<UserService>;
38
39 beforeEach(async () => {
40 // Create a spy object with all methods as spies
41 mockUserService = jasmine.createSpyObj<UserService>('UserService', ['getCurrentUser']);
42 mockUserService.getCurrentUser.and.returnValue(
43 of({ id: '1', name: 'Alice', email: '[email protected]' })
44 );
45
46 await TestBed.configureTestingModule({
47 declarations: [UserProfileComponent],
48 providers: [
49 { provide: UserService, useValue: mockUserService }
50 ]
51 }).compileComponents();
52
53 fixture = TestBed.createComponent(UserProfileComponent);
54 component = fixture.componentInstance;
55 fixture.detectChanges(); // triggers ngOnInit
56 });
57
58 it('should display the user name', () => {
59 const h1 = fixture.debugElement.query(By.css('h1'));
60 expect(h1.nativeElement.textContent).toContain('Alice');
61 });
62
63 it('should display the user email', () => {
64 expect(fixture.nativeElement.querySelector('p').textContent).toContain('[email protected]');
65 });
66
67 it('should show loading when user is null', () => {
68 mockUserService.getCurrentUser.and.returnValue(of(null as any));
69 component.user = null;
70 fixture.detectChanges();
71 expect(fixture.nativeElement.textContent).toContain('Loading...');
72 });
73
74 it('should call onEdit when Edit Profile button is clicked', () => {
75 spyOn(component, 'onEdit');
76 const button = fixture.debugElement.query(By.css('button'));
77 button.nativeElement.click();
78 expect(component.onEdit).toHaveBeenCalled();
79 });
80});jasmine.createSpyObj generates a mock object with all specified methods replaced by spies, ideal for mocking Angular services.
1// src/app/services/auth.service.ts
2@Injectable({ providedIn: 'root' })
3export class AuthService {
4 isLoggedIn(): boolean { /* real check */ return false; }
5 login(email: string, password: string): Observable<User> { /* real HTTP */ }
6 logout(): void { /* real logout */ }
7 getCurrentUser(): User | null { return null; }
8}
9
10// Spec: create spy object matching the service interface
11let mockAuthService: jasmine.SpyObj<AuthService>;
12
13beforeEach(() => {
14 mockAuthService = jasmine.createSpyObj<AuthService>('AuthService', [
15 'isLoggedIn', 'login', 'logout', 'getCurrentUser'
16 ]);
17
18 // Configure return values
19 mockAuthService.isLoggedIn.and.returnValue(true);
20 mockAuthService.getCurrentUser.and.returnValue({ id: '1', name: 'Alice', email: '[email protected]' });
21 mockAuthService.login.and.returnValue(of({ id: '1', name: 'Alice', email: '[email protected]' }));
22 mockAuthService.logout.and.stub(); // void method — no return value needed
23});
24
25// In tests:
26it('displays user name when logged in', () => {
27 expect(mockAuthService.isLoggedIn()).toBe(true);
28 // component uses service result in template
29 fixture.detectChanges();
30 expect(fixture.nativeElement.querySelector('.username').textContent).toBe('Alice');
31});
32
33it('calls logout on the service when sign-out is clicked', () => {
34 const button = fixture.debugElement.query(By.css('[data-testid="logout-btn"]'));
35 button.nativeElement.click();
36 expect(mockAuthService.logout).toHaveBeenCalledTimes(1);
37});
38
39// Spying on existing service instances (alternative to createSpyObj):
40const realService = TestBed.inject(UserService);
41const spy = spyOn(realService, 'getUser').and.returnValue(of({ id: '1', name: 'Bob' }));Reactive forms expose their state programmatically, making them straightforward to test — set values directly on form controls and assert on validity and errors.
1// src/app/registration/registration.component.ts
2@Component({ selector: 'app-registration', template: `...` })
3export class RegistrationComponent {
4 form = this.fb.group({
5 name: ['', [Validators.required, Validators.minLength(2)]],
6 email: ['', [Validators.required, Validators.email]],
7 password: ['', [Validators.required, Validators.minLength(8)]],
8 });
9
10 constructor(private fb: FormBuilder) {}
11
12 get nameErrors() { return this.form.get('name')?.errors; }
13 get emailErrors() { return this.form.get('email')?.errors; }
14
15 submit(): void { if (this.form.valid) { /* submit */ } }
16}
17
18// Spec:
19beforeEach(async () => {
20 await TestBed.configureTestingModule({
21 imports: [ReactiveFormsModule],
22 declarations: [RegistrationComponent],
23 }).compileComponents();
24
25 fixture = TestBed.createComponent(RegistrationComponent);
26 component = fixture.componentInstance;
27 fixture.detectChanges();
28});
29
30it('should be invalid when the form is empty', () => {
31 expect(component.form.valid).toBeFalse();
32});
33
34it('should have required error on name when empty', () => {
35 const nameControl = component.form.get('name')!;
36 nameControl.markAsTouched();
37 expect(nameControl.errors?.['required']).toBeTrue();
38});
39
40it('should have minlength error when name is one character', () => {
41 component.form.get('name')!.setValue('A');
42 expect(component.form.get('name')!.errors?.['minlength']).toBeTruthy();
43});
44
45it('should be valid when all fields meet requirements', () => {
46 component.form.setValue({
47 name: 'Alice Smith',
48 email: '[email protected]',
49 password: 'SecurePassword1!',
50 });
51 expect(component.form.valid).toBeTrue();
52});
53
54it('should have email error for malformed email', () => {
55 component.form.get('email')!.setValue('not-an-email');
56 expect(component.form.get('email')!.errors?.['email']).toBeTrue();
57});HttpClientTestingModule replaces the real HTTP backend with a test controller that lets you assert on outgoing requests and flush fake responses.
1// src/app/services/course.service.ts
2@Injectable({ providedIn: 'root' })
3export class CourseService {
4 constructor(private http: HttpClient) {}
5
6 getCourses(): Observable<Course[]> {
7 return this.http.get<Course[]>('/api/courses');
8 }
9
10 createCourse(data: Partial<Course>): Observable<Course> {
11 return this.http.post<Course>('/api/courses', data);
12 }
13}
14
15// src/app/services/course.service.spec.ts
16import { TestBed } from '@angular/core/testing';
17import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
18import { CourseService } from './course.service';
19
20describe('CourseService', () => {
21 let service: CourseService;
22 let httpMock: HttpTestingController;
23
24 beforeEach(() => {
25 TestBed.configureTestingModule({
26 imports: [HttpClientTestingModule],
27 providers: [CourseService]
28 });
29 service = TestBed.inject(CourseService);
30 httpMock = TestBed.inject(HttpTestingController);
31 });
32
33 afterEach(() => {
34 httpMock.verify(); // ensures no unexpected requests remain
35 });
36
37 it('fetches courses from GET /api/courses', () => {
38 const mockCourses = [{ id: '1', title: 'Angular Essentials' }];
39 service.getCourses().subscribe(courses => {
40 expect(courses).toHaveSize(1);
41 expect(courses[0].title).toBe('Angular Essentials');
42 });
43
44 const req = httpMock.expectOne('/api/courses');
45 expect(req.request.method).toBe('GET');
46 req.flush(mockCourses); // provide the response
47 });
48
49 it('sends course data in POST body', () => {
50 const newCourse = { title: 'TypeScript Deep Dive' };
51 service.createCourse(newCourse).subscribe();
52
53 const req = httpMock.expectOne('/api/courses');
54 expect(req.request.method).toBe('POST');
55 expect(req.request.body).toEqual(newCourse);
56 req.flush({ id: '2', ...newCourse });
57 });
58
59 it('handles server errors correctly', () => {
60 service.getCourses().subscribe({
61 error: (err) => expect(err.status).toBe(500)
62 });
63
64 const req = httpMock.expectOne('/api/courses');
65 req.flush('Server error', { status: 500, statusText: 'Internal Server Error' });
66 });
67});Angular's change detection is manual in tests by default. You must call fixture.detectChanges() after any state change that should update the template.
1import { fakeAsync, tick, flush } from '@angular/core/testing';
2
3// fakeAsync: runs test in a fake async zone where time can be controlled
4it('displays success message after 2 seconds', fakeAsync(() => {
5 component.save();
6 tick(2000); // advance virtual time by 2000ms
7 fixture.detectChanges();
8 expect(fixture.nativeElement.textContent).toContain('Saved successfully');
9 flush(); // drain any remaining macrotasks
10}));
11
12// waitForAsync: for tests using real Promises (not setTimeout)
13it('loads user data on init', waitForAsync(() => {
14 mockUserService.getUser.and.returnValue(
15 Promise.resolve({ id: '1', name: 'Alice' })
16 );
17
18 fixture.detectChanges(); // triggers ngOnInit
19
20 fixture.whenStable().then(() => {
21 fixture.detectChanges(); // update template after Promise resolves
22 const heading = fixture.debugElement.query(By.css('h1'));
23 expect(heading.nativeElement.textContent).toBe('Alice');
24 });
25}));
26
27// Triggering detectChanges after programmatic change:
28it('updates template after property change', () => {
29 component.message = 'Hello world';
30 fixture.detectChanges(); // REQUIRED — template will not update without this
31 expect(fixture.nativeElement.querySelector('p').textContent).toBe('Hello world');
32});
33
34// DebugElement queries:
35const allButtons = fixture.debugElement.queryAll(By.css('button'));
36const byDirective = fixture.debugElement.query(By.directive(RouterLinkWithHref));
37const nativeDiv = fixture.debugElement.query(By.css('.container')).nativeElement;An Angular product list component and its backing service tested end-to-end with TestBed, jasmine spies, HttpClientTestingModule, and change detection.
1// src/app/products/product-list.component.ts
2@Component({
3 selector: 'app-product-list',
4 template: `
5 <div *ngIf="loading" data-testid="spinner">Loading products...</div>
6 <div *ngIf="error" role="alert">{{ error }}</div>
7 <ul *ngIf="!loading && !error">
8 <li *ngFor="let product of products" [attr.data-testid]="'product-' + product.id">
9 {{ product.name }} — ${{ product.price }}
10 <button (click)="addToCart(product)">Add to cart</button>
11 </li>
12 </ul>
13 <p *ngIf="!loading && products.length === 0">No products available.</p>
14 `
15})
16export class ProductListComponent implements OnInit {
17 products: Product[] = [];
18 loading = false;
19 error = '';
20
21 constructor(
22 private productService: ProductService,
23 private cartService: CartService
24 ) {}
25
26 ngOnInit(): void {
27 this.loading = true;
28 this.productService.getAll().subscribe({
29 next: (products) => { this.products = products; this.loading = false; },
30 error: (err) => { this.error = 'Failed to load products'; this.loading = false; }
31 });
32 }
33
34 addToCart(product: Product): void {
35 this.cartService.add(product);
36 }
37}
38
39// src/app/products/product-list.component.spec.ts
40import { ComponentFixture, TestBed } from '@angular/core/testing';
41import { By } from '@angular/platform-browser';
42import { of, throwError } from 'rxjs';
43import { ProductListComponent } from './product-list.component';
44import { ProductService } from '../services/product.service';
45import { CartService } from '../services/cart.service';
46
47const mockProducts = [
48 { id: 1, name: 'Widget Pro', price: 29.99 },
49 { id: 2, name: 'Gadget Plus', price: 49.99 },
50];
51
52describe('ProductListComponent', () => {
53 let component: ProductListComponent;
54 let fixture: ComponentFixture<ProductListComponent>;
55 let mockProductService: jasmine.SpyObj<ProductService>;
56 let mockCartService: jasmine.SpyObj<CartService>;
57
58 beforeEach(async () => {
59 mockProductService = jasmine.createSpyObj<ProductService>('ProductService', ['getAll']);
60 mockCartService = jasmine.createSpyObj<CartService>('CartService', ['add']);
61 mockProductService.getAll.and.returnValue(of(mockProducts));
62
63 await TestBed.configureTestingModule({
64 declarations: [ProductListComponent],
65 providers: [
66 { provide: ProductService, useValue: mockProductService },
67 { provide: CartService, useValue: mockCartService },
68 ]
69 }).compileComponents();
70
71 fixture = TestBed.createComponent(ProductListComponent);
72 component = fixture.componentInstance;
73 fixture.detectChanges();
74 });
75
76 it('renders a list item for each product', () => {
77 const items = fixture.debugElement.queryAll(By.css('li'));
78 expect(items.length).toBe(2);
79 });
80
81 it('displays product name and price', () => {
82 const firstItem = fixture.debugElement.query(
83 By.css('[data-testid="product-1"]')
84 ).nativeElement;
85 expect(firstItem.textContent).toContain('Widget Pro');
86 expect(firstItem.textContent).toContain('29.99');
87 });
88
89 it('shows loading spinner initially', () => {
90 // Re-create with a never-resolving observable
91 mockProductService.getAll.and.returnValue(new Observable());
92 fixture = TestBed.createComponent(ProductListComponent);
93 component = fixture.componentInstance;
94 fixture.detectChanges();
95 expect(fixture.debugElement.query(By.css('[data-testid="spinner"]'))).toBeTruthy();
96 });
97
98 it('shows error message when service fails', () => {
99 mockProductService.getAll.and.returnValue(throwError(() => new Error('Network error')));
100 fixture = TestBed.createComponent(ProductListComponent);
101 fixture.detectChanges();
102 const alert = fixture.debugElement.query(By.css('[role="alert"]'));
103 expect(alert.nativeElement.textContent).toContain('Failed to load products');
104 });
105
106 it('calls cartService.add with the product when Add to cart is clicked', () => {
107 const button = fixture.debugElement.query(By.css('button'));
108 button.nativeElement.click();
109 expect(mockCartService.add).toHaveBeenCalledWith(mockProducts[0]);
110 });
111
112 it('shows empty state when no products are returned', () => {
113 mockProductService.getAll.and.returnValue(of([]));
114 fixture = TestBed.createComponent(ProductListComponent);
115 fixture.detectChanges();
116 expect(fixture.nativeElement.textContent).toContain('No products available.');
117 });
118});This spec demonstrates the complete Angular testing pattern: createSpyObj for both service dependencies, of() for synchronous Observables, throwError() for error path testing, fixture.detectChanges() after each state change, debugElement.query() with By.css() for DOM queries, and data-testid attributes as a last-resort selector when semantic queries are impractical. The afterEach call to httpMock.verify() (in the service spec) ensures no pending requests escape between tests.
The next lesson covers snapshot testing in Jest: capturing serialised component output with toMatchSnapshot(), reviewing and updating snapshot files, and when snapshots add value vs when they become a maintenance burden.
Your progress is saved in this browser, on this device only.