3

First off, there is a long list of similar questions (1, 2, 3, 4, 5, 6, 7, 8 and more), but none of those actually have an answer applicable to my case and many other have not been answered at all.


Description and links to source code

The following code is a simple Minimal, Reproducible Example of a much bigger project.

When running npm run test from the project directory the

  • expected result:
    1. all the tests to PASS without errors
  • actual behavior:
    1. In Chromium the test commented below as // FAILING TEST! doesn't pass and report Uncaught Error: ViewDestroyedError: Attempt to use a destroyed view (link to travis report in the real project)
    2. In Google Chrome the tests pass, but if you open the console (F12) you see the same error being logged (so this also fails, but Chrome swallows it).

Image: Error in console: Uncaught Error: ViewDestroyedError: Attempt to use a destroyed view


Code

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

});

Additional notes:

The order in which the tests is specified in the posted spec matters! If the tests order is changed all the tests may pass. This is not correct: all the tests should pass independently from the order they are specified. In fact, in the real project the tests are failing randomly: when the tests order established by jasmine is set like this. For this reason it wouldn't work for me to "fix" this by changing the tests order.

Question

  • Why does this error happen and what does it mean?, and more importantly,

  • How can we avoid/fix this error when implementing the tests in ?

lealceldeiro
  • 14,342
  • 6
  • 49
  • 80
  • Not sure it will solve your problem, but i'm not seeing a reason for your spec to call `ngOnInit` (or `detectChanges` again). – The Head Rush Jun 12 '19 at 13:13
  • @TheHeadRush, the answer posted by yurzui has the key. On the other hand, you were correct in your comment. Thanks again. – lealceldeiro Jun 12 '19 at 13:52

1 Answers1

4

You create one BehaviorSubject for all your tests, where you subscribe to it and never unsubscribe so it stays alive while all your tests are being executed.

Angular runs TestBed.resetTestingModule() on each beforeEach which basically destroys your Angular application and causes AppComponent view to be destroyed. But your subscriptions are still there.

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

To solve that problem you have to either unsubscribe in each of tests or don't use the same subject for all your tests:

let behaviorSubject;   
...

beforeEach(async(() => {
  behaviorSubject = new BehaviorSubject<boolean>(false)
  TestBed.configureTestingModule({
    ...
  }).compileComponents();
}));
yurzui
  • 205,937
  • 32
  • 433
  • 399
  • Completely correct! It takes some time to get used to the Observable paradigm when we come from Promises :) Thanks! – lealceldeiro Jun 12 '19 at 13:51