Angular Component: unit testing

If Angular services are quite easy to tests (the main difficulty being /what to test/), Component testing deals with more challenges. This post sums up some key points.

Please note that we are using ngx-speculoos to do so, which offers some friendly syntaxic sugars to write the tests. Please go through the presentation of the package before reading forth.

What to test ?

When testing the Component-Under-Test, here are some suggestions about what to test:

  • the presence of HTMLElement or SubComponents in the generated template (potentially, depending on the CUT inputs, some user actions, etc.): for this, syntaxic sugars in the CTester (ngx-speculoos) are welcome.
  • the Services methods called by the CUT (at initialization, on a button push, etc.): this is done in a usual manner, via Services mock
  • the inputs provided to the SubComponent: the subcomponents are not fully tested here (they are tested in their own unit tests), we only tests that we provide them with the right inputs
  • the behaviour of the CUT when the subcomponent outputs: here also, we need to emulate the output emissions of the mocked subcomponents

Component-Under-Test lifecycle hooks

ngOnInit()

When writing tests, you might want to perform some actions before the actual initialization of the CUT (before its ngOnInit() is called), such as: setting up spies on related services, setting up CUT inputs, etc.

It appears the Component actually initializes with the first tester.detectChanges(); occurrence (tester is a CTester, see ngx-speculoos). For desambiguation purposes, we suggest to create a initComponent function, to call at the appropriate time in your tests:

  function initComponent(): void {
    tester.detectChanges();
  }

ngOnChanges()

Unfortunately, ngOnChanges will not be called every time you update the CUT inputs. This is something to be done manually, with laborious code such as:

component.ngOnChanges({
  quantity: new SimpleChange(null, { id: 22 }, false)
});
// instead of
// component.quantity = { id: 22 };

SubComponents references

You’ll have to mock all sub-components used by the Component-Under-Test (the tests will actually run correctly without them, only displaying console errors and warnings). Yet, you’ll soon want to check at the inputs of these sub-components (to check the CUT has provided them with the right data), as well as emulate their outputs (to check the CUT behaves appropriately in these cases).

The fixture created by TestBed helps us get a reference to any subcomponents, and fortunately ngx-speculoos keeps that functionality. In the definition of your CTester, you can implement this kind of accessor:

class CTester extends ComponentTester<StatisticsPanelComponent> {
  // ...
  get selectPeriodComponent(): TimeAbsoluteComponentMock {
    const debugElement = this.fixture.debugElement.query(By.directive(TimeAbsoluteComponentMock));
    // this select a component by its Component name
    // to select by other means, please refer to the DebugElement query documentation
    return debugElement.componentInstance as TimeAbsoluteComponentMock;
  }
  // ...
}

Then in your test:

it('should give its child appropriate inputs', () => {
  // ... test setup ...
  expect(tester.selectPeriodComponent.suggestTime).toBeFalse();
}
it('should behave appropriately when its child outputs', () => {
  tester.selectPeriodComponent.periodChanged.next({type: PeriodType.Absolute});
  expect(......);
}

Mocking Pipes

To mock pipes (especially custom pipes), one solution is to override the mocked pipe prototype directly. It’s a bit of a brutal solution, but it works.

First, create a mock of the pipe, which we will declare in the TestBed options, so it’s accessible from the CUT:

@Pipe({ name: 'svUserSetting', pure: false })
export class SvUserSettingPipeMock {
  transform(obj: unknown): unknown | null {
    return null;
  }
}

The mock will directly be used by the CUT, as it’s the only directives matching the name ‘svUserSetting’ declared within TestBed. Now, we’ll hijack its behavior to test different scenarii:

beforeEach(() => { // ... or directly in a test
  spyOn(SvUserSettingPipeMock.prototype, 'transform').and.returnValue(true);
});


Leave a Reply

Your email address will not be published. Required fields are marked *