首页 > 解决方案 > 如何修复 Angular 测试中的“ViewDestroyedError:尝试使用已破坏的视图”错误?

问题描述

首先,有一长串类似的问题(12345678等等),但实际上没有一个问题有适用于我的案例的答案,还有许多其他问题没有得到解答一点也不。


说明和源代码链接

以下代码是一个更大的项目的简单、重现的最小示例

npm run test从项目目录运行时

图片:控制台中的错误:未捕获的错误:ViewDestroyedError:尝试使用已破坏的视图


代码

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 建立的测试顺序是这样设置的。出于这个原因,我无法通过更改测试顺序来“解决”这个问题。

问题

标签: angularunit-testingjasminekarma-jasminekarma-runner

解决方案


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

Angular 在每个上运行 TestBed.resetTestingModule() ,beforeEach这基本上会破坏您的 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();
}));

推荐阅读