Why is change event for reactive form control not firing in Jasmine Angular unit test?

In your example age.setValue(...) actually sets correct value to the input, but it doesn't append ageError to the DOM - BECAUSE there wasn't real/emulated event to mark the control as dirty. This is why the method ageIsError always returns false in this case.

As a workaround I just emulated input event using document.createEvent('Event') and seems like it works fine:

  it('should show error message when age is less than 18', async(() => {
    const customEvent: Event = document.createEvent('Event');
    customEvent.initEvent('input', false, false);
    const ageInput = fixture.debugElement.query(By.css('input#age')).nativeElement;
    ageInput.value = 10;
    ageInput.dispatchEvent(customEvent);

    fixture.detectChanges();

    fixture.whenStable().then(() => {
      let ageError = fixture.debugElement.query(By.css('#ageError')).nativeElement;
      expect(component.ageIsError()).toBe(true);
      expect(ageError.innerText).toContain('Age has errored');
    });
  }));

I also found the fix to your solution - just call age.markAsDirty() before detectChanges:

  it('should show error message when age is less than 18', async(() => {
    let age = component.myForm.controls.age;
    age.setValue('10'); // { emitEvent: true } is by default
    age.markAsDirty(); // add this line

    fixture.detectChanges();

    fixture.whenStable().then(() => {
        let ageError = fixture.debugElement.query(By.css('#ageError')).nativeElement;
        expect(component.ageIsError()).toBe(true);
        expect(ageError.innerText).toContain('Age has errored');
    });
  }));

I've also created a stackblitz example, please check it out as well. Hope these solutions will be helpful for you :)