In my app's root component, I am defining custom SVG icons for md-icon
. When unit testing a component that displays the custom icon I get an error. It seems that the error is likely due to the fact that my root component is not being used/initialized in my child unit test.
Is there a way to mock or add these custom icons (or md-icon
) when setting up the test module? I would simply define the icons in the component I am testing, but I know other components will need them also.
The error:
Uncaught Error: Error in ./CustomerComponent class CustomerComponent - inline template:34:19 caused by: __WEBPACK_IMPORTED_MODULE_4_rxjs_Observable__.Observable.throw is not a function
Full error:
Removing the custom icons from the template solves the error.
My template is using the custom icons like this:
<md-icon svgIcon="vip">vip</md-icon>
And the root component initializes the icons like this:
this.iconRegistry.addSvgIcon(
'vip',
this.sanitizer.bypassSecurityTrustResourceUrl('assets/icons/vip.svg') as string,
);
I set up the test component like this:
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
SharedModule,
CoreModule,
FormsModule,
ReactiveFormsModule,
],
providers: [
{
provide: Router,
useClass: class {
navigate = jasmine.createSpy('navigate');
},
},
{
provide: ActivatedRoute,
useValue: {
data: {
subscribe: (fn: (value: Data) => void) => fn({
customer: CUSTOMER,
company: COMPANY,
}),
},
},
},
{
provide: UtilityService,
useClass: UtilityServiceMock,
},
// etc...
],
declarations: [
CustomerComponent,
],
schemas: [
CUSTOM_ELEMENTS_SCHEMA,
],
})
.compileComponents();
}));
Introduction. Mocking is a great idea for testing Angular apps because it makes maintenance easier and helps reduce future bugs. There are a few complex tools, such as XUnit, for mocking an Angular CLI project. You can execute the mocking methods described in this guide only if you use vanilla Jasmine + Angular Testbed ...
A mock component in Angular tests can be created by MockComponent function. The mock component respects the interface of its original component, but all its methods are dummies. To create a mock component, simply pass its class into MockComponent function.
I was able to use the overrideModule
method to stub MdIcon. The documentation is sparse but I was able to find a GitHub issue where Angular team members discuss how to override declarations. The idea is to remove the component from the MdIconModule
so that we can declare our own mock icon component.
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ TestedComponent ],
imports: [
RouterTestingModule.withRoutes([]),
SharedModule,
],
})
.overrideModule(MdIconModule, {
remove: {
declarations: [MdIcon],
exports: [MdIcon]
},
add: {
declarations: [MockMdIconComponent],
exports: [MockMdIconComponent]
}
})
.compileComponents();
}));
The MockMdIconComponent
is defined very simply
@Component({
selector: 'md-icon',
template: '<span></span>'
})
class MockMdIconComponent {
@Input() svgIcon: any;
@Input() fontSet: any;
@Input() fontIcon: any;
}
I used this approach because I am not importing the Material modules individually and I did not want my test to have to make Http calls to get the svg icons. The MockMdIconComponent
could be declared in the testing module but I chose to declare/export it in the module override so that I could extract the object into a test helper.
Answering my own question:
After much trial/error with items like mocking the MdIconRegistry
or using componentOverride()
etc with no luck I no longer use a shared module within my tests.
Instead, I declare the MdIcon component directly in my testing module using a mock version of the class.
describe(`CustomerComponent`, () => {
let component: CustomerComponent;
let fixture: ComponentFixture<CustomerComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
FormsModule,
ReactiveFormsModule,
MdButtonModule,
],
providers: [
OVERLAY_PROVIDERS,
{
provide: Router,
useClass: class {
navigate = jasmine.createSpy('navigate');
},
},
{
provide: ActivatedRoute,
useValue: {
data: {
subscribe: (fn: (value: Data) => void) => fn({
customer: customer,
company: COMPANY,
}),
},
params: Observable.of({
customerId: customerId,
}),
},
},
],
declarations: [
CustomerComponent,
// Declare my own version of MdIcon here so that it is available for the CustomerComponent
MdIconMock,
],
});
fixture = TestBed.createComponent(CustomerComponent);
component = fixture.componentInstance;
fixture.detectChanges();
}));
it(`should exist`, () => {
expect(component).toBeTruthy();
});
});
MdIconMock is simply a blank class that matches the selectors:
import { Component } from '@angular/core';
@Component({
template: '',
// tslint:disable-next-line
selector: 'md-icon, mat-icon',
})
// tslint:disable-next-line
export class MdIconMock {
}
Note: Due to TSLint rules that specify the prefix/format of class names/selectors I needed to disable TSLint for this mock.
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