6

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!
pjpscriv
  • 866
  • 11
  • 20
Botond Béres
  • 16,057
  • 2
  • 37
  • 50
  • No matter what I do I cannot mock out the dependency from `TestFormInputComponentBase`. It's as if the `providers` that I add are ignored. I'm using the sample code provided by Angular for this but it doesn't work. https://angular.io/guide/testing#component-with-a-dependency – Botond Béres Jan 18 '19 at 11:05

3 Answers3

3

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!

dmcgrandle
  • 5,934
  • 1
  • 19
  • 38
  • I see in your update to the original question that you'd already started using an anonymous function to test for the error. :) – dmcgrandle Jan 22 '19 at 00:01
  • Yes, I didn't see that initially because the test didn't get that far into execution. – Botond Béres Jan 22 '19 at 12:31
  • Can it be stubbed out though through the `TestBed`? For this particular case it's fine because I need the dummy component anyway to test the abstract class. But I have several real components as well that extend these abstract base classes. Would not be able to test those directly, would have to extend them with a similar dummy component. That's usable if there is no better option but seems like it should work somehow directly with `TestBed.configureTestingModule` – Botond Béres Jan 22 '19 at 12:35
  • With the way you are using inject within `GenComponentBase`, you need to get a hold of the actual inject in order to mock it because of hierarchical dependency injection. Details [here](https://angular.io/guide/hierarchical-dependency-injection). Without mocking it before you send it down, IDK how you'd get a hold of the correct one at that level. That'll take someone with more experience than me to figure out. :) – dmcgrandle Jan 22 '19 at 22:34
1

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

 constructor() {
    super();
}
King Midas
  • 1,442
  • 4
  • 29
  • 50
surya teja
  • 11
  • 1
0

You want to test GenFormInputComponentBase so why not to test it without TestFormInputComponent

   TestBed.configureTestingModule({
        declarations: [
            GenFormInputComponentBase,
        ],
        providers: [
          {
                provide: LanguageService,
                useValue: {}
          }
        ]
    });

Or with LanguageService providers looks like:

        providers: [
          LanguageService,
          {
                provide: Injector,
                useValue: {}
          }
        ]
Adam Michalski
  • 1,722
  • 1
  • 17
  • 38