无法在Angular测试中模拟子组件

时间:2019-11-19 20:35:56

标签: angular unit-testing

我在Angular中有一个测试套件,我试图在其中测试具有多个子组件的组件。我需要断言在调用父方法时会调用子方法。

我正在实现的类如下:

export class UserFiltersComponent implements OnInit, OnDestroy {
  @Output() filtersChange: EventEmitter<any> = new EventEmitter();
  @ViewChild('tooltip', {static: false}) applyTooltip: MatTooltip;
  @ViewChild('filterSearch', {static: false}) searchComponent: SearchComponent;
  @ViewChild('filterTitle', {static: false}) titleComponent: TitleComponent;
  @ViewChild('filterSkills', {static: false}) skillsComponent: SkillsComponent;
  @ViewChild('filterEnglish', {static: false}) englishComponent: EnglishLevelComponent;
  @ViewChild('filterLocation', {static: false}) locationComponent: LocationComponent;
  @ViewChild('filterEducation', {static: false}) educationComponent: EducationComponent;
  @ViewChild('filterWork', {static: false}) workComponent: WorkComponent;
  @ViewChild('filterSocial', {static: false}) socialProfileComponent: SocialProfileComponent;
  @ViewChild('filterRegistered', {static: false}) registeredComponent: RegisteredComponent;
  @ViewChild('filterInvitation', {static: false}) invitationComponent: InvitationsComponent;
.
.
.
populateFilters(result: any): void {
    const filter = {
      id: result.id,
      name: result.name,
      values: result.value
    };
    this.filters = filter;
    this.searchComponent.populate(filter.values.name);
    this.titleComponent.populate(filter.values.titles);
    this.skillsComponent.populate(filter.values.skills);
    this.englishComponent.populate(filter.values.englishLevel);
    this.locationComponent.populate(filter.values.locations);
    this.educationComponent.populate(filter.values.educations);
    this.workComponent.populate(filter.values.works);
    this.socialProfileComponent.populate(filter.values.profiles);
    this.registeredComponent.populate(filter.values.registeredExact, filter.values.registeredGte, filter.values.registeredLte);
    this.invitationComponent.populate(filter.values.invitationsExact, filter.values.invitationsLte, filter.values.invitationsGte);
  }

我为此代码编写的测试是:

import {SearchComponent} from '@feature/administration/user/user-filters/search';
import {TitleComponent} from '@feature/administration/user/user-filters/title';
import {SkillsComponent} from '@feature/administration/user/user-filters/skills';
import {EnglishLevelComponent} from '@feature/administration/user/user-filters/english-level';
import {LocationComponent} from '@feature/administration/user/user-filters/location';
import {WorkComponent} from '@feature/administration/user/user-filters/work';
import {EducationComponent} from '@feature/administration/user/user-filters/education';
import {SocialProfileComponent} from '@feature/administration/user/user-filters/social-profile';
import {RegisteredComponent} from '@feature/administration/user/user-filters/registered';
import {InvitationsComponent} from '@feature/administration/user/user-filters/invitations';
.
.
.
beforeEach(async(() => {
    TestBed.configureTestingModule({
      imports: [
        BrowserAnimationsModule,
        MatTooltipModule,
        TranslateTestingModule
      ],
      declarations: [
        UserFiltersComponent,
        SearchComponent
      ],
      providers: [
        {
          provide: UserFiltersService,
          useClass: UserFiltersServiceStub
        },
        {
          provide: PageLoadingService,
          useClass: PageLoadingServiceStub
        },
        {
          provide: AuthenticationService,
          useClass: AuthenticationServiceStub
        },
        {
          provide: UserService,
          useClass: UserServiceStub
        },
        {
          provide: MatDialog,
          useClass: MatDialogStub
        },
      ],
      schemas: [
        NO_ERRORS_SCHEMA
      ]
    })
      .compileComponents();
  }));
.
.
.
it('should populate the filters', () => {
    const filter = {
      id: '12345',
      name: 'filters test',
      value: {
        name: 'search',
        titles: [''],
        skills: [''],
        englishLevel: 1,
        locations: [''],
        educations: [''],
        works: [''],
        profiles: [''],
        registeredExact: null,
        registeredGte: null,
        registeredLte: null,
        invitationsExact: null,
        invitationsLte: null,
        invitationsGte: null
      }
    };
    spyOn(component.searchComponent, 'populate');
    component.populateFilters(filter);
    expect(component.searchComponent.populate).toHaveBeenCalled();
  });

到目前为止,一切正常。问题是当我尝试添加其余的子组件时:

declarations: [
        UserFiltersComponent,
        SearchComponent,
        TitleComponent
      ],
.
.
.
it('should populate the filters', () => {
    const filter = {
      id: '12345',
      name: 'filters test',
      value: {
        name: 'search',
        titles: [''],
        skills: [''],
        englishLevel: 1,
        locations: [''],
        educations: [''],
        works: [''],
        profiles: [''],
        registeredExact: null,
        registeredGte: null,
        registeredLte: null,
        invitationsExact: null,
        invitationsLte: null,
        invitationsGte: null
      }
    };
    spyOn(component.searchComponent, 'populate');
    spyOn(component.titleComponent, 'populate');
    component.populateFilters(filter);
    expect(component.searchComponent.populate).toHaveBeenCalled();
    expect(component.titleComponent.populate).toHaveBeenCalled();
  });

然后我遇到以下错误:

Summary of all failing tests
 FAIL  src/app/feature/administration/user/user-filters/user-filters.component.spec.ts (10.102s)
  ● UserFiltersComponent › should create

    NullInjectorError: StaticInjectorError(DynamicTestModule)[TitleComponent -> FormBuilder]: 
      StaticInjectorError(Platform: core)[TitleComponent -> FormBuilder]: 
        NullInjectorError: No provider for FormBuilder!

      at NullInjector.get (../packages/core/src/di/injector.ts:44:21)
      at resolveToken (../packages/core/src/di/injector.ts:337:20)
      at tryResolveToken (../packages/core/src/di/injector.ts:279:12)
      at StaticInjector.get (../packages/core/src/di/injector.ts:168:14)
      at resolveToken (../packages/core/src/di/injector.ts:337:20)
      at tryResolveToken (../packages/core/src/di/injector.ts:279:12)
      at StaticInjector.get (../packages/core/src/di/injector.ts:168:14)
      at resolveNgModuleDep (../packages/core/src/view/ng_module.ts:125:25)
      at NgModuleRef_.get (../packages/core/src/view/refs.ts:507:12)
      at resolveDep (../packages/core/src/view/provider.ts:423:43)
      at createClass (../packages/core/src/view/provider.ts:277:11)
      at createDirectiveInstance (../packages/core/src/view/provider.ts:136:20)
      at createViewNodes (../packages/core/src/view/view.ts:303:28)
      at callViewAction (../packages/core/src/view/view.ts:636:7)
      at execComponentViewsAction (../packages/core/src/view/view.ts:559:7)
      at createViewNodes (../packages/core/src/view/view.ts:331:3)
      at createRootView (../packages/core/src/view/view.ts:210:3)
      at callWithDebugContext (../packages/core/src/view/services.ts:630:23)
      at Object.debugCreateRootView [as createRootView] (../packages/core/src/view/services.ts:122:10)
      at ComponentFactory_.create (../packages/core/src/view/refs.ts:93:27)
      at initComponent (../../packages/core/testing/src/test_bed.ts:589:28)
      at ZoneDelegate.Object.<anonymous>.ZoneDelegate.invoke (node_modules/zone.js/dist/zone.js:391:26)
      at ProxyZoneSpec.Object.<anonymous>.ProxyZoneSpec.onInvoke (node_modules/zone.js/dist/proxy.js:129:39)
      at ZoneDelegate.Object.<anonymous>.ZoneDelegate.invoke (node_modules/zone.js/dist/zone.js:390:52)
      at Object.onInvoke (../packages/core/src/zone/ng_zone.ts:273:25)
      at ZoneDelegate.Object.<anonymous>.ZoneDelegate.invoke (node_modules/zone.js/dist/zone.js:390:52)
      at Zone.Object.<anonymous>.Zone.run (node_modules/zone.js/dist/zone.js:150:43)
      at NgZone.run (../packages/core/src/zone/ng_zone.ts:171:50)
      at TestBedViewEngine.createComponent (../../packages/core/testing/src/test_bed.ts:593:56)
      at Function.TestBedViewEngine.createComponent (../../packages/core/testing/src/test_bed.ts:232:36)
      at beforeEach (src/app/feature/administration/user/user-filters/user-filters.component.spec.ts:131:23)
      at ZoneDelegate.Object.<anonymous>.ZoneDelegate.invoke (node_modules/zone.js/dist/zone.js:391:26)
      at ProxyZoneSpec.Object.<anonymous>.ProxyZoneSpec.onInvoke (node_modules/zone.js/dist/proxy.js:129:39)
      at ZoneDelegate.Object.<anonymous>.ZoneDelegate.invoke (node_modules/zone.js/dist/zone.js:390:52)
      at Zone.Object.<anonymous>.Zone.run (node_modules/zone.js/dist/zone.js:150:43)
      at Object.testBody.length (node_modules/jest-preset-angular/zone-patch/index.js:52:27)

我意识到构造器正在尝试构建FormBuilder时出现了我做错的事情,但这并未被嘲笑。我的观点是,我不需要嘲笑这一点。我只需要断言调用了“填充”方法,因为在每个组件中都创建了测试以测试填充方法。我该如何模拟这个子组件以断言该方法已被调用?

3 个答案:

答案 0 :(得分:2)

恕我直言...如果要对方法进行单元测试,请尝试将该方法与外部依赖项隔离。这样,您可以解决复杂性(例如,不必要的喷油器初始化)和潜在的错误。在这种情况下,模拟是我的首选。因此,在这种情况下,我建议以这种方式提出单独的关注点:

首先,模拟类:

class TitleComponentStub {
  populate = () => {
  };
}

然后将提供程序添加到TitleComponent

        ,
        {
          provide: TitleComponent,
          useClass: TitleComponentStub
        },

在测试内部添加一行:

it('should populate the filters', () => {
    component.titleComponent = TestBed.get(TitleComponent); // <- THIS LINE
    const filter = {
      id: '12345',
      name: 'filters test',
[...]

从此处删除TitleComponent

declarations: [
        UserFiltersComponent,
        SearchComponent,
        TitleComponent
      ],

之后,如果要测试titleComponent.populate,则可以为其创建单独的单元测试:)。

答案 1 :(得分:0)

我通常为子组件创建组件存根。那只包含我需要的输入,然后调用我想查看的所有功能。

我将像下面这样的代码转储到我的spec文件的底部(如果打算再次使用它,则将其转储到共享位置),并将'MockTitleComponent'添加到我的声明中。从那里,您应该可以照常进行间谍活动和期望。

@Component({
    selector: 'app-title-component',
    template: '<p>Mock App Title Component</p>'
})
class MockTitleComponent{
    @Input()
    Input1;
    @Input()
    Input2;

    testFunction(){}
}

答案 2 :(得分:0)

谢谢Kyle Anderson。不幸的是,它不能解决我的问题。

感谢Walter Gómez Milán评论,我设法解决了问题。当我添加

component.titleComponent = TestBed.get(TitleComponent);

它修复了它。

最终解决方案如下:

import {AuthenticationService, PageLoadingService, UserFiltersService, UserService} from '@core/services';
import {UserLogin} from '@core/models';
import {UserFiltersComponent} from './user-filters.component';
import {TranslateTestingModule} from 'src/app/test-utils';
import {SearchComponent} from '@feature/administration/user/user-filters/search';
import {TitleComponent} from '@feature/administration/user/user-filters/title';
import {SkillsComponent} from '@feature/administration/user/user-filters/skills';
import {EnglishLevelComponent} from '@feature/administration/user/user-filters/english-level';
import {LocationComponent} from '@feature/administration/user/user-filters/location';
import {WorkComponent} from '@feature/administration/user/user-filters/work';
import {EducationComponent} from '@feature/administration/user/user-filters/education';
import {SocialProfileComponent} from '@feature/administration/user/user-filters/social-profile';
import {RegisteredComponent} from '@feature/administration/user/user-filters/registered';
import {InvitationsComponent} from '@feature/administration/user/user-filters/invitations';
.
.
.
class FilterComponentStub {
  populate = () => {
  }
}
.
.
.
beforeEach(async(() => {
.
.
.providers: [
        {
          provide: UserFiltersService,
          useClass: UserFiltersServiceStub
        },
        {
          provide: PageLoadingService,
          useClass: PageLoadingServiceStub
        },
        {
          provide: AuthenticationService,
          useClass: AuthenticationServiceStub
        },
        {
          provide: UserService,
          useClass: UserServiceStub
        },
        {
          provide: MatDialog,
          useClass: MatDialogStub
        },
        {
          provide: SearchComponent,
          useClass: FilterComponentStub
        },
        {
          provide: TitleComponent,
          useClass: FilterComponentStub
        },
        {
          provide: SkillsComponent,
          useClass: FilterComponentStub
        },
        {
          provide: EnglishLevelComponent,
          useClass: FilterComponentStub
        },
        {
          provide: LocationComponent,
          useClass: FilterComponentStub
        },
        {
          provide: WorkComponent,
          useClass: FilterComponentStub
        },
        {
          provide: EducationComponent,
          useClass: FilterComponentStub
        },
        {
          provide: SocialProfileComponent,
          useClass: FilterComponentStub
        },
        {
          provide: RegisteredComponent,
          useClass: FilterComponentStub
        },
        {
          provide: InvitationsComponent,
          useClass: FilterComponentStub
        },
      ],
.
.
.
}
.
.
.
it('should populate the filters', () => {
    const filter = {
      id: '12345',
      name: 'filters test',
      value: {
        name: 'search',
        titles: [''],
        skills: [''],
        englishLevel: 1,
        locations: [''],
        educations: [''],
        works: [''],
        profiles: [''],
        registeredExact: null,
        registeredGte: null,
        registeredLte: null,
        invitationsExact: null,
        invitationsLte: null,
        invitationsGte: null
      }
    };

    component.searchComponent = TestBed.get(SearchComponent);
    spyOn(component.searchComponent, 'populate');
    component.titleComponent = TestBed.get(TitleComponent);
    spyOn(component.titleComponent, 'populate');
    component.skillsComponent = TestBed.get(SkillsComponent);
    spyOn(component.skillsComponent, 'populate');
    component.englishComponent = TestBed.get(EnglishLevelComponent);
    spyOn(component.englishComponent, 'populate');
    component.locationComponent = TestBed.get(LocationComponent);
    spyOn(component.locationComponent, 'populate');
    component.educationComponent = TestBed.get(EducationComponent);
    spyOn(component.educationComponent, 'populate');
    component.workComponent = TestBed.get(WorkComponent);
    spyOn(component.workComponent, 'populate');
    component.socialProfileComponent = TestBed.get(SocialProfileComponent);
    spyOn(component.socialProfileComponent, 'populate');
    component.registeredComponent = TestBed.get(RegisteredComponent);
    spyOn(component.registeredComponent, 'populate');
    component.invitationComponent = TestBed.get(InvitationsComponent);
    spyOn(component.invitationComponent, 'populate');

    component.populateFilters(filter);

    expect(component.searchComponent.populate).toHaveBeenCalled();
    expect(component.titleComponent.populate).toHaveBeenCalled();
    expect(component.skillsComponent.populate).toHaveBeenCalled();
    expect(component.englishComponent.populate).toHaveBeenCalled();
    expect(component.locationComponent.populate).toHaveBeenCalled();
    expect(component.educationComponent.populate).toHaveBeenCalled();
    expect(component.workComponent.populate).toHaveBeenCalled();
    expect(component.socialProfileComponent.populate).toHaveBeenCalled();
    expect(component.registeredComponent.populate).toHaveBeenCalled();
    expect(component.invitationComponent.populate).toHaveBeenCalled();
  });

然后所有测试都在运行。