A hands-on, memory-efficient, and future-proof approach to mocking HTTP calls, services, and dependencies in Angular standalone applications using Jest.
Introduction
Have you ever struggled to mock services, constants, timers, or injected dependencies in Angular—especially after switching to standalone components?
You're not alone.
With Angular moving towards standalone APIs and modular design, our testing practices must evolve too. If you're still using TestBed.configureTestingModule() for everything—you might be doing more than you need.
In this guide, you'll learn exactly how to mock Angular services, RxJS-based timers, external libraries, constants, and directives in Jest, using modern Angular testing practices that are clean, scalable, and memory-safe.
What You’ll Learn
- How to test standalone Angular services with Jest
- Mocking HTTP requests using
HttpClientTestingModulewith standalone setup - Handling dependency injection with external libraries, constants, pipes, or directives
- Simulating timers and RxJS
interval,timer, orsetTimeoutwithjest.useFakeTimers() - Making Jest tests memory-efficient (why and how)
- Practical code demos for each use case
- Jest best practice
- Angular best practice
Example: Standalone Angular Service
user.service.ts
import { inject, Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable, timer } from 'rxjs';
import { map } from 'rxjs/operators';
import { API_URL } from './tokens'; // a custom InjectionToken
@Injectable({ providedIn: 'root' })
export class UserService {
private http = inject(HttpClient);
private baseUrl = inject(API_URL); // Using custom token
getUser(id: number): Observable<any> {
return this.http.get(`${this.baseUrl}/users/${id}`);
}
getUserWithDelay(id: number): Observable<any> {
return timer(1000).pipe(
map(() => ({ id, name: 'Delayed User' }))
);
}
}
Unit Test Using Standalone Jest Setup
user.service.spec.ts
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { TestBed } from '@angular/core/testing';
import { UserService } from './user.service';
import { API_URL } from './tokens';
import { jest } from '@jest/globals';
describe('UserService (Standalone + Jest)', () => {
let service: UserService;
let httpMock: HttpTestingController;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [HttpClientTestingModule],
providers: [
UserService,
{ provide: API_URL, useValue: 'https://mock.api' }
]
});
service = TestBed.inject(UserService);
httpMock = TestBed.inject(HttpTestingController);
});
afterEach(() => {
httpMock.verify(); // Ensures no memory leaks from open HTTP calls
jest.clearAllTimers(); // Reset timers between tests
});
it('should fetch user by ID', () => {
const mockUser = { id: 1, name: 'Jane' };
service.getUser(1).subscribe(user => {
expect(user).toEqual(mockUser);
});
const req = httpMock.expectOne('https://mock.api/users/1');
expect(req.request.method).toBe('GET');
req.flush(mockUser);
});
it('should return delayed user with fake timer', () => {
jest.useFakeTimers(); // Prevent real timer execution
let result: any;
service.getUserWithDelay(2).subscribe(user => (result = user));
jest.advanceTimersByTime(1000); // Simulate delay
expect(result).toEqual({ id: 2, name: 'Delayed User' });
});
});
Section: Spying and Mocking With jest.fn()
How to:
- Replace service methods
- Simulate return values or throw errors
- Combine
jest.spyOn()with dependency injection
const mockUserService = {
getUser: jest.fn().mockReturnValue(of({ id: 1, name: 'Mock User' })),
};
Section: Error Handling & Retry Tests
Demo:
- Simulating 500 errors
- Handling retries with
retry()operator - Verifying failure UI states
Bonus: Shared Mocks & DRY Test Setup
How to:
- Create reusable
mock-user.service.ts - Abstract mocks in
__mocks__folder for auto-mocking - Mock with dynamic data using factory functions
Advanced Use Case: Injecting Pipes and Directives in Angular Services
Why would a service inject a pipe or directive?
- You may be using custom pipes for transformation logic you want to reuse (e.g., formatting currency, date, slugs).
- You might inject a directive that provides some shared behavior or config via
@Directive({ providers: [...] }).
These scenarios are uncommon but powerful—especially in design systems, custom form libraries, or rendering engines.
Demo Use Case: Injecting a Pipe and a Directive in a Service
slugify.pipe.ts
import { Pipe, PipeTransform, inject } from '@angular/core';
@Pipe({ name: 'slugify', standalone: true })
export class SlugifyPipe implements PipeTransform {
transform(value: string): string {
return value.toLowerCase().replace(/\s+/g, '-');
}
}
feature-toggle.directive.ts
import { Directive, inject } from '@angular/core';
@Directive({
selector: '[appFeatureToggle]',
standalone: true,
providers: [{ provide: FeatureToggleDirective, useExisting: FeatureToggleDirective }]
})
export class FeatureToggleDirective {
isFeatureEnabled = true; // Could be dynamically set
}
seo.service.ts — Using Both
import { Injectable, inject } from '@angular/core';
import { SlugifyPipe } from './slugify.pipe';
import { FeatureToggleDirective } from './feature-toggle.directive';
@Injectable({ providedIn: 'root' })
export class SeoService {
private slugify = inject(SlugifyPipe);
private featureToggle = inject(FeatureToggleDirective);
getSeoSlug(title: string): string {
if (this.featureToggle.isFeatureEnabled) {
return this.slugify.transform(title);
}
return 'seo-disabled';
}
}
Jest Unit Test: Mocking Pipes and Directives
🧪 seo.service.spec.ts
import { TestBed } from '@angular/core/testing';
import { SeoService } from './seo.service';
import { SlugifyPipe } from './slugify.pipe';
import { FeatureToggleDirective } from './feature-toggle.directive';
describe('SeoService with Pipe & Directive Injection', () => {
let service: SeoService;
const mockSlugifyPipe: Partial<SlugifyPipe> = {
transform: jest.fn().mockImplementation((s: string) => `slug--${s}`)
};
const mockFeatureToggleDirective: Partial<FeatureToggleDirective> = {
isFeatureEnabled: true
};
beforeEach(() => {
TestBed.configureTestingModule({
providers: [
SeoService,
{ provide: SlugifyPipe, useValue: mockSlugifyPipe },
{ provide: FeatureToggleDirective, useValue: mockFeatureToggleDirective }
]
});
service = TestBed.inject(SeoService);
});
it('should return slugified title when feature is enabled', () => {
const result = service.getSeoSlug('Hello World');
expect(result).toBe('slug--Hello World');
});
it('should return "seo-disabled" if feature is off', () => {
(mockFeatureToggleDirective.isFeatureEnabled as boolean) = false;
const result = service.getSeoSlug('Hello World');
expect(result).toBe('seo-disabled');
});
});
Real Developer Talk: Why You Should Care
The standalone approach is Angular’s future—and Jest is your ticket to faster, smarter, and more scalable tests.
Say goodbye to heavy test modules and long setup code.
Say hello to on-demand injection, fast feedback loops, and confidence in every refactor.
Best Practices Summary
Angular Testing Best Practices
- Use
inject()instead of constructor injection where practical - Avoid global providers when not needed
- Always call
httpMock.verify()inafterEach - Use
jest.useFakeTimers()+jest.clearAllTimers()for memory-safe timer mocks
Jest Best Practices
- Prefer
jest.fn()for mocks, not manual stubs - Use
jest.mock()orjest.spyOn()for external dependencies - Clean up side effects (
timers,mocks) inafterEach - Keep test cases small, focused, and stateless
Takeaway Best Practices
Never directly depend on DOM-based directive logic in services, unless:
- The directive is logic-based (feature flags, configuration, etc.)
- It’s provided via DI using
useExisting
For pipes, prefer:
-
@Pipe({ standalone: true })with properprovidedIninjection - Creating mock pipes using
jest.fn()in unit tests
Avoid leaking real logic in tests. Always:
- Use fake timers
- Use jest mocks for all dependencies
- Avoid DOM usage in unit tests
🎯 Your Turn, Devs!
👀 Did this article spark new ideas or help solve a real problem?
💬 I'd love to hear about it!
✅ Are you already using this technique in your Angular or frontend project?
🧠 Got questions, doubts, or your own twist on the approach?
Drop them in the comments below — let’s learn together!
🙌 Let’s Grow Together!
If this article added value to your dev journey:
🔁 Share it with your team, tech friends, or community — you never know who might need it right now.
📌 Save it for later and revisit as a quick reference.
🚀 Follow Me for More Angular & Frontend Goodness:
I regularly share hands-on tutorials, clean code tips, scalable frontend architecture, and real-world problem-solving guides.
- 💼 LinkedIn — Let’s connect professionally
- 🎥 Threads — Short-form frontend insights
- 🐦 X (Twitter) — Developer banter + code snippets
- 👥 BlueSky — Stay up to date on frontend trends
- 🌟 GitHub Projects — Explore code in action
- 🌐 Website — Everything in one place
- 📚 Medium Blog — Long-form content and deep-dives
- 💬 Dev Blog — Free Long-form content and deep-dives
- ✉️ Substack — Weekly frontend stories & curated resources
- 🧩 Portfolio — Projects, talks, and recognitions
- ✍️ Hashnode — Developer blog posts & tech discussions
🎉 If you found this article valuable:
- Leave a 👏 Clap
- Drop a 💬 Comment
- Hit 🔔 Follow for more weekly frontend insights
Let’s build cleaner, faster, and smarter web apps — together.
Stay tuned for more Angular tips, patterns, and performance tricks! 🧪🧠🚀
Top comments (0)