【问题标题】:How to fix "ViewDestroyedError: Attempt to use a destroyed view" error in Angular tests?如何修复 Angular 测试中的“ViewDestroyedError:尝试使用已破坏的视图”错误?
【发布时间】:2019-06-12 12:35:44
【问题描述】:

首先,有很多类似的问题(12345678 等等),但实际上没有一个答案适用于我的案例,而且许多其他的根本没有答案。


说明和源代码链接

以下代码是much bigger project 的简单Minimal, Reproducible Example

当从项目目录运行npm run test

  • 预期结果:
    1. 所有测试通过没有错误
  • 实际行为:
    1. 在 Chromium 中,下面注释为 // FAILING TEST! 的测试未通过并报告 Uncaught Error: ViewDestroyedError: Attempt to use a destroyed view (link to travis report in the real project)
    2. 在 Google Chrome 中测试通过,但如果您打开控制台 (F12),您会看到记录了相同的错误(因此这也失败了,但 Chrome 将其吞下)。


代码

app.component.ts

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit, OnDestroy {
  hide: boolean = false;
  someSubscription: Subscription;

  constructor(private appServiceService: AppServiceService) { }
  
  ngOnInit() {
    this.someSubscription = this.appServiceService.shouldHide().subscribe(shouldHide => this.hide = shouldHide);
  }
  ngOnDestroy() {
    this.someSubscription.unsubscribe();
  }
}

app.component.html

<div class="row" id="jmb-panel" *ngIf="!hide">
  Hello
</div>

app.component.spec

describe('AppComponent', () => {
  let component: AppComponent;
  let componentDe: DebugElement;
  let fixture: ComponentFixture<AppComponent>;
  const behaviorSubject = new BehaviorSubject<boolean>(false);

  const appServiceStub = {
    shouldHide: () => { spy.shouldHideSpyFn(); return behaviorSubject.asObservable() }
  };
  const spy = { shouldHideSpyFn: () => { } };
  let spyShouldHide: jasmine.Spy;

  beforeEach(async(() => {
    TestBed.configureTestingModule({
      declarations: [AppComponent],
      schemas: [NO_ERRORS_SCHEMA],
      providers: [{ provide: AppServiceService, useValue: appServiceStub }]
    }).compileComponents();
  }));

  beforeEach(() => {
    behaviorSubject.next(false);
    fixture = TestBed.createComponent(AppComponent);
    component = fixture.componentInstance;
    componentDe = fixture.debugElement;
    fixture.detectChanges();
    spyShouldHide = spyOn(spy, 'shouldHideSpyFn');
  });

  it('should call AppServiceService#shouldHide on init', () => {
    component.ngOnInit();
    fixture.detectChanges();
    expect(spyShouldHide).toHaveBeenCalledTimes(1);
  });

  it('should not render div if the AppServiceService#shouldHide observables emit true', () => {
    appServiceStub.shouldHide().subscribe((li) => {
      if (li) {
        fixture.detectChanges();
        expect(componentDe.query(By.css('#jmb-panel'))).toBeNull();
      }
    });
    behaviorSubject.next(true);
  });

    // FAILING TEST!    
  it('should render div if the AppServiceService#shouldHide observables emit true', () => {
    appServiceStub.shouldHide().subscribe((li) => {
      if (!li) {
        fixture.detectChanges();
        expect(componentDe.query(By.css('#jmb-panel'))).not.toBeNull('Jumbotron panel should not be null');
      }
    });
    behaviorSubject.next(false);
  });

  it('should create', () => {
    fixture.detectChanges();
    expect(component).toBeTruthy();
  });

});

补充说明:

在发布的规范中指定测试的顺序很重要!如果更改测试顺序,则所有测试都可能通过。这是不正确的:所有测试都应该独立于它们指定的顺序通过。事实上,在实际项目中,测试是随机失败的:当 jasmine 建立的测试顺序是这样设置的。因此,我无法通过更改测试顺序来“解决”这个问题。

问题

  • 为什么会发生这个错误,这是什么意思?,更重要的是,

  • 中实施测试时,我们如何避免/修复此错误?

【问题讨论】:

  • 不确定它是否能解决您的问题,但我没有看到您的规范调用ngOnInit(或再次调用detectChanges)的理由。
  • @TheHeadRush,yurzui 发布的答案有关键。另一方面,您的评论是正确的。再次感谢。

标签: angular angular unit-testing jasmine karma-jasmine karma-runner


【解决方案1】:

您为所有测试创建一个 BehaviorSubject,您可以在其中订阅它并且永远不会取消订阅,以便在执行所有测试时它保持活动状态。

Angular 在每个 beforeEach 上运行 TestBed.resetTestingModule(),这基本上会破坏您的 Angular 应用程序并导致 AppComponent 视图被破坏。但是您的订阅仍然存在。

beforeEach(() => {
  behaviorSubject.next(false); (3) // will run all subscriptions from previous tests
  ...
});
...

// FAILING TEST!
it('should render jumbotron if the user is not logged in', () => {
  appServiceStub.shouldHide().subscribe((li) => { // (1)

    // will be executed 
    1) once you've subscribed since it's BehaviorSubject
    2) when you call behaviorSubject.next in the current test
    3) when you call behaviorSubject.next in beforeEach block 
         which causes the error since AppComponent has been already destoryed


    fixture.detectChanges();
    ....      
  });
  behaviorSubject.next(false); // (2)
});

要解决这个问题,您必须在每个测试中取消订阅,或者不要在所有测试中使用相同的主题:

let behaviorSubject;   
...

beforeEach(async(() => {
  behaviorSubject = new BehaviorSubject<boolean>(false)
  TestBed.configureTestingModule({
    ...
  }).compileComponents();
}));

【讨论】:

  • 完全正确!当我们来自 Promises 时,需要一些时间来适应 Observable 范式 :) 谢谢!
猜你喜欢
  • 1970-01-01
  • 2019-10-23
  • 2017-10-23
  • 2017-12-13
  • 2023-03-14
  • 1970-01-01
  • 1970-01-01
  • 2016-10-17
  • 1970-01-01
相关资源
最近更新 更多