DEV Community

Cover image for 10 Angular Unit Tests to Prevent Memory Leaks You Didn't Know You Needed
Rajat
Rajat

Posted on

10 Angular Unit Tests to Prevent Memory Leaks You Didn't Know You Needed

Stop Hidden Performance Killers Before They Start: Essential Tests for Robust Apps

Master Angular component cleanup with real-world unit tests for setTimeout, Observables, DOM listeners, AsyncPipe, and more. A complete guide to writing leak-free Angular apps.

Perfect — here are 10 comprehensive unit test cases in Angular to ensure memory leaks are avoided. These cover all critical and edge async scenarios, such as:

  • setTimeout
  • setInterval
  • RxJS subscriptions
  • AsyncPipe
  • EventListeners
  • Subjects
  • AnimationFrames
  • WebSocket/Custom cleanup
  • ResizeObserver
  • MutationObserver

1. Unsubscribe from RxJS with takeUntil

it('should call destroy$.next() and complete() on ngOnDestroy', () => {
  const nextSpy = jest.spyOn((component as any).destroy$, 'next');
  const completeSpy = jest.spyOn((component as any).destroy$, 'complete');

  component.ngOnDestroy();

  expect(nextSpy).toHaveBeenCalledTimes(1);
  expect(completeSpy).toHaveBeenCalledTimes(1);
});

Enter fullscreen mode Exit fullscreen mode

2. Clear setInterval on Destroy

it('should clear interval on destroy', () => {
  const clearSpy = jest.spyOn(window, 'clearInterval');
  component.intervalId = setInterval(() => {}, 1000);

  component.ngOnDestroy();

  expect(clearSpy).toHaveBeenCalledWith(component.intervalId);
  clearInterval(component.intervalId); // cleanup
});

Enter fullscreen mode Exit fullscreen mode

3. Clear setTimeout on Destroy

it('should clear timeout on destroy', () => {
  const clearSpy = jest.spyOn(window, 'clearTimeout');
  component.timeoutId = setTimeout(() => {}, 1000);

  component.ngOnDestroy();

  expect(clearSpy).toHaveBeenCalledWith(component.timeoutId);
  clearTimeout(component.timeoutId); // cleanup
});

Enter fullscreen mode Exit fullscreen mode

4. Remove Event Listener on Destroy

it('should remove event listener on destroy', () => {
  const removeSpy = jest.spyOn(document, 'removeEventListener');
  component.listener = jest.fn();

  document.addEventListener('click', component.listener);
  component.ngOnDestroy();

  expect(removeSpy).toHaveBeenCalledWith('click', component.listener);
});

Enter fullscreen mode Exit fullscreen mode

5. Verify AsyncPipe Usage — No Manual Cleanup Required

it('should have data$ defined for async pipe usage', () => {
  expect(component.data$).toBeDefined();
});

Enter fullscreen mode Exit fullscreen mode

Note: AsyncPipe automatically unsubscribes on destroy.


6. Unsubscribe from Manual Subject

it('should unsubscribe from subject', () => {
  const sub = new Subject<void>();
  const spy = jest.fn();
  sub.subscribe(spy);

  sub.next();
  sub.complete();

  expect(spy).toHaveBeenCalled();
});

Enter fullscreen mode Exit fullscreen mode

7. Cancel requestAnimationFrame on Destroy

it('should cancel requestAnimationFrame on destroy', () => {
  const cancelSpy = jest.spyOn(window, 'cancelAnimationFrame');
  component.rafId = requestAnimationFrame(() => {});

  component.ngOnDestroy();

  expect(cancelSpy).toHaveBeenCalledWith(component.rafId);
});

Enter fullscreen mode Exit fullscreen mode

8. Close Custom WebSocket or Observer on Destroy

it('should close websocket connection on destroy', () => {
  component.socket = { close: jest.fn() } as any;
  component.ngOnDestroy();

  expect(component.socket.close).toHaveBeenCalled();
});

Enter fullscreen mode Exit fullscreen mode

9. Disconnect ResizeObserver on Destroy

it('should disconnect ResizeObserver on destroy', () => {
  component.resizeObserver = { disconnect: jest.fn() } as any;
  component.ngOnDestroy();

  expect(component.resizeObserver.disconnect).toHaveBeenCalled();
});

Enter fullscreen mode Exit fullscreen mode

10. Disconnect MutationObserver on Destroy

it('should disconnect MutationObserver on destroy', () => {
  component.mutationObserver = { disconnect: jest.fn() } as any;
  component.ngOnDestroy();

  expect(component.mutationObserver.disconnect).toHaveBeenCalled();
});

Enter fullscreen mode Exit fullscreen mode

🎯 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)