Search code examples
angularunit-testingrxjsobservablesubscription

How to unit test unsubscribe function in angular


I would like to find a way to test unsubscribe function calls on Subscriptions and Subjects.

I came up with a few possible solutions, but every one of these have pros and cons. Please keep in mind that I do not want to alter the access modifier of a variable for testing purposes.

  1. Accessing private variable of component with reflection.

In that case I have a private class variable which stores a subscription:

component.ts:

private mySubscription: Subscription;
//...
ngOnInit(): void {
    this.mySubscription = this.store
        .select(mySelector)
        .subscribe((value: any) => console.log(value));
}

ngOnDestroy(): void {
    this.mySubscription.unsubscribe();
}

component.spec.ts:

spyOn(component['mySubscription'], 'unsubscribe');
component.ngOnDestroy();
expect(component['mySubscription'].unsubscribe).toHaveBeenCalledTimes(1);

pros:

  • I can reach mySubscription.
  • I can test that the unsubscribe method was invoked on the right subscription.

cons:

  • I can reach mySubscription only with reflection, what I would like to avoid if possible.

  1. I create a variable for subscription just like in option 1., but instead of reaching the variable with reflection I simply check that the unsubscribe method was invoked, without knowing the source.

component.ts: same as in option 1


component.spec.ts:

spyOn(Subscription.prototype, 'unsubscribe');
component.ngOnDestroy();
expect(Subscription.prototype.unsubscribe).toHaveBeenCalledTimes(1);

pros:

  • I can test that the unsubscribe method was called

cons:

  • I can not test the source of the invoked unsubscribe method.

  1. I implemented a helper function which invokes unsubscribe method on the passed parameters which are subscriptions.

subscription.helper.ts:

export class SubscriptionHelper {

    static unsubscribeAll(...subscriptions: Subscription[]) {
        subscriptions.forEach((subscription: Subscription) => {
                subscription.unsubscribe();
            });
    }
}

component.ts: same as in option 1, but ngOnDestroy is different:

ngOnDestroy(): void {
    SubscriptionHelper.unsubscribeAll(this.mySubscription);
}

component.spec.ts:

spyOn(SubscriptionHelper, 'unsubscribeAll');
component.ngOnDestroy();
expect(SubscriptionHelper.unsubscribeAll).toHaveBeenCalledTimes(1);

pros:

  • I can test that the helper function was called

cons:

  • I can not test that the unsubscribe function was called on a specific subscription.

What do you guys suggest? How do you test the cleanup in unit test?


Solution

  • I had exactly the same problem, here's my solution:

    component.ts:

    private subscription: Subscription;
    //...
    ngOnInit(): void {
        this.subscription = this.route.paramMap.subscribe((paramMap: ParamMap) => {
            // ...
        });
    }
    
    ngOnDestroy(): void {
        this.subscription.unsubscribe();
    }
    
    

    component.spec.ts:

    
    let dataMock;
    
    let storeMock: Store;
    let storeStub: {
        select: Function,
        dispatch: Function
    };
    
    let paramMapMock: ParamMap;
    let paramMapSubscription: Subscription;
    let paramMapObservable: Observable<ParamMap>;
    
    let activatedRouteMock: ActivatedRoute;
    let activatedRouteStub: {
        paramMap: Observable<ParamMap>;
    };
    
    beforeEach(async(() => {
        dataMock = { /* some test data */ };
    
        storeStub = {
            select: (fn: Function) => of((id: string) => dataMock),
            dispatch: jasmine.createSpy('dispatch')
        };
    
        paramMapMock = {
            keys: [],
            has: jasmine.createSpy('has'),
            get: jasmine.createSpy('get'),
            getAll: jasmine.createSpy('getAll')
        };
    
        paramMapSubscription = new Subscription();
        paramMapObservable = new Observable<ParamMap>();
    
        spyOn(paramMapSubscription, 'unsubscribe').and.callThrough();
        spyOn(paramMapObservable, 'subscribe').and.callFake((fn: Function): Subscription => {
            fn(paramMapMock);
            return paramMapSubscription;
        });
    
        activatedRouteStub = {
            paramMap: paramMapObservable
        };
    
        TestBed.configureTestingModule({
           // ...
           providers: [
               { provide: Store, useValue: storeStub },
               { provide: ActivatedRoute, useValue: activatedRouteStub }
           ]
        })
        .compileComponents();
    }));
    
    // ...
    
    it('unsubscribes when destoryed', () => {
        fixture.detectChanges();
    
        component.ngOnDestroy();
    
        expect(paramMapSubscription.unsubscribe).toHaveBeenCalled();
    });
    

    This works for me, I hope it will for you too !