angulartestingjestjstestbed

Default value on @Input causes test problems - angular, abstract class


I got some problems with my tests.

In my case, I have 2 components:

import {Component, Input, OnInit} from '@angular/core';
import {BComponent} from '../b/b.component';

@Component({
    selector: 'a',
    templateUrl: './a.component.html',
    styleUrls: ['./a.component.scss'],
    providers: [{provide: BComponent, useExisting: AComponent}]

})
export class AComponent extends BComponent<any> implements OnInit {
    @Input() foo = true;


    ngOnInit(): void {
        super.ngOnInit();
    }

}
import {Component, Input, OnInit} from '@angular/core';
import {FormBuilder} from '@angular/forms';

@Component({
    template: '',
})
export abstract class BComponent<T> implements OnInit {

    @Input() value: T;

    constructor(
        protected formBuilder: FormBuilder
    ) {
        let a = this.formBuilder.control(null);
    }

    ngOnInit(): void {
    }
}

My test looks like this:

import {ComponentFixture, TestBed} from '@angular/core/testing';
import {AComponent} from './a.component';
import { ReactiveFormsModule} from '@angular/forms';

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

    beforeEach(async () => {

        await TestBed.configureTestingModule({
            declarations: [AComponent],
            imports: [
                ReactiveFormsModule],
        })
            .compileComponents();
    });

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

    it('should create', () => {
        expect(component).toBeTruthy();
    });
});

The problem is the @Input() foo = true; in AComponent. There must be some problems with the default value and ReactiveFormsModule.

Error shows:

AComponent › should create

    TypeError: Cannot read property 'control' of undefined

      12 |         protected formBuilder: FormBuilder
      13 |     ) {
    > 14 |         let a = this.formBuilder.control(null);
         |                                  ^
      15 |
      16 |     }
      17 |

      at new BComponent (src/app/testing/brainfuck/b/b.component.ts:14:34)
      at new AComponent (src/app/testing/brainfuck/a/a.component.ts:258:5)
      at createClass (../packages/core/src/view/provider.ts:273:14)
      at createDirectiveInstance (../packages/core/src/view/provider.ts:142:7)
      at createViewNodes (../packages/core/src/view/view.ts:314:28)
      at createRootView (../packages/core/src/view/view.ts:216:3)
      at callWithDebugContext (../packages/core/src/view/services.ts:641:23)
      at Object.debugCreateRootView [as createRootView] (../packages/core/src/view/services.ts:121:10)
      at ComponentFactory_.Object.<anonymous>.ComponentFactory_.create (../packages/core/src/view/refs.ts:91:27)
      at initComponent (../packages/core/testing/src/test_bed.ts:605:28)
      at ZoneDelegate.Object.<anonymous>.ZoneDelegate.invoke (node_modules/zone.js/bundles/zone-testing-bundle.umd.js:407:30)
      at ProxyZoneSpec.Object.<anonymous>.ProxyZoneSpec.onInvoke (node_modules/zone.js/bundles/zone-testing-bundle.umd.js:3765:43)
      at ZoneDelegate.Object.<anonymous>.ZoneDelegate.invoke (node_modules/zone.js/bundles/zone-testing-bundle.umd.js:406:56)
      at Object.onInvoke (../packages/core/src/zone/ng_zone.ts:405:29)
      at ZoneDelegate.Object.<anonymous>.ZoneDelegate.invoke (node_modules/zone.js/bundles/zone-testing-bundle.umd.js:406:56)
      at Zone.Object.<anonymous>.Zone.run (node_modules/zone.js/bundles/zone-testing-bundle.umd.js:167:47)
      at NgZone.Object.<anonymous>.NgZone.run (../packages/core/src/zone/ng_zone.ts:184:50)
      at TestBedViewEngine.Object.<anonymous>.TestBedViewEngine.createComponent (../packages/core/testing/src/test_bed.ts:609:56)
      at Function.Object.<anonymous>.TestBedViewEngine.createComponent (../packages/core/testing/src/test_bed.ts:236:36)
      at src/app/testing/brainfuck/a/a.component.spec.ts:20:27
      at ZoneDelegate.Object.<anonymous>.ZoneDelegate.invoke (node_modules/zone.js/bundles/zone-testing-bundle.umd.js:407:30)
      at ProxyZoneSpec.Object.<anonymous>.ProxyZoneSpec.onInvoke (node_modules/zone.js/bundles/zone-testing-bundle.umd.js:3765:43)
      at ZoneDelegate.Object.<anonymous>.ZoneDelegate.invoke (node_modules/zone.js/bundles/zone-testing-bundle.umd.js:406:56)
      at Zone.Object.<anonymous>.Zone.run (node_modules/zone.js/bundles/zone-testing-bundle.umd.js:167:47)
      at Object.wrappedFunc (node_modules/zone.js/bundles/zone-testing-bundle.umd.js:4250:34)

If I remove the default value, everything works fine.

Angular version is

 "@angular/core": "12.0.5",

Thanks in advance!


Solution

  • When you add a default property value on AComponent it's forcing the inherited constructor to be called. This occurs because class properties that have an initial value are effectively moved into the constructor during Typescript transpilation

    You probably shouldn't have a constructor on the abstract class, but at the very least you'll need to call it by overriding that constructor in AComponent, passing the FormBuilder as a param

    constructor(
            protected formBuilder: FormBuilder
        ) {
            super(formBuilder)
        }
    

    Note: I didn't get a chance to test this out, but there may be some (other) unexpected behavior with constructors on abstract classes