How to fix "ViewDestroyedError: Attempt to use a destroyed view" error in Angular tests?

How to fix "ViewDestroyedError: Attempt to use a destroyed view" error in Angular tests?

首先,有一长串类似的问题(1, 2, 3, 4, 5, , 7, 和更多),但其中 none 实际上有适用于我的情况的答案,还有许多其他问题尚未在全部.


描述和源代码链接

下面的代码很简单Minimal, Reproducible Example of a much bigger project.

当运行从项目目录npm run test


代码

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 建立的测试顺序是这样设置的。因此,我无法通过更改测试顺序来“修复”此问题。

问题

您为所有测试创建了一个 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();
}));