30

I've been tasked with writing tests for a chat app developed with Angular. Below is the snippet of Angular template code I'm currently writing tests for:

<div class="title-menu-container" fxLayoutAlign="center center">
  <button id="save-title-button" mat-icon-button *ngIf="titleInputEdit; else settings">
    <mat-icon class="secondary-text" (click)="saveTitle(titleInput.value)">check</mat-icon>
  </button>
  <ng-template #settings>
    <button mat-icon-button [matMenuTriggerFor]="menu" [disabled]="!(isGroupConversation$ | async)">
      <mat-icon class="secondary-text">settings</mat-icon>
    </button>
  </ng-template>
</div>

Essentially, if the component boolean variable 'titleInputEdit' is true, the save-title-button is displayed, otherwise the settings button is displayed. Here is the test case that is causing problems:

it('save title button should be present', () => {
  component.titleInputEdit = true;
  fixture.detectChanges();
  expect(fixture.nativeElement.querySelector('#save-title-button')).not.toBe(null);
}); 

I simply "mock" the component variable, call .detectChanges(), and then test for the presence of the button. However, the test fails with 'Expected null not to be null.'

Through various console.log calls, I have confirmed that the component.titleInputEdit is correctly set to true but the fixture.nativeElement DOES NOT contain the correct button.

Some things I have noticed:

  • If I move the 'component.titleInputEdit = true' line into my beforeEach and remove it, and the detectChanges() call, from my test, the test passes.

    beforeEach(() => {
      fixture = TestBed.createComponent(TestComponent);
      component = fixture.componentInstance;
      component.titleInputEdit = true
      fixture.detectChanges();
      debugElement = fixture.debugElement;
    });     
    
    it('save title button should be present', () => {
        expect(fixture.nativeElement.querySelector('#save-title-button')).not.toBe(null);
    });
    
  • If I remove the .detectChanges() call from beforeEach(), and leave it in the test case, the test passes.

I'm relatively new to Angular, but I've found instances of people with a similar issue. After trying some of the things recommended in those posts I'm still left scratching my head. What's even stranger is that I have written tests for other Angular components that do almost the exact same thing with no issue.

The example provided in the Angular docs show something very similar as well:

it('should display a different test title', () => {
  component.title = 'Test Title';
  fixture.detectChanges();
  expect(h1.textContent).toContain('Test Title');
});    
Liam
  • 27,717
  • 28
  • 128
  • 190
LHB
  • 843
  • 1
  • 7
  • 9
  • Does this answer your question? [Testing OnPush components in Angular 2](https://stackoverflow.com/questions/40895855/testing-onpush-components-in-angular-2) – Remi Feb 10 '22 at 12:21

6 Answers6

51

It turns out this is due to using ChangeDetectionStrategy.OnPush in the component. Using OnPush only allows you to call .detectChanges() one time, so subsequent calls will fail to do anything. I'm not familiar enough with Angular to fully understand why.

I was able to produce the required behaviour by overriding the ChangeDetectionStrategy in my TestBed configuration.

TestBed.configureTestingModule({
    imports: [],
    declarations: [TestComponent],
    providers: []
  })
    .overrideComponent(TestComponent, {
      set: { changeDetection: ChangeDetectionStrategy.Default }
    })
    .compileComponents();
Liam
  • 27,717
  • 28
  • 128
  • 190
LHB
  • 843
  • 1
  • 7
  • 9
  • 15
    Sorry, the answer is somehow misleading. `OnPush` strategy tells to only mark your component for check when its parent updates one of component inputs. Setting the property manually in test cannot be tracked by Angular, thus no change is seen in DOM. If `titleInputEdit` is an input, then in test you create a host component and bind its property to `titleInputEdit`. Just the statement that `Using OnPush only allows you to call .detectChanges() one time` is not 100% correct. Something should mark your component for check before that. – Eugene Starov Oct 16 '19 at 10:48
  • When you use `onPush` strategy you probably have a `cdr` property in the component so you can use this (and it will work) `component.cdr.detectChanges()` – Erez Shlomo Nov 15 '21 at 12:18
  • 1
    This shouldn't be the most upvoted and accepted answer, new developers arriving on this page will blindly deactivate `OnPush` in their tests. You're not testing your component in real conditions if you do that – Alfred Sep 09 '22 at 12:41
12

Still running on this to this day...

I personally like the overriding ChangeDetectionStrategy solution as it's a one time thing on TestBed setup, but I understand this kind of intrusive solution isn't ideal.

TestBed.configureTestingModule({
    imports: [],
    declarations: [TestComponent],
    providers: []
})
.overrideComponent(TestComponent, {
    set: { changeDetection: ChangeDetectionStrategy.Default }
})
.compileComponents();

There's the "ChangeDetectorRef" solution which I've seen being used on the Component class itself with "changeDetector.markForCheck()" and that is not a good way as your component should not have to adapt to testing, but you can still use this solution, without messing with the actual component, by calling instead of the normal "detectChanges()", as presented here

const cdr = debugEl.injector.get<ChangeDetectorRef>(ChangeDetectorRef as any);
cdr.detectChanges();

And finally there's the simplest solution, at least in my head, and which, curiously, I haven't found any mentions to it. So, you already probably know you can (or end up having to) create a host component to wrap the one you're testing, a lot of blogs, for example, out there showcase the usage of a @ViewChild(ComponentUnderTestComponent) approach which would be perfect if jasmine could actually perceive a change in the child component, but, as it looks like, it doesn't and we are stuck with the normal intuitive approach of just listing the inputs in the host and binding them directly in the template of the testing component, like this:

@Component({
    template: `<component-tag [(ngModel)]="val" [someProperty]="flag"></component-tag>`
})
class HostComponent {
    val: number;
    flag: boolean = false;
}

with that, now you can actually change the value of HostComponent.someProperty and then call detectChanges() and jasmine will perfectly do what it's supposed to and update the DOM with the change:

fixture.componentInstance.readonly = true;
fixture.detectChanges();

Now, if your component goes ahead and have dozens of input attributes, then I guess this isn't really viable, but anyway, I thought I'd throw it out there, enjoy

Josef
  • 2,869
  • 2
  • 22
  • 23
LeGorge
  • 141
  • 1
  • 3
12

As for today Angular has solved this issue in Angular v14.1. You must set the Inputs of your components in your tests by fixture.componentRef.setInput method, so the component will be marked as dirty and detectChanges can be run.

Nardo
  • 309
  • 4
  • 5
5

In my case due to async loading I needed to use fixture.whenStable not just fixture.detectChanges e.g.

it('test description', async(async () => {

    await fixture.whenStable();
}));
Michael
  • 11,571
  • 4
  • 63
  • 61
0

The better way would be to just write a wrapper component in the spec itself with ChangeDetectionStrategy.Default strategy and test the actual component to be tested (i.e. the child component) in the spec through parent's component instance (i.e. the wrapper component).

Child instance and it's native element can be accessed through parent's fixture.debugElement by using fixture.debugElement.query(By.css('your-child-selector'))

Nugu
  • 852
  • 7
  • 10
-1

I know this question is old, but I recently had this same issue where a spinner would constantly spin on the Karma page because change detection only occurred once. The fix for me is whether to call fixture.detectChanges(true) or fixture.autoDetectChanges(true).

beforeEach(() => { 
  fixture = TestBed.createComponent(TestComponent);
  component = fixture.componentInstance;
  component.titleInputEdit = true
  // 'detectChanges' will only test for onPush events: 
  // fixture.detectChanges();

  // 'autoDetectChanges' will continually check for changes until the test is complete.  
  // This is slower, but necessary for certain UI changes
  fixture.autoDetectChanges(true);

  debugElement = fixture.debugElement;
}); 
  • This is somewhat throwing the baby out with the bathwater. There are very good reasons why you would want to use a manual `detectChanges` strategy so simply throwing all that away is not the correct solution – Liam May 15 '20 at 08:10