'Angular - How to Unit Test a Subscription on a Service?
I've been trying to find a way to test a subscription that is on a mocked service within Angular using Jest. But I can't find a way to mock the Observable and test the assignment of the isMobile property.
When I try to spyOn and then mock the service to return a value, it is never updated. And I would like to be able to test the code with different responses from the service.
Code below.
Component
import { Component, Input, OnInit } from '@angular/core';
import { BREAKPOINT, BreakpointService } from '../../services';
@Component({
selector: 'auxiliary-bar',
templateUrl: './auxiliary-bar.component.html',
styleUrls: ['./auxiliary-bar.component.scss'],
})
export class AuxiliaryBarComponent implements OnInit {
public isMobile = false;
constructor(private breakpointService: BreakpointService) {}
ngOnInit(): void {
this.breakpointService.onBreakpoint$.subscribe(
(breakpoint) => (this.isMobile = breakpoint <= BREAKPOINT.SM),
);
}
}
Breakpoint Service
import { Injectable, NgZone, OnDestroy } from '@angular/core';
import { Observable, Subject } from 'rxjs';
import { distinctUntilChanged } from 'rxjs/operators';
export enum BREAKPOINT {
XS,
SM,
MD,
LG,
XL,
}
@Injectable({
providedIn: 'root',
})
export class BreakpointService implements OnDestroy {
private breakpointSubject: Subject<BREAKPOINT> = new Subject();
private observer!: ResizeObserver;
private sizes = [
{
id: BREAKPOINT.SM,
width: 0,
},
{
id: BREAKPOINT.SM,
width: 320,
},
{
id: BREAKPOINT.MD,
width: 768,
},
{
id: BREAKPOINT.LG,
width: 1024,
},
{
id: BREAKPOINT.XL,
width: 1200,
},
];
constructor(private zone: NgZone) {
this.observer = new ResizeObserver((entries) => {
this.zone.run(() => {
const matchedSize = this.sizes
.slice(1)
.reverse()
.find(
(size) => window.matchMedia(`(min-width: ${size.width}px)`).matches,
);
this.breakpointSubject.next(
matchedSize ? matchedSize.id : BREAKPOINT.XS,
);
});
});
this.observer.observe(document.body);
}
ngOnDestroy(): void {
this.observer.unobserve(document.body);
}
public get onBreakpoint$(): Observable<BREAKPOINT> {
return this.breakpointSubject.asObservable().pipe(distinctUntilChanged());
}
}
Attempted Test Suite
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { MockProvider } from 'ng-mocks';
import { Observable, of } from 'rxjs';
import { BreakpointService } from '../../services';
import { AuxiliaryBarComponent } from './auxiliary-bar.component';
describe('AuxiliaryBarComponent', () => {
let component: AuxiliaryBarComponent;
let fixture: ComponentFixture<AuxiliaryBarComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [
AuxiliaryBarComponent,
],
providers: [
MockProvider(BreakpointService, {
onBreakpoint$: new Observable(),
}),
],
}).compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(AuxiliaryBarComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should', () => {
const breakpointService = TestBed.inject(BreakpointService);
const spy = jest
.spyOn(breakpointService, 'onBreakpoint$', 'get')
.mockReturnValue(of(3));
fixture.detectChanges();
expect(breakpointService.onBreakpoint$.subscribe).toHaveBeenCalled();
});
)};
Solution 1:[1]
I'm pretty sure it's too late because the component is already instantiated and therefore ngInit is already called by the time your it starts. If I'm right, then a fix might be as easy as inserting component.ngOnInit() before your last fixture.detectChanges() to re-trigger it.
As an aside, your component shouldn't subscribe if you can help it. Instead, if it works for your needs then I would change the component's ngOnInit to
public isMobile$: Observable<boolean>;
ngOnInit(): void {
this.isMobile$ = this.breakpointService.onBreakpoint$.pipe(
map(breakpoint => breakpoint <= BREAKPOINT.SM)
);
}
and then in your template, wherever you used to have isMobile, use (isMobile$ | async) instead.
But if you must subscribe within a service or component (or maybe also do this for safety and good programming practice), your service should have an ngOnDestroy that calls this.breakpointSubject?complete();.
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
| Solution | Source |
|---|---|
| Solution 1 | JSmart523 |
