简体   繁体   中英

Cannot mock a child component in an Angular test

I have a test suite in Angular, where I am trying to test a component that has several child components. I need to assert that the child's methods are being called when the parent method is called.

The class I am implementing is as follows:

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

And the test that I wrote for this code is this:

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

Up to this point, everything works fine. The problem is when I try to add the rest of the components that are childs:

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

Then I am getting the following error:

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)

I realize that something I am doing wrong that the constructor is trying to build the FormBuilder, but that is not mocked. My point is that I do not need to mock this. I just need to assert that the method 'populate' was called, since in every component tests have been created to test the populate methods. How can I mock this child component just to assert the method has been called?

IMHO... if you are going to unit test a method, try to isolate that method from external dependencies. This way, you tackle complexity (eg, unneeded injector initialization) and potential errors. Mocking is my preferred option in such cases. So in this case, I'd suggest separate concerns this way:

First, the mock class:

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

Then add the provider to TitleComponent

        ,
        {
          provide: TitleComponent,
          useClass: TitleComponentStub
        },

Inside your test add a line:

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

Remove TitleComponent from here:

declarations: [
        UserFiltersComponent,
        SearchComponent,
        TitleComponent
      ],

After that, if you want to test titleComponent.populate , you may create a separate unit test for it:).

Thanks, Kyle Anderson for your answer. Unfortunately, it did not fix my problem.

Thanks to Walter Gómez Milán comment, I managed to fix my problem. When I added

component.titleComponent = TestBed.get(TitleComponent);

it fixed it.

The final solution looks like this:

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

Then all the tests are running.

I generally create component stubs for my child components. That just contain the inputs I need and then any functions I want to see were called.

I'll just dump code like the below at the bottom of my spec file (or a shared location if I plan on using it again) and add 'MockTitleComponent' to my declarations. From there you should be able to do your spys and expects as normal.

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

    testFunction(){}
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM