Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

In Jasmine unit tests: Can't resolve all parameters for TestFormInputComponentBase

I'm new to unit testing Angular application and I'm trying to test my first component. Actually, I'm trying to test an abstract base class that is used by the actual components, so I've created a simple Component in my spec based on that and I'm using that to test it. But there is a dependency to handle (Injector) and I'm not stubbing it out correctly because when I try to run the test I get this error:

Can't resolve all parameters for TestFormInputComponentBase

But I'm not sure what I have missed? Here is the spec:

import { GenFormInputComponentBase } from './gen-form-input-component-base';
import { Injector, Component } from '@angular/core';
import { TestBed } from '@angular/core/testing';

// We cannot test an abstract class directly so we test a simple derived component
@Component({
    selector: 'test-form-input-component-base'
})
class TestFormInputComponentBase extends GenFormInputComponentBase {}

let injectorStub: Partial<Injector>;

describe('GenFormInputComponentBase', () => {
    let baseClass: TestFormInputComponentBase;
    let stub: Injector;

    beforeEach(() => {
        // stub Injector for test purpose
        injectorStub = {
            get(service: any) {
                return null;
            }
        };

        TestBed.configureTestingModule({
            declarations: [TestFormInputComponentBase],
            providers: [
                {
                    provide: Injector,
                    useValue: injectorStub
                }
            ]
        });

        // Inject both the service-to-test and its stub dependency
        stub = TestBed.get(Injector);
        baseClass = TestBed.get(TestFormInputComponentBase);
    });

    it('should validate required `field` input on ngOnInit', () => {
        expect(baseClass.ngOnInit()).toThrowError(
            `Missing 'field' input in AppFormInputComponentBase`
        );
    });
});

This is the GenFormInputComponentBase class that I'm trying to test:

import { Input, OnInit } from '@angular/core';
import { FormGroup } from '@angular/forms';
import { GenComponentBase } from './gen-component-base';

export abstract class GenFormInputComponentBase extends GenComponentBase
    implements OnInit {
    @Input() form: FormGroup | null = null;
    @Input() field: string | null = null;

    @Input() label: string | null = null;
    @Input() required: boolean | null = null;

    @Input('no-label') isNoLabel: boolean = false;

    ngOnInit(): void {
        this.internalValidateFields();
    }

    /**
     * Validates that the required inputs are passed to the component.
     * Raises clear errors if not, so that we don't get lots of indirect, unclear errors
     * from a mistake in the template.
     */
    protected internalValidateFields(): boolean {
        if (null == this.field) {
            throw Error(`Missing 'field' input in AppFormInputComponentBase`);
        }

        if (null == this.label && !this.isNoLabel) {
            throw Error(
                `Missing 'label' input in AppFormInputComponentBase for '${
                    this.field
                }'.`
            );
        }

        if (null == this.form) {
            throw Error(
                `Missing 'form' input in AppFormInputComponentBase for '${
                    this.field
                }'.`
            );
        }

        return true;
    }
}

And GenComponentBase has the dependency that I'm trying to stub out

import { Injector } from '@angular/core';
import { LanguageService } from 'app/shared/services';

declare var $: any;

export abstract class GenComponentBase {
    protected languageService: LanguageService;

    constructor(injector: Injector) {
        this.languageService = injector.get(LanguageService);
    }

    l(key: string, ...args: any[]) {
        return this.languageService.localize(key, args);
    }
}

Any help would be appreciated. Thanks!

Update:

By adding a constructor to TestFormInputComponentsBase I can stub out the LanguageService and it works fine like that. But if I try to stub out the Injector, it will get ignored and it tries to use the real injector anyway.

@Component({})
class TestFormInputComponent extends GenesysFormInputComponentBase {
    constructor(injector: Injector) {
        super(injector);
    }
}

describe('GenesysFormInputComponentBase (class only)', () => {
    let component: TestFormInputComponent;

    beforeEach(() => {
        TestBed.configureTestingModule({
            providers: [
                TestFormInputComponent,
                {
                    provide: Injector,
                    useObject: {}
                }
            ]
        });

        component = TestBed.get(TestFormInputComponent);
    });

    it('should validate required field inputs on ngOnInit', () => {
        expect(() => component.ngOnInit()).toThrowError(
            `Missing 'field' input in GenesysFormInputComponentBase.`
        );
    });
});

I would expect to get some error due to the fact that the mock/stub injector provided is an empty object. But I get an error from the real injector. Can the injector simply not be mocked?

    Error: StaticInjectorError(DynamicTestModule)[LanguageService]: 
    StaticInjectorError(Platform: core)[LanguageService]: 
    NullInjectorError: No provider for LanguageService!
like image 718
Botond Béres Avatar asked Oct 16 '22 08:10

Botond Béres


2 Answers

There are many different ways to approach this, but you can stub it right in the call to super() in your TestFormInputComponent, like so:

class TestFormInputComponent extends GenFormInputComponentBase {
      constructor() {
          let injectorStub: Injector = { get() { return null } };
          super(injectorStub);
    }
}

Also, you need to change how you are testing for an error thrown in a function. See a detailed discussion here. As you can see in that discussion there are many ways to do this as well, here is a simple one using an anonymous function:

it('should validate required `field` input on ngOnInit', () => {
    expect(() => baseClass.ngOnInit()).toThrowError(
        `Missing 'field' input in AppFormInputComponentBase`
    );
});

Here is a working StackBlitz that shows this running. I also added another test to show an error-free initialization.

I hope this helps!

like image 92
dmcgrandle Avatar answered Oct 19 '22 00:10

dmcgrandle


Yes, writing a constructor() {} and calling super() within constructor solves that problem if your class doesn't have @injectable() decorator.

 constructor() {
    super();
}
like image 42
surya teja Avatar answered Oct 18 '22 23:10

surya teja