【问题标题】:Testing Angular component with unsubscribe Error during cleanup of component在清理组件期间使用取消订阅错误测试 Angular 组件
【发布时间】:2017-09-07 02:06:34
【问题描述】:

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

清理组件ApplicationViewComponent时出错 localConsole.(匿名函数)@context.js:232

你知道为什么会这样吗?

我尝试从ngOnDestroy() 方法中删除unsubscribe(),错误消失了。

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

【问题讨论】:

  • AppServiceStub?
  • 只有一个从模拟数据返回可观察的存根。添加到帖子中。
  • 我在测试中得到了完全相同的结果......如果我找到答案,我会告诉你

标签: angular typescript karma-runner karma-jasmine


【解决方案1】:

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

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

【讨论】:

  • 事实上我并不一定希望在我的测试中调用ngOnInit
  • @BlackHoleGalaxy 为什么不呢?如果您在测试时没有完全初始化组件,那么您的测试将无法准确地测试组件的行为。
  • 如果我想在组件本身的上下文之外测试组件中的方法。纯单元测试。之后,我从组件上下文中测试该方法。我将单元测试与集成测试分开。
  • @BlackHoleGalaxy 在这种情况下,您可能不应该使用TestBed 创建夹具。如果我没记错的话,当单元测试完成时,TestBed 会自动在您的组件上调用ngOnDestoy,因此您需要确保组件在测试中完全初始化。如果您的组件具有静态方法,那么您可以单独对其进行单元测试而无需创建夹具,但需要实例的方法应在组件的完全初始化实例上进行测试。
  • 在我的测试用例中添加了fixture.detectChanges(),但我仍然收到错误
【解决方案2】:

你需要重构你的方法 ngOnDestroy 如下:

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

【讨论】:

  • 仅更改代码以使测试正常工作通常会隐藏问题...我认为@excaliburHisShealth 答案应该是公认的
【解决方案3】:

在我的情况下,在每次测试解决问题后销毁组件。因此,您可以尝试将其添加到您的描述函数中:

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

【讨论】:

    【解决方案4】:

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

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

    原来那是因为我没有正确处理我的 observables,而且它们没有错误函数。所以修复是添加一个错误函数:

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

    【讨论】:

      【解决方案5】:

      我处于类似的情况,我想在组件本身的上下文之外测试组件中的函数。

      这对我有用:

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

      【讨论】:

        【解决方案6】:

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

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

        【讨论】:

          【解决方案7】:

          你必须做两件事来解决这个错误。

          1- 在 beforeEach()
          中添加 fixture.detectChanges(); 2 - 您需要在下面添加,以便组件清晰。

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

          【讨论】:

            【解决方案8】:

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

            但我们也可能会处理创建BarComponentFooComponentBarComponent 在其ngOnDestroy 中使用unsubscribe。必须进行适当的模拟。

            我会建议一种不同的订阅清理方法,一种声明式的并且不会触发此类问题的方法。这是一个例子:

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

            更多关于这种方法here

            【讨论】:

              【解决方案9】:

              在我的例子中,错误出现在模板中。子组件 ngDestroy 出现错误,因为我试图设置只读属性,所以它没有被销毁。值得您花时间检查您的子组件是否被正确销毁。

              【讨论】:

                【解决方案10】:

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

                【讨论】:

                • 您能否展示一些代码,您在应用程序中究竟做了什么?
                • @slfan 试试看
                【解决方案11】:

                在我的例子中,我正在测试一个具有多个 @Input 属性的组件。我必须将它设置在 beforeEach 块内到 [] component.xyz = [] (因为它是数组的类型)。这就是问题的根源。

                【讨论】:

                  猜你喜欢
                  • 1970-01-01
                  • 1970-01-01
                  • 1970-01-01
                  • 2021-06-22
                  • 2018-09-29
                  • 1970-01-01
                  • 2019-04-06
                  • 2021-02-25
                  • 1970-01-01
                  相关资源
                  最近更新 更多