DEV Community

Cover image for How to Mock Standalone Angular Services and RxJS Timers in Jest (Without Memory Leaks) With Best Practises Of Jest, Angular
Rajat
Rajat

Posted on

How to Mock Standalone Angular Services and RxJS Timers in Jest (Without Memory Leaks) With Best Practises Of Jest, Angular

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 HttpClientTestingModule with standalone setup
  • Handling dependency injection with external libraries, constants, pipes, or directives
  • Simulating timers and RxJS interval, timer, or setTimeout with jest.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' }))
    );
  }
}

Enter fullscreen mode Exit fullscreen mode

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' });
  });
});

Enter fullscreen mode Exit fullscreen mode

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' })),
};

Enter fullscreen mode Exit fullscreen mode

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, '-');
  }
}

Enter fullscreen mode Exit fullscreen mode

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
}

Enter fullscreen mode Exit fullscreen mode

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';
  }
}

Enter fullscreen mode Exit fullscreen mode

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');
  });
});

Enter fullscreen mode Exit fullscreen mode

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() in afterEach
  • Use jest.useFakeTimers() + jest.clearAllTimers() for memory-safe timer mocks

Jest Best Practices

  • Prefer jest.fn() for mocks, not manual stubs
  • Use jest.mock() or jest.spyOn() for external dependencies
  • Clean up side effects (timers, mocks) in afterEach
  • 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 proper providedIn injection
  • 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! 🧪🧠🚀

✨ Share Your Thoughts To 📣 Set Your Notification Preference

Top comments (0)