How to check inner component's method is called during tests?

1.2k Views Asked by At

I have a component (MapComponent) that calls external API via HexesService. Results of Data of this API call are loaded to some buttons (View Map, Edit Map). I have another internal component (HexesComponent) and clicking on a View button makes a call to this internal component (which in turn makes another API call and displays the data inside of HexesComponent).

I've created a test with a mock of external service. My test checks that after the main component is loaded it makes call to the mocked service and loads data properly (all buttons are populated). This works perfectly fine.

The next thing I'd like to check that click on the button makes a call to a method of the HexesComponent.

The problem I have is that my internal component is always undefined. I've created a stub for my internal component, but this did not help: even the stab is empty.

Question #1: What do I do wrong? I've tried using async for the method inside the 'beforeEach', this did not help.

Needless to say that outside of 'tests', the functionality works perfectly fine.

Question #2: How to validate that clicking on the button leads to the call of 'loadMap' method of the HexComponent?

Here is the code of my test:

class MockHexesService extends HexesService {
  getDataForButtons(){
    return of(...);
  }
  ...
}

@Component({ selector: 'app-hex', template: '' })
class HexStubComponent {
   public loadMap = jasmine.createSpy('loadMap');
}

describe('MapsComponent', () => {
  let component: MapsComponent;
  let hexComponent: HexStubComponent;
  let fixture: ComponentFixture<MapsComponent>;
  let componentService: HexesService;
  let hexComponentSpy: any;

  beforeEach(() => {
    TestBed.configureTestingModule({
      imports: [HttpClientModule],
      declarations: [MapsComponent, HexStubComponent],
      providers: [HexesService],
    }).compileComponents();

    TestBed.overrideComponent(MapsComponent, {
      set: {
        providers: [{ provide: HexesService, useClass: MockHexesService }],
      },
    });

    fixture = TestBed.createComponent(MapsComponent);
    component = fixture.componentInstance;
    componentService = fixture.debugElement.injector.get(HexesService);

    fixture.detectChanges();

    hexComponent = fixture.debugElement.injector.get(HexStubComponent);
    hexComponentSpy = spyOn(hexComponent, 'loadMap');
  });

  it('Loaded map descriptions should load data for buttons', () => {
    const tblRows =
      fixture.debugElement.nativeElement.querySelectorAll('table tr');
    const cells0 = tblRows[0].querySelectorAll('td');
    expect(cells0[0].innerHTML).toBe('Map 1');
    ... checking other buttons are populated properly

    expect(component.hexComponent).toBeTruthy(); // - this fails here
  });

  it('Click on "View" button should load the map', async(() => {
    //spyOn(component.hexComponent.loadMap)
    const btns = fixture.debugElement.nativeElement.querySelectorAll('button');
    expect(btns.length).toBe(6);
    const btnView = btns[0];
    expect(btnView.innerHTML).toBe('View Map');
    btnView.click();

    fixture.whenStable().then(() => {
      expect(component.hexComponent).toBeTruthy(); // - this fails 
      // and how do I check that 'loadMap' method of hexComponent is called with proper data?
    });
  }));
});

Essential part of maps.component.html:

...
<tr *ngFor="let d1 of data">
  ...    
  <button (click)="loadMap(d1.mapId)">View Map</button>
  ...
</tr>
<app-hex id="hexComponent"></app-hex>

Essential part of maps.component.ts:

export class MapsComponent implements OnInit {
  @ViewChild(HexComponent) hexComponent: HexComponent;

  constructor(public ngZone: NgZone, private hexesService: HexesService) {}

  ngOnInit(): void {
    this.hexesService
      .getDataForButtons()
      .subscribe((data: any) => this.populateButtons(data));
  }

  loadMap(mapId): void {
    this.hexComponent.loadMap(mapId);
  }

P.S. I've found a similar question (How can I use a fake/mock/stub child component when testing a component that has ViewChildren in Angular 10+?) which suggests to use ng-mocks, but could not make them work.

P.P.S. I've tried to use

@Component({ selector: 'app-hex', template: '' })
class HexStubComponent {
  //public loadMap(mapId: number) {}
  public loadMap = jasmine.createSpy('loadMap');
}

and use HexStubComponent in declarations for 'TestBed.configureTestingModule', but my test gives error:

NullInjectorError: R3InjectorError(DynamicTestModule)[HexStubComponent -> HexStubComponent]: NullInjectorError: No provider for HexStubComponent!

Can't figure out how to resolve that.

2

There are 2 best solutions below

1
On BEST ANSWER

Rather than pushing for an option to use Stabs, I decided to give it another try and make it work with ng-mocks.

Apparently, my question is a close to Mocking Child Components - Angular 2 and the good guidance is https://www.npmjs.com/package/ng-mocks/v/11.1.4 The difference is in my case, I also need to ensure I call some methods of the child component.

To help someone else (and maybe myself in future) here is a list of issues I've encountered on my way.

  • Install ng-mocks using "npm install ng-mocks --save-dev" command

  • Add import lines (obvious, but VS Code did not autosuggest those and I struggled to realize how to get access to exact classes I needed):

    import { MockComponent, MockInstance, ngMocks } from 'ng-mocks';
    
  • Add the mock of your component inside of async version of TestBed.configureTestingModule:

    declarations: [MapsComponent, MockComponent(HexComponent)],
    
  • Create a spy on the component and method you need to check:

    beforeEach(() => MockInstance(HexComponent, 'loadMap', jasmine.createSpy()));
    
  • Validation:

    it('Click on "View" button should load the map', async(() => {
      const hexComponent = ngMocks.findInstance(HexComponent);
      expect(hexComponent).toBeDefined();
      expect(hexComponent.loadMap).not.toHaveBeenCalled();
    
      const btns = fixture.debugElement.nativeElement.querySelectorAll('button');
      expect(btns.length).toBe(6);
      const btnView = btns[0];
      expect(btnView.innerHTML).toBe('View Map');
    
      btnView.click();
    
      fixture.whenStable().then(() => {
        expect(hexComponent.loadMap).toHaveBeenCalled();
      });
    }));
    

The only problem I did not resolve yet is mock some services use by HexComponent. I'll try this a bit later.

1
On

Angular @ViewChild and other queries are based on either id or ctor.

Because HexStubComponent isn't HexComponent, queries cannot find it.

In order to fix it, as you said, you could use ng-mocks and MockComponent or MockBuilder, then the lib would do all the setup.

Or you can tell Angular that HexStubComponent should be provided when HexComponent is requested:

@Component({ selector: 'app-hex', template: '',

  // this is the change
  providers: [
    {
      provide: HexComponent,
      useExisting: HexStubComponent,
    },
  ],
  // the end of the change

})
class HexStubComponent {
   public loadMap = jasmine.createSpy('loadMap');
}

This is what ng-mocks does under the hood and should help you to achieve the desired behavior.

Regarding the second question. It sounds like a test of HexComponent itself, because the desired expectation is to ensure how HexComponent behavior, whereas here we have its mock.

In the current test, I would suggest to cover that getDataForButtons has been called on the click, but not what it does. What it does should be a different test suite.