Testing Angular component with unsubscribe Error during cleanup of component

AngularTypescriptKarma RunnerKarma Jasmine

Angular Problem Overview


I'm testing a component which subscribe router params. Every test pass and everything works fine. But if I look in the console, I can see an error:

> Error during cleanup of component ApplicationViewComponent > localConsole.(anonymous function) @ context.js:232

Do you know why this occurs?

I tried removing the unsubscribe() from ngOnDestroy() method and the error disappears.

Is karma/jasmine supporting unsubscribe() automatically?

Here is the component and tests

Component

import { Component, OnInit } from '@angular/core';   
import { ActivatedRoute } from '@angular/router';
import { Subscription } from 'rxjs/Rx'

import { AppService } from 'app.service';

@Component({
  selector: 'app-component',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss']
})
export class AppComponent implements OnInit {
  private routeSubscription: Subscription;

  // Main ID
  public applicationId: string;
 

  constructor(
    private route: ActivatedRoute,
	private _service: AppService
  ) { }

  ngOnInit() {
    this.routeSubscription = this.route.params.subscribe(params => {
      this.applicationId = params['id'];

      this.getDetails();
      this.getList();
    });
  }

  getDetails() {
    this._service.getDetails(this.applicationId).subscribe(
      result => {     
        console.log(result);
      },
      error => {  
        console.error(error);        
      },
      () => {
        console.info('complete');
      }
    );
  }

  getList(notifyWhenComplete = false) {
    this._service.getList(this.applicationId).subscribe(
      result => {     
        console.log(result);
      },
      error => {  
        console.error(error);        
      },
      () => {
        console.info('complete');
      }
    );
  }

  ngOnDestroy() {
    this.routeSubscription.unsubscribe();
  }

}

Component spec file

import { NO_ERRORS_SCHEMA } from '@angular/core';
import {
  async,
  fakeAsync,
  ComponentFixture,
  TestBed,
  tick,
  inject
} from '@angular/core/testing';
import {
  RouterTestingModule
} from '@angular/router/testing';
import {
  HttpModule
} from '@angular/http';
import { Observable } from 'rxjs/Observable';
import { Router, ActivatedRoute } from '@angular/router';

// Components
import { AppComponent } from './app.component';

// Service
import { AppService } from 'app.service';
import { AppServiceStub } from './app.service.stub';

let comp:    AppComponent;
let fixture: ComponentFixture<AppComponent>;
let service: AppService;

let expectedApplicationId = 'abc123';

describe('AppComponent', () => {
  beforeEach(async(() => {
    TestBed.configureTestingModule({
      declarations: [AppComponent],
      imports: [RouterTestingModule, HttpModule],
      providers: [
        FormBuilder,
        {
          provide: ActivatedRoute,
          useValue: {
            params:  Observable.of({id: expectedApplicationId})
          }
        },
        {
          provide: AppService,
          useClass: AppServiceStub
        }    
      ],
      schemas: [ NO_ERRORS_SCHEMA ]
    })
    .compileComponents();
  }));

  tests();
});

function tests() {
  beforeEach(() => {
    fixture = TestBed.createComponent(AppComponent);
    comp = fixture.componentInstance;

    service = TestBed.get(AppService);
  });


  /*
  *   COMPONENT BEFORE INIT
  */
  it(`should be initialized`, () => {
    expect(fixture).toBeDefined();
    expect(comp).toBeDefined();
  });


  /*
  *   COMPONENT INIT
  */

  it(`should retrieve param id from ActivatedRoute`, async(() => {
    fixture.detectChanges();

    expect(comp.applicationId).toEqual(expectedApplicationId);
  }));

  it(`should get the details after ngOnInit`, async(() => {
    spyOn(comp, 'getDetails');
    fixture.detectChanges();
	
	expect(comp.getDetails).toHaveBeenCalled();
  }));

  it(`should get the list after ngOnInit`, async(() => {
    spyOn(comp, 'getList');
    fixture.detectChanges();
	
	expect(comp.getList).toHaveBeenCalled();
  }));
}
service.stub
import { Observable } from 'rxjs/Observable';

export class AppServiceStub {
  getList(id: string) {
    return Observable.from([              
	  {
		id: "7a0c6610-f59b-4cd7-b649-1ea3cf72347f",
		name: "item 1"
	  },
	  {
		id: "f0354c29-810e-43d8-8083-0712d1c412a3",
		name: "item 2"
	  },
	  {
		id: "2494f506-009a-4af8-8ca5-f6e6ba1824cb",
		name: "item 3"		
	  }
    ]);
  }
  getDetails(id: string) {
    return Observable.from([      
      {        
        id: id,
        name: "detailed item 1"         
      }
    ]);
  }
}

Angular Solutions


Solution 1 - Angular

The "Error during component cleanup" error message happens because when ngOnDestroy() is called, this.routeSubscription is undefined. This happens because ngOnInit() was never invoked, meaning that you never subscribed to the route. As described in the Angular testing tutorial, the component isn't initialized fully until you call fixture.detectChanges() the first time.

Therefore, the correct solution is to add fixture.detectChanges() to your beforeEach() block right after the createComponent is called. It can be added any time after you create the fixture. Doing so will ensure that the component is fully initialized, that way component cleanup will also behave as expected.

Solution 2 - Angular

You need to refactor your method ngOnDestroy as below :

ngOnDestroy() {
  if ( this.routeSubscription)
    this.routeSubscription.unsubscribe();
}

Solution 3 - Angular

In my case destroying the component after each test solved the problem. So you could try adding this to your describe function:

afterEach(() => {
  fixture.destroy();
})

Solution 4 - Angular

So my situation was similar, but not exactly the same: I'm just putting this here in case someone else finds it helpful. When unit testing with Jamine/Karma I was getting

 'ERROR: 'Error during cleanup of component','

It turns out that was because I wasn't properly handling my observables, and they didn't have an error function on them. So the fix was adding an error function:

this.entityService.subscribe((items) => {
      ///Do work
},
  error => {
    this.errorEventBus.throw(error);
  });

Solution 5 - Angular

I'm in a similar situation where I want to test a function in my component outside the context of the component itself.

This is what worked for me:

afterEach(() => {
  spyOn(component, 'ngOnDestroy').and.callFake(() => { });
  fixture.destroy();
});

Solution 6 - Angular

Adding to @David Brown's response the code below is what worked for me.

      .subscribe(res => {
          ...
        },
        error => Observable.throw(error)
      )

Solution 7 - Angular

You have to do 2 things, to solve this error.

1- add fixture.detectChanges(); in beforeEach()
2 - you need to add below, so that component can be clear.

afterEach(() => {
        fixture.destroy();
      });

Solution 8 - Angular

As explained by @randomPoison, the error is triggered when the component that uses unsubscribe is not initialised. However, calling fixture.detectChanges() is a solution for when the error is in the spec file of the respective component.

But we might also be dealing with a FooComponent that creates BarComponent and BarComponent uses unsubscribe in its ngOnDestroy. Proper mocking must be done.

I'd suggest a different approach to the subscription cleanup, one that is declarative and won't trigger such problems. Here's an example:

export class BazComponent implements OnInit, OnDestroy {
  private unsubscribe$ = new Subject();

  ngOnInit(): void {
    someObservable$
      .pipe(takeUntil(this.unsubscribe$))
      .subscribe(...);
  }

  ngOnDestroy(): void {
    this.unsubscribe$.next();
    this.unsubscribe$.complete();
  }
}

More on this approach here

Solution 9 - Angular

Well in my case the error was in the template. There was error in the child component ngDestroy ,which wasn't getting destroyed as i was trying to set readonly property. It would be worth your time checking your child components whether they are getting destroyed properly.

Solution 10 - Angular

For me what fixed this error was inside of my component's ngOnDestroy, I wrapped my store dispatch and my unsubscribe in a try catch.

ngOnDestroy(): void {
 try {
  this.store.dispatch(new foo.Bar(this.testThing()));
  if(this.fooBarSubscription) {
   this.fooBarSubscription.unsubscribe();
  }
 } catch (error) {
   this.store.dispatch(new foo.Bar(this.testThing()));
  }
}

Solution 11 - Angular

In my case I was testing a component with multiple @Input properties. I had to set it inside beforeEach block to [] component.xyz = [] (since it was type of an array). That was the origin of an problem.

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionBlackHoleGalaxyView Question on Stackoverflow
Solution 1 - AngularrandomPoisonView Answer on Stackoverflow
Solution 2 - AngularmuseczView Answer on Stackoverflow
Solution 3 - AngularAlex LinkView Answer on Stackoverflow
Solution 4 - AngularDavid BrownView Answer on Stackoverflow
Solution 5 - AngularRichard MedeirosView Answer on Stackoverflow
Solution 6 - AngularPetros KyriakouView Answer on Stackoverflow
Solution 7 - AngularManas Kumar MaharanaView Answer on Stackoverflow
Solution 8 - AngularBogdan DView Answer on Stackoverflow
Solution 9 - AngularVikhyath MaiyaView Answer on Stackoverflow
Solution 10 - AngularPapa_DView Answer on Stackoverflow
Solution 11 - AngularChaka15View Answer on Stackoverflow