angular / angular

Deliver web apps with confidence 🚀

Home Page:https://angular.dev

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

TestBed: rethink interactions with the @Input / @output of the root component

mattdistefano opened this issue · comments

I'm submitting a ... (check one with "x")

[x] bug report => search github for a similar issue or PR before submitting
[ ] feature request
[ ] support request => Please do not submit support request here, instead see https://github.com/angular/angular/blob/master/CONTRIBUTING.md#question

TBH I'm not sure if this is a bug, a feature request, or just a product of my ignorance/incomplete docs.

Current behavior

When I have a component using ChangeDetectionStrategy.OnPush, I can create a test fixture via const fixture = TestBed.createComponent(TestComponent) but can't figure out how to force a change detection after altering one of the input properties. Just calling fixture.detectChanges() like I would for a ChangeDetectionStrategy.Default component doesn't seem to do it. I've also tried in conjunction with fixture.changeDetectorRef.markForCheck() but still no luck. However, if I wrap my OnPush component in another component for testing purposes, it works as expected.

Expected behavior

Should be some way to force a change detection on a OnPush component when changing its inputs directly via the component fixture (rather than via a wrapper component).

Minimal reproduction of the problem with instructions

test.component:

import { Component, Input, ChangeDetectionStrategy } from '@angular/core';

@Component({
  selector: 'my-test',
  template: '<p>{{ content }}</p>',
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class TestComponent {
  @Input() content: string;
}

test.component.spec.ts (this test fails w/ OnPush but passes with Default):

import { TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { TestComponent } from './test.component';

describe('Test Component', () => {
  beforeEach(() => {
    TestBed.configureTestingModule({declarations: [TestComponent]});
  });
  it('should render the bound content within a p element', () => {
    const fixture = TestBed.createComponent(TestComponent);
    fixture.detectChanges();
    const p = fixture.debugElement.query(By.css('p'));
    expect((p.nativeElement as HTMLParagraphElement).innerText).toBeFalsy();
    fixture.componentInstance.content = 'Test!';
    fixture.detectChanges();
    expect((p.nativeElement as HTMLParagraphElement).innerText).toEqual('Test!');
  });
});

test.component.wrapped.spec.ts (this passes with OnPush or Default):

import { Component } from '@angular/core';
import { TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { TestComponent } from './test.component';

@Component({
  selector: 'my-wrapper',
  template: '<my-test [content]="content"></my-test>'
})
class WrapperComponent {
  content: string;
}
describe('Test Component (Wrapped)', () => {
  beforeEach(() => {
    TestBed.configureTestingModule({declarations: [TestComponent, WrapperComponent]});
  });

  it('should render the bound content within a p element', () => {
    const fixture = TestBed.createComponent(WrapperComponent);
    fixture.detectChanges();
    const p = fixture.debugElement.query(By.css('p'));
    expect((p.nativeElement as HTMLParagraphElement).innerText).toBeFalsy();
    fixture.componentInstance.content = 'Test!';
    fixture.detectChanges();
    expect((p.nativeElement as HTMLParagraphElement).innerText).toEqual('Test!');
  });
});

What is the motivation / use case for changing the behavior?

Would prefer to use roughly the same testing patterns regardless of which change detection strategy I'm using.

Please tell us about your environment:

  • Angular version: 2.1.0
  • Browser: all
  • Language: TS 2.0.3

Every single one of my components is using OnPush and I haven't noticed this behavior using Angular 2.1.0 at all?? Looking over your provided example nothing jumps out at me though :/

But I do have a theory based on something odd that I noticed. Are you certain that fixture.componentInstance.content is actually there? Check to see if it's visible on fixture.componentInstance in the beforeEach block. I noticed an issue where any @Input I had declared inside a component class implementation that WAS NOT initialized to some value wasn't showing up inside my test.

So for example if I had an Input like so:

@Input() paymentMethods : List<PaymentMethodState>;

It would not show up on my fixture.componentInstance. However, if I had:

@Input() paymentMethods : List<PaymentMethodState> = List();

Then it would be there. Wasted quite a bit of time on that one :/ I noticed that you're not initializing @Input() content: string; to anything so perhaps you're running into the same oddity I was experiencing?? It would explain why it works in a wrapper component though since it's being set to something by the wrapper.

If you can verify the same behavior I'd say we have a reasonable defect here. I hate to have to go back and change all my component @Input declarations now >.<

UPDATE:
Oddly enough, whatever it was that was causing the condition I described above, sigh...seems to have gone away. The only change I made on my end was working with fixture.nativeElement instead of fixture.debugElement ??? No clue

Thanks! I'll take a look at that and let you know. Also glad to hear that this is supposed to work as-is.

@mattdistefano here's an example unit test that seems to behave with Angular 2.1.0:

import {
    ComponentFixture,
    TestBed
} from '@angular/core/testing';
import {Http} from '@angular/http';
import {List} from 'immutable';
import {
    TranslateModule,
    TranslateLoader,
    TranslateStaticLoader
} from 'ng2-translate/ng2-translate';

import {PaymentStepsMethodComponent} from './payment-steps-method.component';
import {SvgIconComponent} from '../../../../shared/components/SvgIcon/svg-icon.component';
import {
    EnumPaymentMethodType,
    PaymentMethodState
} from '../../../../store/Payments/';

// suite of related tests
describe('Payment Steps Method Component', () => {
    let fixture         : ComponentFixture<PaymentStepsMethodComponent>,
        comp            : PaymentStepsMethodComponent,
        element         : any,
        paymentsMethods : List<PaymentMethodState>;

    // setup tasks to perform before each test
    beforeEach(() => {
        // refine the initial testing module configuration
        TestBed.configureTestingModule({
            imports         : [
                TranslateModule.forRoot({
                    provide     : TranslateLoader,
                    useFactory  : (http : Http) => new TranslateStaticLoader(http, 'i18n', '.json'),
                    deps        : [Http]
                })
            ],
            declarations    : [
                SvgIconComponent,
                PaymentStepsMethodComponent
            ]
        });

        // create component fixture
        fixture = TestBed.createComponent(PaymentStepsMethodComponent);

        // grab instance of component class
        comp = fixture.componentInstance;

        // grab DOM representation
        element = fixture.nativeElement;

        // wire up @Input bindings
        paymentsMethods     = List([
            {
                type        : EnumPaymentMethodType.BANK_DRAFT,
                label       : 'Bank Account',
                code        : 0,
                selected    : false
            },
            {
                type        : EnumPaymentMethodType.CHECK,
                label       : 'Send a Check',
                code        : 1,
                selected    : false
            }
        ].map(value => new PaymentMethodState(value)));

        // bind to input
        comp.paymentMethods = paymentsMethods;

        // trigger initial bindings
        fixture.detectChanges();
    });

    // test definitions
    it('should display payment methods', () => {
        // inspect content of *ngFor for child nodes
        expect(element.getElementsByClassName('Method__Card').length).toEqual(2);
    });

    it('should raise paymentMethodSelected event when payment method is chosen', () => {
        let selectedPaymentMethod : PaymentMethodState = undefined;

        // subscribe to paymentMethodSelected event
        comp.paymentMethodSelected.subscribe((method : PaymentMethodState) => {
            // store selected payment method
            selectedPaymentMethod = method;
        });

        // trigger click event on first payment method
        element.getElementsByClassName('Method__Card')[0].click();

        // verify selection
        expect(selectedPaymentMethod).toBe(comp.paymentMethods.get(0));
    });
});

Just a component that displays a couple buttons...user selects payment method by clicking either of the buttons, etc. Component has one @Input paymentMethods and one @Output paymentMethodSelected

Well, setting a default value didn't seem to make a difference, but that suggestion and your example above did help me narrow down the issue a bit. It seems like the first detectChanges() works, but not subsequent calls.

import { TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { TestComponent } from './test.component';

describe('Test Component', () => {
  beforeEach(() => {
    TestBed.configureTestingModule({ declarations: [TestComponent] });
  });
  it('should render the bound content within a p element', () => {
    const fixture = TestBed.createComponent(TestComponent);
    fixture.componentInstance.content = 'Foo';
    fixture.detectChanges();

    const p = fixture.debugElement.query(By.css('p'));

    // this will pass
    expect((p.nativeElement as HTMLParagraphElement).innerText).toEqual('Foo');

    // now change value and rerun change detection
    fixture.componentInstance.content = 'Bar';
    fixture.detectChanges();
    // this will fail - it's still 'Foo'
    expect((p.nativeElement as HTMLParagraphElement).innerText).toEqual('Bar');
  });
});

try moving it inside the beforeEach block instead of calling it inside the first it block. I know that first call to fixture.detectChanges will init the component, call ngOnInit, etc. maybe just some odd timing issue??

Still no luck. The initial value flows through to the DOM but not updates. IIRC the OnPush change detection for a given component more or less runs within the parent component and evaluates its bindings rather than the child component's @Inputs. So if that's correct, it would sort of make sense that the initial detectChanges() would work to initialize the component, but, because there's no bindings within the parent (the fixture in this case), subsequent calls don't do anything.

Do you guys have any test examples where you're updating an @Input after the component is initialized?

Let me try to reproduce what you're seeing in that test example I posted above #strengthinnumbers

Okay I'm definitely seeing the same behavior on my end. The initial binding to the @Input is picked up by the fixture.detectChanges call but not subsequent ones. Changing component's changeDetection to ChangeDetectionStrategy.Default works, just not when it's OnPush

I think that is because you can trigger updating OnPush component only by changing @Input property from parent component.

  1. As workaround you can try the following hack

fixture.changeDetectorRef['internalView']['compView_0'].markAsCheckOnce();

Plunker

  1. Seems it looks very hackly so another way to run manually change detection might be as follows:
const fixture = TestBed.overrideComponent(TestComponent, {set: {host: { "(click)": "dummy" }}}).createComponent(TestComponent);
....
fixture.debugElement.triggerEventHandler('click', null);
fixture.detectChanges();

Plunker Example

All magic happens here

/TestComponent/host.ngfactory.js

View_TestComponent_Host0.prototype.handleEvent_0 = function(eventName,$event) {
  var self = this;
  self.debug(0,0,0);
  self.compView_0.markPathToRootAsCheckOnce(); <=== this line
  var result = true;
  result = (self._TestComponent_0_3.handleEvent(eventName,$event) && result);
  return result;
};
  1. One more option is to have ChangeDetectorRef injected in your test component
export class TestComponent {
  constructor(public cdRef: ChangeDetectorRef) {}
}

then just use component changeDetector

fixture.componentInstance.cdRef.markForCheck();

Plunker Example

Ran across this today spent a few hours scratching my head. At least an example in the Test docs because this can really run you for a loop.

hey @juliemr is there any update on this ?
Is it something with how test is written, or is this for real ?
@alexzuza 's suggestion works btw.

Found same issue. Very frustating! Thanks for the discussion above.

The weirdest thing I found about this is that fixture.changeDetectorRef.markForCheck() doesn't seem to be doing anything in tests

Any ideas when this might get fixed?

I've found the same. I'm not proficient in angular internals. In my opinion, the problem is that ComponentFixture doesn't provide access to the component itself, just to its wrapper (except fixture.componentInstance, which is a reference to component class). All other properties of fixture are that of wrapper component - changeDetectorRef, injector, componentRef, debugElement, elementRef, nativeElement). In particular, changeDetectorRef, injector, and componentRef are most problematic.

As a temporary solution, I do

compRef = fixture.componentRef.hostView['internalView']['compView_0'];

in beforeEach. Then, instead of doing (quite natural) fixture.changeDetectorRef.markForCheck(), I do compRef.changeDetectorRef.markForCheck();.

In my opinion, ComponentFixture should include references to the component itself. If that is not possible for some reason, then there should be these references in addition to those to parent component.

Can somebody fix it? Pretty please...

The provided workarounds don't work in angular 4-rc1, since internalView no longer contains compView_0. It would be nice to get this fixed in 4 final. Otherwise, there would be no way to test OnPush components.

@sebek64 Anyway here is 4-rc1 version :) https://plnkr.co/edit/veBSvE8dw9ucyQneI1Wf?p=preview

fixture.changeDetectorRef['_view'].nodes[0].componentView.state |= (1 << 1);

What indeed is a weird behaviour, I can also reproduce it. But why is that?

Any progress on this ?

Ran into the same problem.
@alexzuza's fix worked out quite well.

commented

I've spent that last 12 hours trying to figure out wtf was going on with my unit tests until I found this >_<

It was also broken for me after upgrade to ng4. @alexzuza soluction solved all failing tests :) Thanks

I don't know if it is an acceptable work around but I use to override the component to reset the changeDetectionStrategy to Default in the component initialization like this:

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

Overriding the change detection described in #12313 (comment) didn't work for me in 4.1.1.

However

fixture.changeDetectorRef['_view'].nodes[0].componentView.state |= (1 << 3);

does.

Having issues with this as well. The way I got around it was to do this:

describe('LoadingIndicatorComponent', () => {
    let component: LoadingIndicatorComponent;
    let fixture: ComponentFixture<LoadingIndicatorComponent>;

    beforeEach(async(() => {
        TestBed.configureTestingModule({
            imports: [CommonModule],
            schemas: [NO_ERRORS_SCHEMA],
            declarations: [LoadingIndicatorComponent]
        })
            .compileComponents();
    }));

    beforeEach(() => {
        fixture = TestBed.createComponent(LoadingIndicatorComponent);
        component = fixture.componentInstance;
    });

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

    it('should show spinner when loading is true', () => {
        component.loading = true;
        fixture.detectChanges();
        const result = fixture.debugElement.query(By.css('md-spinner'));
        expect(result).toBeTruthy();
    });
    
    it('should hide spinner when loading is false', () => {
        component.loading = false;
        fixture.detectChanges();
        const result = fixture.debugElement.query(By.css('md-spinner'));
        expect(result).toBeFalsy();
    });
});

by not calling detectChanges in the beforeEach block my tests began to pass. I do feel like this is not good behavior for tests with @Input()'s or it needs to be better documented. What really threw me for a loop was that the angular cli generates components with the detect changes in the beforeEach() when you specify that you want OnPush change detection.

@kylecannon The real problem is that there is a defect in Angular and it run only once in testing! so in each of your test u put it in the begining and it run the on push once now per each test, that the reason that it work for you.

I found that the simplest and cleanest solution was to use a fake host component to contain the dumb one: https://angular.io/guide/testing#test-a-component-inside-a-test-host-component

This is still happening in the latest cli & angular. Is it really not going to be addressed?

I lost too much time figuring that out... Let's hope it can be solved soon :)

Apparently it's intended behaviour, you need to use a test component in which you test the OnPush component in.

commented

Please let Testbed have a function to handle the change detection of "onPush" components in tests without any workarounds.

The angular docs mention using ComponentFixture.changeDetectorRef as

most valuable when testing a component that has the ChangeDetectionStrategy.OnPush method

however, I did not find it success with it. I created a test component taking a queue from what is recommended here: #14087 (comment).

@juliemr since this is assigned to you, and has been open for almost a year, would you mind offering what the recommended workaround path would be while this is addressed or if this is simply "working as expected"?

I had a bit more hair on my head yesterday. I read & reread & rereread the testing docs, went through the apis, tried everything on StackOverflow, frantically went through 10 videos on ng testing on youtube,... was almost in tears & was about to call my mom to pour my heart out about the bad life choices I've made... boy, am i glad to have stumbled across this. Come on Angular... not one thing in the docs about this. In bird culture, this is considered a dik move.

I similarly was running into an issue using TDD where I had a simple toggle button component that would display a "on" or "off" depending on a boolean that was using ChangeDetectionStrategy.OnPush. I wanted to make sure that the component would both display the correct value and pass to any given parent the proper boolean value. Needless to say I ran in to wall before coming across this article and a few others like it.

I was able to eventually find a solution when I noticed that detectChanges seems to reset every time the component emitted its change. That got me thinking that in conjunction with changeDetection I could also dispatch an event that would emit a value. In this case the event basically simulates the user clicking the button but then I would be able to call detect changes to see the updated value in the test. Sure enough this seems to work at least in the simple case in which I was testing.

Here is my toggle component code:

export interface SliderButtonText {
  on: string;
  off: string;
}

@Component({
  selector: 'conn-slider-toggle',
  templateUrl: './slider-toggle.component.html',
  styleUrls: ['./slider-toggle.component.css'],
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class SliderToggleComponent {

  @Input() isOn: boolean;
  @Input() labelName: string;
  @Input() buttonText: SliderButtonText;
  @Output() sliderEmitter: EventEmitter<boolean>;

  constructor() {
    this.sliderEmitter = new EventEmitter<boolean>();
    if (!this.buttonText) {
      this.buttonText = {
        on: "On",
        off: "Off"
      } as SliderButtonText;
    }
  }

  toggle() {
    if (this.isOn === undefined) {
      this.isOn = false;
    }
    this.isOn = !this.isOn;
    this.sliderEmitter.emit(this.isOn);
  }
}

The test I was able to run to show that this component changes and displays button text correctly is as follows:

describe('SliderToggleComponent', () => {
  let component: SliderToggleComponent;
  let fixture: ComponentFixture<SliderToggleComponent>;
  
beforeEach(async(() => {
    TestBed.configureTestingModule({
      declarations: [SliderToggleComponent]
    })
      .compileComponents();
  }));

  beforeEach(() => {
    fixture = TestBed.createComponent(SliderToggleComponent);

    component = fixture.componentInstance;
    component.isOn = false;
    fixture.detectChanges();
  });

  it('should be able to customize the text being used for the button', () => {
    const newButtonText = {on: "Subscribed", off: "Unsubscribed"}; // setting default conditions
     // Change made but fixture.detectChanges() won't see this unless the emit has 
    component.buttonText = newButtonText;been dispatched

    // Emit gets called when a click event occurs, then detectChanges sees the changes and tests work as expected
    fixture.nativeElement.querySelector('button').dispatchEvent(new Event("click"));
    fixture.detectChanges();
    expect(fixture.nativeElement.querySelector('button').innerText).toBe('Subscribed', "First test failed");

    fixture.nativeElement.querySelector('button').dispatchEvent(new Event("click"));
    fixture.detectChanges();
    expect(fixture.nativeElement.querySelector('button').innerText).toBe('Unsubscribed', "Second test failed");
  });
});

Hope this helps anyone who is struggling with using OnPush for their changeDetection.

commented

Using this hack/workaround by @alexzuza and @fjozsef:

fixture.changeDetectorRef['_view'].nodes[0].componentView.state |= (1 << 3);
fixture.detectChanges();

The test started rendering changes to the inputs, but it's still ignoring lifecycle hooks such as ngOnChanges().

Update: Using a light test host component seems to solve all these issues.

People, stop fighting with this and follow the documentation: https://angular.io/guide/testing#test-a-component-inside-a-test-host-component
This will work seamlessly.

Scratched my head 30minutes. Im guilty though, should had read the documentation for testing first 😄

Will create an example of testing a OnPush Component.

The approach would be to create a container test component as mentioned in the docs.

Basically change detection is available only once when you are testing component with
changeDetection: ChangeDetectionStrategy.OnPush
So if you remove the fixture.detectChanges(); from beforeEach block and use them separately in each test it works like a charm.

Manually creating entire new components and configuring all of that just to unit test the inputs and outputs of an OnPush component feels excessive. It might be the "recommended" way, but it certainly doesn't feel like an ideal developer experience.

@vikerman

The approach would be to create a container test component as mentioned in the docs.

I have a case where even this does not seem to work. I have a component (UserDetailComponent) with no inputs (it is a "router component" i.e. mounted by the router via the RouteConfig) and I use the OnPush change detection strategy. This works fine because I am using observables + async pipe for my template bindings.

In this case even using the container test component approach does not work:

@Component({
    selector: 'test-host',
    template: `<user-detail></user-detail>`
})
class TestHostComponent {}

Calling testHostComponentFixture.detectChanges() does not trigger change detection in the UserDetailComponent

So the only way I can find to actually test my component is to inject the ChangeDetectorRef into UserDetailComponent and expose it publicly so I can invoke userDetailComponent.changeDetectorRef.detectChanges() in my test.

This is unsatisfactory since I now have a dependency in my component which is absolutely redundant apart from its use as a hack for unit testing.

Interesting... this issue has almost 1year and a half and we have no rock solid solution. I've feeling that people just don't write that much tests or don't use onPush and immutable data structures, which is sad 😥...

@michaelbromley hmm,

so your use case looks something like following ?:

@Component({..., onPush})
class UserDetailComponent {
  userId$ = this.route.paramMap.pipe(map(pmap=>pmap.get('id')))
  user$ = this.userId$.pipe(switchMap((userId)=>this.svc.get(userId)))

  constructor(private route: ActivatedRoute, private svc: MySvc){}
}

are you providing those observables with new values in the test properly ? It should work even without container, because CD is run with onPush if Input changed ( not your case ) or something async happens within the component, user clicks somewhere or new observable data are pushed. But I may be wrong ofc.

Also have you tried "dirty" solution, overriding CD to Default ?

Interesting... this issue has almost 1year and a half and we have no rock solid solution. I've feeling that people just don't write that much tests or don't use onPush and immutable data structures, which is sad...

I think you are confusing users and the angular team priorities there ;)

@Hotell Yeah that's pretty much my setup, except that my component also has a FormGroup which gets updated and then I want to check if the "submit" button is enabled:

<button [disabled]="submitIsDisabled()"></button>

In the actual app, the form is of course updated via a user event, which then triggers change detection. On the other hand, my unit test updates the form directly with form.patchValue() and then I want to manually trigger change detection, which is where I am stuck.

I don't really want to override the cd strategy to default, because then I'm no longer testing the actual component. I think I'll just have to directly invoke submitIsDisabled() and test the result.

@michaelbromley You can use method 2 from this comment:

#12313 (comment)

All my components are onPush change detection and your right there are examples where you don't have any @Input() properties to trigger the CD. Plus the whole Host Element thing seems messy for testing purposes.

Similar to the work around that @michaelbromley did to expose the ChangeDetectionRef but since this is only for tests I just turned off TypeScript errors for the next line using // @ts-ignore flag from v2.6 so I could leave the ref private.

import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input } from '@angular/core';
import { TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';

import { WidgetComponent } from './widget.component';

@Component({
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `<my-widget *ngIf="widgetEnabled"></my-widget>`,
});
export class PushyComponent {
  @Input() widgetEnabled = true;

  constructor(private cdr: ChangeDetectorRef) {}

  // methods that actually use this.cdr here...
}

TestBed.configureTestingModule({
  declarations: [ PushyComponent, WidgetComponent ],
}).compileComponents();

const fixture = TestBed.createComponent(PushyComponent);
const component = fixture.componentInstance;
fixture.detectChanges();

expect(component.widgetEnabled).toBe(true);
let el = fixture.debugElement.query(By.directive(WidgetComponent));
expect(el).toBeTruthy();

component.widgetEnabled = false;
// @ts-ignore: for testing we need to access the private cdr to detect changes
component.cdr.detectChanges();
el = fixture.debugElement.query(By.directive(WidgetComponent));
expect(el).toBeFalsy();

Thanks @victornoel. I use a test host component as mentioned. Here is my test:

  it(
    'should show some conditional button',
    fakeAsync(() => {
      const newConfig = ...;
      testHost.cofig = newConfig;
      fixture.detectChanges();
      tick(100);
      fixture.detectChanges();
      const someEl = fixture.nativeElement.querySelector('.some-class');
      expect(someEl).not.toBeNull();
    })
  );

My html has this:

*ngIf="isSomeCondition$ | async"

And my isSomeCondition$ has a debounceTime(100). So I had to do this to let the test pass:

fixture.detectChanges();
tick(100);
fixture.detectChanges();

@soupay said:

Basically change detection is available only once when you are testing component with
changeDetection: ChangeDetectionStrategy.OnPush
So if you remove the fixture.detectChanges(); from beforeEach block and use them separately in each test it works like a charm.

@soupay, your suggestion worked perfectly after trying so many. Sorry you got so many thumb downs, but I added one up. ty!

You can use the injector of the component to get the change detector which is reachable from the fixture:
const cd: ChangeDetectorRef = fixture.componentRef.injector.get(ChangeDetectorRef);
But it's not like a binding update, rather like the component changes its state then calls the change detector, so no life-cycle hook will be called. To properly test with inputs currently the official way is to wrap with a test host component.
A quick example how to do this: https://stackblitz.com/edit/angular-testing-template-h9wks4

@nagytom But the lifecycle hook ngOnChanges would not be called, https://stackblitz.com/edit/angular-testing-template-tmynke?file=app/dummy.component.ts

So I think the best way if you are using lifecycle hooks is to use a wrapper component.

Still not resolved on Angular v 7.1. Cute when you find out about the issue after you've written 50 unit tests haha.

@markomisura yep, lost an entire day. And this doesn't work with @Outputs anyway.

I tried on this issue for half an hour following is the reason and two solutions :

Reason : It fails due to a defect in Angular that fixture.detectChanges() works only the first time with ChangeDetectionStrategy.OnPush

Solution 1 : override a component metadata with overrideComponent in TestBed, override OnPush with Default change detection for testing .

TestBed.overrideComponent(TestComponent, {
changeDetection: ChangeDetectionStrategy.Default
});

Solution 2 : Remove fixture.detectChanges from beforeEach block add it in your every spec.

Both the solutions works !

beforeEach(() => {
fixture = TestBed.createComponent(TodoComponent);
component = fixture.componentInstance;
// fixture.detectChanges();
});
it('should......., () => {
fixture.detectChanges();
});
it('should......., () => {
fixture.detectChanges();
});

I have no idea why fixture.detectChanges won't work in my test case here where I am replacing an object. But I found a way to get the ChangeDetectorRef that will detectChanges.

  it('should set wrapper styles', () => {
    const compiled: HTMLElement = fixture.debugElement.nativeElement;
    const wrapEl: HTMLElement | null = compiled.querySelector('.wrapper');
    if (wrapEl) {
      fixture.detectChanges();
      expect(wrapEl.getAttribute('style')).toBe(`transform: ${component.myStyles.transform};`, 'default style');

      component.myStyles = { transform: 'initial' };
      // detect changes for the OnPush Component
      const debugEl = fixture.debugElement.query(By.directive(WidgetComponent));
      // Need to cast as any until https://github.com/angular/angular/issues/23611 resolved
      const cdr = debugEl.injector.get<ChangeDetectorRef>(ChangeDetectorRef as any);
      cdr.detectChanges();
      expect(wrapEl.getAttribute('style')).toBe('transform: initial;', 'manual style');
    } else {
      fail('div.wrapper element not found');
    }
  });

Except the linter warning that required me to cast the injector.get parameter as any this seems like an acceptable work around for Components that don't have the CDR in the constructor as in my last comment.

I have no idea why fixture.detectChanges won't work in my test case here where I am replacing an object. But I found a way to get the ChangeDetectorRef that will detectChanges.

  it('should set wrapper styles', () => {
    const compiled: HTMLElement = fixture.debugElement.nativeElement;
    const wrapEl: HTMLElement | null = compiled.querySelector('.wrapper');
    if (wrapEl) {
      fixture.detectChanges();
      expect(wrapEl.getAttribute('style')).toBe(`transform: ${component.myStyles.transform};`, 'default style');

      component.myStyles = { transform: 'initial' };
      // detect changes for the OnPush Component
      const debugEl = fixture.debugElement.query(By.directive(WidgetComponent));
      // Need to cast as any until https://github.com/angular/angular/issues/23611 resolved
      const cdr = debugEl.injector.get<ChangeDetectorRef>(ChangeDetectorRef as any);
      cdr.detectChanges();
      expect(wrapEl.getAttribute('style')).toBe('transform: initial;', 'manual style');
    } else {
      fail('div.wrapper element not found');
    }
  });

Except the linter warning that required me to cast the injector.get parameter as any this seems like an acceptable work around for Components that don't have the CDR in the constructor as in my last comment.

After hitting that same problem, we decided that overriding the changing detection in the TestBed means you are testing your component using a behaviour that is different from the one it is actually running on. Not ideal and not really useful particularly when your strategy relies on you knowing when to trigger change detection (or when it happens particularly).

Heavily inspired on the solution quoted, here is an util function we export as a part of a test-util kindda lib we use for this particularities like this.

import { ChangeDetectorRef } from '@angular/core';
import { ComponentFixture } from '@angular/core/testing';

export async function runOnPushChangeDetection<T>(cf: ComponentFixture<T>) {
  const cd = cf.debugElement.injector.get<ChangeDetectorRef>(
    // tslint:disable-next-line:no-any
    ChangeDetectorRef as any
  );
  cd.detectChanges();
  await cf.whenStable();
  return;
}

On the gist there is also an example on how to use it. Hope it helps!
https://gist.github.com/7jpsan/5ac270f19c5d8d715220c06f6ccd123a

We used the code from @psoares-resilient and improved it a bit.
You can copy this code directly into the test-setup.ts or write it into a library and call the function ImproveChangeDetection() in the test-setup.ts.
Now you can always call fixture.detectChanges() as usual.
Note that some tests will be async by this.

import { ChangeDetectorRef, Type } from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing';

function runOnPushChangeDetection<T>(cf: ComponentFixture<T>) {
    return async () => {
        const cd: ChangeDetectorRef = cf.debugElement.injector.get<ChangeDetectorRef>(ChangeDetectorRef as any);
        cd.detectChanges();
        return await cf.whenStable();
    }
}

export const ImproveChangeDetection = () => {
    const originalCreate = TestBed.createComponent;
    TestBed.createComponent = <T>(component: Type<T>) => {
        const componentFixture: ComponentFixture<T> = originalCreate(component);
        componentFixture.detectChanges = runOnPushChangeDetection(componentFixture);
        return componentFixture;
    };
};

the solution works, but for some reason there is a difference in output between the following two statements:

    console.log(fixture.debugElement.query(By.css('.download-button')).attributes.href);
    console.log((fixture.nativeElement as HTMLElement).getElementsByClassName('download-button').item(0).getAttribute('href'));

the first one says undefined, the second gives a nice URL. Both do correctly output the class name (which is not behind a binding). Any idea why that could be?

You can use the injector of the component to get the change detector which is reachable from the fixture:
const cd: ChangeDetectorRef = fixture.componentRef.injector.get(ChangeDetectorRef);
But it's not like a binding update, rather like the component changes its state then calls the change detector, so no life-cycle hook will be called. To properly test with inputs currently the official way is to wrap with a test host component.
A quick example how to do this: https://stackblitz.com/edit/angular-testing-template-h9wks4

This is the right way to do at the moment I think. It avoids overriding changeDetection.

This is the right way to do at the moment I think. It avoids overriding changeDetection.

Based on the code, shouldn't fixture.componentRef.injector.get(ChangeDetectorRef)
and fixture.changeDetectorRef be the same thing?

this.changeDetectorRef = componentRef.changeDetectorRef;

I guess I don't understand why the injected one works but the fixture's does not

Any updates on this ? will be there any fix :(

Any updates on this ? will be there any fix :(

This seems to be working for me with Angular 8:

it('should display the loading bar if isLoading is true', fakeAsync(() => {
        fixture.detectChanges()
        component.isLoading = true;
        fixture.detectChanges();
        flush();
        const loadingBar = fixture.debugElement.query(By.css('[data-test=loading-bar]'));
        expect(loadingBar).toBeTruthy();
    }));

You can move the first fixture.detectChanges() in your beforeEach hook.

Docs team would like engineering input on verification of details in this conversation, if docs should be updated and if so, what we should focus on. Thanks!

This is the right way to do at the moment I think. It avoids overriding changeDetection.

Based on the code, shouldn't fixture.componentRef.injector.get(ChangeDetectorRef)
and fixture.changeDetectorRef be the same thing?

this.changeDetectorRef = componentRef.changeDetectorRef;

I guess I don't understand why the injected one works but the fixture's does not

Can anybody please explain this behavior? It at least feels completely wrong.

@pfeileon i guess it is related to: #14656

@meriturva - I think that clarifies things: the version on the fixture is the component's changeDetectorRef, whereas calling componentRef.injector.get(ChangeDetectorRef) does not return the changeDetectorRef of the fixture component, but instead the component's parent (<div root> probably?)

Is there any reason fixture.detectChanges() shouldn't be calling change detection at that top level?

A good solution which doesn't involve 'hacking' into the changedetectionstrategy of the component is described here:

ngneat/spectator#324

The library is also a very decent improvement for the angular testing experience.

commented

experienced this today. how come such framework struggles with it for 4 years and 9 major releases?

tried on this issue for half an hour following is the reason and two solutions :

Reason : It fails due to a defect in Angular that fixture.detectChanges() works only the first time with ChangeDetectionStrategy.OnPush

Solution 1 : override a component metadata with overrideComponent in TestBed, override OnPush with Default change detection for testing .

TestBed.overrideComponent(TestComponent, {
changeDetection: ChangeDetectionStrategy.Default
});

Solution 2 : Remove fixture.detectChanges from beforeEach block add it in your every spec.

Both the solutions works !

beforeEach(() => {
fixture = TestBed.createComponent(TodoComponent);
component = fixture.componentInstance;

this should be pinned or anything for quick reference. Solution 2 seems the best, easiest and least intrusive way to deal with the problem.

I tried on this issue for half an hour following is the reason and two solutions :

Reason : It fails due to a defect in Angular that fixture.detectChanges() works only the first time with ChangeDetectionStrategy.OnPush

Solution 1 : override a component metadata with overrideComponent in TestBed, override OnPush with Default change detection for testing .

TestBed.overrideComponent(TestComponent, {
changeDetection: ChangeDetectionStrategy.Default
});

Solution 2 : Remove fixture.detectChanges from beforeEach block add it in your every spec.

Both the solutions works !

beforeEach(() => {
fixture = TestBed.createComponent(TodoComponent);
component = fixture.componentInstance;
// fixture.detectChanges();
});
it('should......., () => {
fixture.detectChanges();
});
it('should......., () => {
fixture.detectChanges();
});

You saved my day. It's a really good answer. I prefer the second solution so that the test is the same component.

Ohh ! thank you, My pleasure 😄 @albertzubkowicz , @xuanchuong

I am glad to know that my answer saved your day ! Njoy !!

@mattdistefano

Add fixture.detectChanges(); after the change.

Confirmed on:

Angular CLI: 9.1.12
Node: 14.15.0
OS: darwin x64

Angular: 9.1.12
... animations, cli, common, compiler, compiler-cli, core, forms
... language-service, platform-browser, platform-browser-dynamic
... router
Ivy Workspace: Yes

Package                           Version
-----------------------------------------------------------
@angular-devkit/architect         0.901.12
@angular-devkit/build-angular     0.901.12
@angular-devkit/build-optimizer   0.901.12
@angular-devkit/build-webpack     0.901.12
@angular-devkit/core              9.1.12
@angular-devkit/schematics        9.1.12
@angular/cdk                      9.2.4
@angular/material                 9.2.4
@angular/pwa                      0.901.12
@ngtools/webpack                  9.1.12
@schematics/angular               9.1.12
@schematics/update                0.901.12
rxjs                              6.5.5
typescript                        3.9.7
webpack                           4.42.0

What works is to override component's changeDetection to default AND provide ComponentFixtureAutoDetect as true.

beforeEach(async(() => {
  TestBed.configureTestingModule({
    declarations: [SpinnerComponent],
    providers: [
      {provide: ComponentFixtureAutoDetect, useValue: true}
    ]
  })
  .overrideComponent(SpinnerComponent, {
    set: { changeDetection: ChangeDetectionStrategy.Default }
  })
  .compileComponents();

  fixture = TestBed.createComponent(SpinnerComponent);
  component = fixture.componentInstance;
}));

Now fixture.detectChanges() should work in the tests.

Though this works, keep in mind that you are testing one behaviour, and actually running another. A couple ways of managing it without having to override the change detection strategy (in this issue):

#12313 (comment)
#12313 (comment)

We have used used it since v8 If memory serves me well and still works perfectly (v10.2.x), removing the need to override the component change detection strategy.

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

Writing a wrapper component every time can become annoying.
You can use libraries like @testing-library/angular or @ngneat/spectator that wrap the tested component automatically.

I have no idea why fixture.detectChanges won't work in my test case here where I am replacing an object. But I found a way to get the ChangeDetectorRef that will detectChanges.

  it('should set wrapper styles', () => {
    const compiled: HTMLElement = fixture.debugElement.nativeElement;
    const wrapEl: HTMLElement | null = compiled.querySelector('.wrapper');
    if (wrapEl) {
      fixture.detectChanges();
      expect(wrapEl.getAttribute('style')).toBe(`transform: ${component.myStyles.transform};`, 'default style');

      component.myStyles = { transform: 'initial' };
      // detect changes for the OnPush Component
      const debugEl = fixture.debugElement.query(By.directive(WidgetComponent));
      // Need to cast as any until https://github.com/angular/angular/issues/23611 resolved
      const cdr = debugEl.injector.get<ChangeDetectorRef>(ChangeDetectorRef as any);
      cdr.detectChanges();
      expect(wrapEl.getAttribute('style')).toBe('transform: initial;', 'manual style');
    } else {
      fail('div.wrapper element not found');
    }
  });

Except the linter warning that required me to cast the injector.get parameter as any this seems like an acceptable work around for Components that don't have the CDR in the constructor as in my last comment.

After hitting that same problem, we decided that overriding the changing detection in the TestBed means you are testing your component using a behaviour that is different from the one it is actually running on. Not ideal and not really useful particularly when your strategy relies on you knowing when to trigger change detection (or when it happens particularly).

Heavily inspired on the solution quoted, here is an util function we export as a part of a test-util kindda lib we use for this particularities like this.

import { ChangeDetectorRef } from '@angular/core';
import { ComponentFixture } from '@angular/core/testing';

export async function runOnPushChangeDetection<T>(cf: ComponentFixture<T>) {
  const cd = cf.debugElement.injector.get<ChangeDetectorRef>(
    // tslint:disable-next-line:no-any
    ChangeDetectorRef as any
  );
  cd.detectChanges();
  await cf.whenStable();
  return;
}

On the gist there is also an example on how to use it. Hope it helps!
https://gist.github.com/7jpsan/5ac270f19c5d8d715220c06f6ccd123a

Can this be used with fakeAsync() instead of async()?
In FakeAsync you cannot wait with "await" for the Promise to resolve.
But in my opinion fakeAsync() will do that with tick() automatically (or even without tick())?

with async it is straightforward:

it('test', async() => {
  ...
  await runOnPushChangeDetection(fixture);
  // do some stuff AFTER the fixture is stable
});

But for fakeAsync?

it('test', fakeAsync(() => {
  ....
  runOnPushChangeDetection(fixture);
  // is tick() needed?

  // will it be guaranteed that the fixture is already stable?
  // do some stuff AFTER the fixture is stable
}));

If it helps to understand, fakeAsync is supposed to make all promises synchronous -- all pending tasks are resolved before another statement is executed. You shouldn't need whenStable etc because it "should" always be stable after every statement. (This also makes fakeAsync unsuitable for certain design patterns, but that's another issue.)

I have no idea why fixture.detectChanges won't work in my test case here where I am replacing an object. But I found a way to get the ChangeDetectorRef that will detectChanges.

  it('should set wrapper styles', () => {
    const compiled: HTMLElement = fixture.debugElement.nativeElement;
    const wrapEl: HTMLElement | null = compiled.querySelector('.wrapper');
    if (wrapEl) {
      fixture.detectChanges();
      expect(wrapEl.getAttribute('style')).toBe(`transform: ${component.myStyles.transform};`, 'default style');

      component.myStyles = { transform: 'initial' };
      // detect changes for the OnPush Component
      const debugEl = fixture.debugElement.query(By.directive(WidgetComponent));
      // Need to cast as any until https://github.com/angular/angular/issues/23611 resolved
      const cdr = debugEl.injector.get<ChangeDetectorRef>(ChangeDetectorRef as any);
      cdr.detectChanges();
      expect(wrapEl.getAttribute('style')).toBe('transform: initial;', 'manual style');
    } else {
      fail('div.wrapper element not found');
    }
  });

Except the linter warning that required me to cast the injector.get parameter as any this seems like an acceptable work around for Components that don't have the CDR in the constructor as in my last comment.

After hitting that same problem, we decided that overriding the changing detection in the TestBed means you are testing your component using a behaviour that is different from the one it is actually running on. Not ideal and not really useful particularly when your strategy relies on you knowing when to trigger change detection (or when it happens particularly).
Heavily inspired on the solution quoted, here is an util function we export as a part of a test-util kindda lib we use for this particularities like this.

import { ChangeDetectorRef } from '@angular/core';
import { ComponentFixture } from '@angular/core/testing';

export async function runOnPushChangeDetection<T>(cf: ComponentFixture<T>) {
  const cd = cf.debugElement.injector.get<ChangeDetectorRef>(
    // tslint:disable-next-line:no-any
    ChangeDetectorRef as any
  );
  cd.detectChanges();
  await cf.whenStable();
  return;
}

On the gist there is also an example on how to use it. Hope it helps!
https://gist.github.com/7jpsan/5ac270f19c5d8d715220c06f6ccd123a

Can this be used with fakeAsync() instead of async()?
In FakeAsync you cannot wait with "await" for the Promise to resolve.
But in my opinion fakeAsync() will do that with tick() automatically (or even without tick())?

with async it is straightforward:

it('test', async() => {
  ...
  await runOnPushChangeDetection(fixture);
  // do some stuff AFTER the fixture is stable
});

But for fakeAsync?

it('test', fakeAsync(() => {
  ....
  runOnPushChangeDetection(fixture);
  // is tick() needed?

  // will it be guaranteed that the fixture is already stable?
  // do some stuff AFTER the fixture is stable
}));

Might look a bit weird, but fakeAsync allows you pass an a async function to it. I see no reason the following example wouldn't work, but happy to hear if people have a different view on it:

it('test', fakeAsync(async () => {
  .... doing some setup and  invoking some method that triggers a promise somewhere internally that will resolve in 1s...
  .... call "tick()" so that promise will resolve ...

  // Next, call the "runOnPushChangeDetection()" and you're golden! 
  // Use the same syntax as the non fakeAsync() version.
  await runOnPushChangeDetection(fixture);

 ... Assert on things changing based on what should have happened as you normally would. Trust the CD was called :D ...

}));

An actual example from one of our tests:

  it('should render a badge when there are new records', fakeAsync(async () => {
    component.ngOnInit();
    tick();
    await testUtil.runOnPushChangeDetection();
    expect(fixture.debugElement.query(By.css(`[e2e="tab-1"] [badge].active`))).toBeFalsy();
    expect(fixture.debugElement.query(By.css(`[e2e="tab-2"] [badge].active`))).toBeFalsy();
    expect(fixture.debugElement.query(By.css(`[e2e="tab-3"] [badge].active`))).toBeTruthy();
    expect(fixture.debugElement.query(By.css(`[e2e="tab-4"] [badge].active`))).toBeFalsy();
  }));

As stated rather early in this discussion, creating a wrapping component is the only way currently to test the actual real world functionality of an OnPush component. @alexzuza references this with regard to testing @input(). All of the workarounds in this discussion are subverting the nature of OnPush.

  • Do not override the change detection strategy.
  • Do not be sneaky about getting a reference to the change detector (this could break in subsequent Angular updates)
  • Don't you WANT to test that your OnPush component updates when it should, legitimately test @inputs @outputs and RxJS streams to test change detection properly.

This issue has been automatically locked due to inactivity.
Please file a new issue if you are encountering a similar or related problem.

Read more about our automatic conversation locking policy.

This action has been performed automatically by a bot.