是否可以为单元测试模拟自定义 Angular 2 Material SVG 图标?

Is it possible to mock custom Angular 2 Material SVG icons for unit tests?

在我的应用程序的根组件中,我正在为 md-icon 定义自定义 SVG 图标。当对显示自定义图标的组件进行单元测试时,出现错误。看来错误可能是由于我的根组件在我的子单元测试中没有 used/initialized。

有没有办法在设置测试模块时模拟或添加这些自定义图标(或md-icon)?我会简单地在我正在测试的组件中定义图标,但我知道其他组件也需要它们。

错误:

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

完整错误:

从模板中删除自定义图标解决了错误。


我的模板正在使用这样的自定义图标:

<md-icon svgIcon="vip">vip</md-icon>

根组件像这样初始化图标:

this.iconRegistry.addSvgIcon(
    'vip',
    this.sanitizer.bypassSecurityTrustResourceUrl('assets/icons/vip.svg') as string,
);

我这样设置测试组件:

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();
}));

版本

回答我自己的问题:

在经历了很多 trial/error 项目后,例如模拟 MdIconRegistry 或使用 componentOverride() 等等,但没有成功,我不再在我的测试中使用共享模块。

相反,我使用 class 的模拟版本直接在我的测试模块中声明 MdIcon 组件。

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 只是一个匹配选择器的空白 class:

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.

这是一个迟到的答案。以防万一有人遇到这个问题,除了 OP 的解决方案(这也很好)之外还有其他选择:

使用 forRoot() 导入 MaterialModule

TestBed.configureTestingModule({
        declarations: [
            TestComponent
        ],
        imports: [SharedModule, MaterialModule.forRoot()],
        providers: [{ provide: Router, useValue: routerStub }]
    });
    TestBed.compileComponents();

获取注入的MdIconRegistry和DomSanitizer

let iconRegistry = TestBed.get(MdIconRegistry);
let sanitizer = TestBed.get(DomSanitizer);

像在普通应用中一样配置它们

iconRegistry.addSvgIcon( 'some-icon',
sanitizer.bypassSecurityTrustResourceUrl('assets/img/some-icon.svg'));

我能够使用 overrideModule method to stub MdIcon. The documentation is sparse but I was able to find a GitHub issue,其中 Angular 团队成员讨论如何覆盖声明。我们的想法是从 MdIconModule 中删除该组件,以便我们可以声明我们自己的模拟图标组件。

  beforeEach(async(() => {
    TestBed.configureTestingModule({
      declarations: [ TestedComponent ],
      imports: [
        RouterTestingModule.withRoutes([]),
        SharedModule,
      ],
    })
    .overrideModule(MdIconModule, {
      remove: {
        declarations: [MdIcon],
        exports: [MdIcon]
      },
      add: {
        declarations: [MockMdIconComponent],
        exports: [MockMdIconComponent]
      }
    })
    .compileComponents();
  }));

MockMdIconComponent的定义很简单

@Component({
  selector: 'md-icon',
  template: '<span></span>'
})
class MockMdIconComponent {
  @Input() svgIcon: any;
  @Input() fontSet: any;
  @Input() fontIcon: any;
}

我使用这种方法是因为我没有单独导入 Material 模块,而且我不希望我的测试必须进行 Http 调用才能获取 svg 图标。 MockMdIconComponent 可以在测试模块中声明,但我选择在模块覆盖中 declare/export 它,以便我可以将对象提取到测试助手中。

基于@Chic 的回答,因为我到处都有图标,所以我制作了一个测试平台补丁:

import {TestBed} from '@angular/core/testing';
import {MatIconModule, MatIcon} from '@angular/material/icon';
import {Component, Input} from '@angular/core';

export function PatchTestBedMatIcons() {
  const original = TestBed.configureTestingModule;
  TestBed.configureTestingModule = (moduleDef) => {
    return original(moduleDef)
      .overrideModule(MatIconModule, {
        remove: {
          declarations: [MatIcon],
          exports: [MatIcon]
        },
        add: {
          declarations: [MockMatIconComponent],
          exports: [MockMatIconComponent]
        }
      });
  };
}

@Component({
  selector: 'mat-icon',
  template: '<span></span>'
})
class MockMatIconComponent {
  @Input() svgIcon: any = null;
  @Input() fontSet: any = null;
  @Input() fontIcon: any = null;
}

然后在你的组件测试中简单地:

import {PatchTestBedMatIcons} from 'src/app/patchTestBedIcons';
PatchTestBedMatIcons();