在清理组件期间使用unsubscribe错误测试Angular组件

时间:2017-04-11 15:23:29

标签: angular typescript karma-runner karma-jasmine

我正在测试订阅路由器参数的组件。每个测试通过,一切正常。但如果我查看控制台,我会看到一个错误:

  

清理组件ApplicationViewComponent时出错   localConsole。(匿名函数)@ 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"         
      }
    ]);
  }
}

10 个答案:

答案 0 :(得分:64)

公认的解决方案并不是最优的,它可以解决测试未正确设置的问题。

组件清理期间&#34;错误&#34;发生错误消息,因为调用ngOnDestroy()时,this.routeSubscription未定义。发生这种情况是因为从未调用过ngOnInit(),这意味着您从未订阅该路由。如Angular testing tutorial中所述,在您第一次致电fixture.detectChanges()之前,该组件尚未完全初始化。

因此,正确的解决方案是在调用fixture.detectChanges()后立即将beforeEach()添加到createComponent块。它可以在您创建夹具后随时添加。这样做将确保组件完全初始化,组件清理也将按预期运行。

答案 1 :(得分:28)

您需要重构您的方法ngOnDestroy,如下所示:

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

答案 2 :(得分:5)

所以我的情况类似,但不完全相同:我只是把它放在这里以防其他人觉得它有用。当我用Jamine / Karma进行单元测试时

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

事实证明,这是因为我没有正确处理我的观察者,他们也没有错误功能。所以修复是添加一个错误函数:

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

答案 3 :(得分:3)

添加@David Brown的回复,下面的代码对我有用。

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

答案 4 :(得分:2)

在类似的情况下,我想在组件本身上下文之外的组件中测试功能。

这对我有用:

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

答案 5 :(得分:1)

就我而言,在每次测试解决问题后都销毁了组件。因此,您可以尝试将其添加到您的describe函数:

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

答案 6 :(得分:1)

您必须做两件事才能解决此错误。

1-在beforeEach()中添加 fixture.detectChanges();
2-您需要在下方添加内容,以便使组件清晰可见。

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

答案 7 :(得分:1)

如@randomPoison所述,当未初始化使用unsubscribe的组件时,将触发错误。但是,对于相应组件的规范文件中的错误,调用fixture.detectChanges()是一种解决方案。

但是我们可能还需要处理创建FooComponent的{​​{1}},并且BarComponent在其BarComponent中使用unsubscribe。必须进行适当的模拟。

我建议使用另一种方法来进行订阅清除,这是声明性的,不会触发此类问题。这是一个示例:

ngOnDestroy

有关此方法的更多信息here

答案 8 :(得分:0)

在我的情况下,错误在模板中。子组件ngDestroy中存在错误,因为我试图设置只读属性而没有被销毁。值得您花时间检查您的子组件是否被正确销毁。

答案 9 :(得分:0)

对我来说,解决此错误的原因是组件的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()));
  }
}