web-dev-qa-db-ja.com

コンポーネントのクリーンアップ中にサブスクライブ解除エラーでAngularコンポーネントをテストする

ルーターのパラメーターをサブスクライブするコンポーネントをテストしています。すべてのテストパスとすべてが正常に動作します。しかし、コンソールを見ると、エラーが表示されます。

コンポーネントApplicationViewComponent localConsole。(anonymous function)@ context.js:232のクリーンアップ中のエラー

これが起こる理由を知っていますか?

unsubscribe()メソッドからngOnDestroy()を削除しようとすると、エラーが消えます。

karma/jasmineはunsubscribe()を自動的にサポートしていますか?

これがコンポーネントとテストです

成分

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

}

コンポーネント仕様ファイル

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"         
      }
    ]);
  }
}
35
BlackHoleGalaxy

受け入れられた解決策は最適ではなく、テストが正しくセットアップされていないという事実を回避します。

「コンポーネントのクリーンアップ中のエラー」エラーメッセージは、ngOnDestroy()が呼び出されたときにthis.routeSubscriptionが未定義であるために発生します。これは、ngOnInit()が呼び出されなかったため、つまり、ルートにサブスクライブしたことがないためです。 Angular testing tutorial で説明されているように、_fixture.detectChanges()を最初に呼び出すまで、コンポーネントは完全に初期化されません。

したがって、正しいソリューションは、createComponentが呼び出された直後にfixture.detectChanges()ブロックにbeforeEach()を追加することです。フィクスチャを作成した後はいつでも追加できます。そうすることで、コンポーネントが完全に初期化され、コンポーネントのクリーンアップも期待どおりに動作するようになります。

69
randomPoison

メソッドngOnDestroyを以下のようにリファクタリングする必要があります。

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

したがって、私の状況は似ていましたが、まったく同じではありません。他の誰かがそれを助けてくれると思った場合に備えて、これをここに置いています。 Jamine/Karmaで単体テストを行ったとき、

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

それは、私がオブザーバブルを適切に処理していなかったためであり、それらにはエラー関数がありませんでした。そのため、修正によりエラー関数が追加されました。

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

@David Brownの応答に次のコードを追加すると、うまくいきました。

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

私は、コンポーネント自体のコンテキスト外でコンポーネントの機能をテストしたいという同様の状況にあります。

これは私のために働いたものです:

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

このエラーを解決するには、2つのことを行う必要があります。

1- fixture.detectChanges(); beforeEach()を追加
2-コンポーネントを明確にするために、以下を追加する必要があります。

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

私の場合、各テストが問題を解決した後にコンポーネントを破壊しました。したがって、これを記述関数に追加してみてください。

afterEach(() => {
  fixture.destroy();
})
1
Alex Link

私の場合、エラーはテンプレートにありました。子コンポーネントngDestroyにエラーがありましたが、読み取り専用プロパティを設定しようとしていたため破壊されませんでした。子コンポーネントが適切に破棄されているかどうかを確認するのに時間をかける価値があります。

0
Vikhyath Maiya

私にとって、このエラーを修正したのはコンポーネントのngOnDestroyの内部でした。ストアディスパッチとサブスクライブ解除を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()));
  }
}
0
Papa_D