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!
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!
Yes, writing a constructor() {}
and calling super()
within constructor solves that problem if your class doesn't have @injectable()
decorator.
constructor() {
super();
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With